본문 바로가기

Coding/TIL (Today I Learned)

JavaScript: Promise, async & await 이해

이전 과제를 진행하던 중에 Promise라는 개념이 이미 몇 차례 등장했었는데 그때는 그때 당장 익혀야 할 다른 것들이 많아서 프로미스는 공부하지못했다. 나중에 다루게 될 개념이라고 들었기 때문에 이런 게 있구나 정도로 넘겼음. 하지만 자바스크립트에서 비동기 처리는 중요한 부분이기 때문에 이번에 정리를 해 둔다.

 

레퍼런스:

MDN: Using promises

캡틴판교: 자바스크립트 Promise 쉽게 이해하기


비동기 처리

자바스크립트의 비동기 처리란 특정 코드의 연산이 끝날 때까지 기다리지 않고 다음 코드를 먼저 실행하는 것.(non blocking)

비동기 처리의 예로는 setTimeout(), ajax 통신을 들 수 있다. 이런 특징은 코드를 실행하는 시간을 효율적으로 쓸 수 있지만, 실행되는 순서를 예측할 수 없다는 단점이 있다.

 

아래 코드의 결괏값은 A, B, C 가 순차적으로 나오지 않는다.

const printString = (string) => {
    setTimeout(
        () => {
            console.log(string)
        }, 
        Math.floor(Math.random() * 100) + 1
      )
    }

    const printAll = () => {
        printString("A")
        printString("B")
        printString("C")
    }

printAll() // 결과 값은?

의도한 순서대로 코드를 실행하기 위해서는 위의 코드에 콜백 인자를 넣어서 이렇게 수정할 수 있다.

const printString = (string, callback) => {
    setTimeout(
        () => {
            console.log(string)
            callback() //setTimeout 내에서 콜백 실행
        }, 
        Math.floor(Math.random() * 100) + 1
      )
    }

    const printAll = () => {
        printString("A", () => { 
            printString("B", () => { 
                printString("C", () => {}) 
            })
        })
    } 
    
    printAll() // A B C가 순서대로 출력됨.

 

코드가 실행되는 순서를 원하는 대로 제어하기 위해 Callback을 활용할 수 있지만, 콜백 함수를 연속해서 사용할 때 콜백 지옥이 생길 수 있다. 웹이 고도화될수록 이런 연속적인 콜백이 필요하게 되었고, 이런 코드 구조는 가독성이 떨어지고 변경 등의 관리가 몹시 어렵다.

콜백 지옥의 해결책으로 등장하는 것이 Promise와 async & await.

 

Promise란?

프로미스는 자바스크립트 비동기 처리에 사용되는 객체를 말한다.

new Promise()로 프로미스를 생성하고 resolve()와 reject() 메서드를 통해서 다음 액션을 진행하거나, 에러 핸들링을 할 수 있다.

위의 콜백을 이용한 코드를 프로미스로 변경하면 더 가독성 좋은 코드를 작성할 수 있다.

const printString = (string) => {
    return new Promise((resolve, reject) => {
        setTimeout(
            () => {
                console.log(string)
                resolve() //콜백대신 resolve
            }, 
            Math.floor(Math.random() * 100) + 1
          )
       })
    
    }

const printAll = () => {
    printString("A")
    .then(() => {
        return printString("B")
    })
    .then(() => {
        return printString("C")
    })
    .then(() => {
        return printString("D")
    })
    // ... 
} 

printAll() // A B C D 순서대로 출력됨

무엇보다 에러 핸들링을 할 때 콜백을 사용한 패턴에 비해 훨씬 편한 장점이 있다.

위의 코드에서 보듯이 프로미스는 .then 으로 여러 개의 프로미스를 연결하여 사용할 수 있다.

then() 메서드를 호출하고 나면 새로운 프로미스 객체가 반환되기 때문에 계속 연결해서 사용이 가능함. (Promise Chaining)

 

프로미스는종료될 때까지 3가지 상태(states)를 갖는데, 여기서 상태란 프로미스의 처리 과정을 의미한다.

-Pending(대기) :  비동기 처리 로직이 아직 완료되지 않은 상태

-Fulfilled(이행): 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태

-Rejected(실패): 비동기 처리가 실패하거나 오류가 발생한 상태

 

프로미스 처리 흐름 : 출처 MDN

 

점점 고도화되는 웹앱을 보면 비동기 처리 패턴은 앞으로도 더 많이 필요하게 될 것 같다.

많이 사용되는 패턴은 점점 더 단점을 보완한 코드를 작성할 수 있는 방법들이 나오게 마련이고 프로미스의 단점을 보완한 async & await가 등장하였으니,

Async & Await

비동기 함수를 async 함수로 만들기 위해서는 함수 앞에 async 키워드를 추가한다. async함수는 await키워드가 비동기 코드를 호출할 수 있게 해 준다. (await는 async함수 안에서만 쓸 수 있음. async함수를 실행하면 Promise를 반환함)

 

좋은 예시는 아니지만 위의 프로미스 코드에서 .then() 블록 대신 async & await를 사용해 보면, 

일단 코드가 짧고, 왼쪽 정렬된 코드에서 순서를 한눈에 알 수 있고, 겹겹의 괄호들로 인한 혼잡함이 없다.

const printString = (string) => {
        setTimeout(
            () => {
                console.log(string)
            }, 
            Math.floor(Math.random() * 100) + 1
          )
    }

const printAll = async () => {
    await printString("A")
    await printString("B")
    await printString("C")
} 

printAll() // A B C 순서대로 출력됨

 

예외처리 방식은 이어서 공부 후 추가 포스팅하기로!