자바스크립트 코드를 위에서 아래로 작성해도, 비동기 코드는 작성한 순서와 다르게 실행될 때가 있습니다.
API 호출 결과가 오기 전에 다음 코드가 먼저 실행되어 undefined가 출력되는 경우가 그 예입니다.
이 글에서는 자바스크립트에서 비동기 작업을 다루는 Promise와 이를 사용하는 async/await를 설명합니다.
Promise 객체
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.
자바스크립트의 비동기 함수는 동기 함수처럼 결과값을 즉시 반환하지 않습니다. 대신 미래에 값을 제공하겠다는 '약속'인 Promise 객체를 즉시 반환합니다.
Promise의 세 가지 상태
"Promise 객체의 상태 전이도"
Promise 객체는 작업의 진행 상황에 따라 세 가지 상태 중 하나를 가집니다.
- Pending (대기): 이행되지도 거부되지도 않은 초기 상태입니다.
- Fulfilled (이행): 비동기 작업이 성공적으로 완료된 상태입니다.
- Rejected (거부): 비동기 작업이 실패한 상태입니다.
이미 Fulfilled나 Rejected가 된 상태를 **Settled(결정됨)**라고 부릅니다.
한번 Settled된 Promise는 다시 상태가 변하지 않습니다. 또한, 이미 결정된 Promise에 나중에 추가한 핸들러도 상태에 맞으면 그 결과를 받아 실행되므로 레이스 컨디션을 걱정할 필요가 없습니다.
Then 메서드
then은 Promise 객체의 기본 메서드입니다. 비동기 작업이 성공했을 때와 실패했을 때 실행할 콜백 함수를 등록할 수 있습니다.
promise.then(onFulfilled, onRejected);Then의 반환값과 체이닝(Chaining)
then 메서드는 언제나 새로운 Promise 객체를 반환합니다. 이 특징 덕분에 여러 비동기 작업을 순차적으로 연결하는 '체이닝'이 가능해집니다.
반환된 새로운 Promise의 상태는 핸들러가 반환하는 값에 따라 달라집니다.
- 값을 반환할 때: 해당 값을 결과로 하여 즉시
Fulfilled상태가 됩니다. - 아무것도 반환하지 않을 때:
undefined를 결과로 하여Fulfilled상태가 됩니다. - 에러를 던질 때 (throw): 해당 에러를 사유로 하여
Rejected상태가 됩니다. - 또 다른 Promise를 반환할 때: 반환된 Promise가 결정될 때까지 기다린 후, 그 결과를 그대로 따릅니다.
Thenables
자바스크립트 공식 Promise가 등장하기 전에도 여러 라이브러리에서 자체적인 비동기 구현체를 사용했습니다.
이들은 최소한 then 메서드를 가진 객체 인터페이스를 구현했는데, 이를 Thenable이라고 부릅니다.
자바스크립트는 기존 라이브러리와 호환되도록 Thenable 인터페이스를 따르는 객체라면 무엇이든 Promise처럼 취급합니다.
콜백 지옥에서 탈출하기
Promise가 없던 시절에는 비동기 결과를 처리하기 위해 콜백 함수를 직접 인자로 넘겨야 했습니다.
// 과거의 방식: 함수를 호출할 때 성공/실패 콜백을 함께 넘깁니다.
createAudioFileAsync(audioSettings, successCallback, failureCallback);문제는 비동기 작업이 연속될 때 발생합니다. 흔히 이 문제를 **'콜백 지옥(Callback Hell)'**이라고 부릅니다.
// 콜백 지옥: 코드가 오른쪽으로 점점 밀려나며 가독성이 크게 떨어집니다.
doSomethingCB((result) => {
doSomethingElseCB(
result,
(newResult) => {
doThirdThingCB(
newResult,
(finalResult) => {
console.log(`최종 결과: ${finalResult}`);
},
failureCallbackCB,
);
},
failureCallbackCB,
);
}, failureCallbackCB);Promise를 사용하면 이 구조를 선형적으로 바꿀 수 있습니다.
// Promise Chaining: 훨씬 읽기 편해졌습니다.
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => console.log(`최종 결과: ${finalResult}`))
.catch((error) => console.error(error));async/await 문법
async와 await는 ES2017에서 도입된 문법으로, 비동기 코드를 동기 코드처럼 읽히게 합니다.
async 함수
async키워드가 붙은 함수는 항상 Promise를 반환합니다.- 함수 내부에서 일반 값을 반환하면, 자바스크립트는 이를 자동으로
FulfilledPromise로 감쌉니다.
await 표현식
await는 오직async함수 내부에서만 사용할 수 있습니다.- Promise가 결정될 때까지 함수의 실행을 잠시 중단시키고, Promise의 결과값을 반환합니다.
- 실제 스레드가 멈추는 것이 아니라 해당 함수만 일시 정지되므로, 그동안 이벤트 루프가 다른 작업을 처리할 수 있습니다.
async function process() {
try {
// 동기 코드처럼 위에서 아래로 읽힙니다.
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`최종 결과: ${finalResult}`);
} catch (error) {
// 일반적인 try-catch로 비동기 에러를 처리합니다.
console.error(error);
}
}async/await는 앞에서 본 then 체이닝보다 직관적이며, 비동기 에러도 동기 코드처럼 try-catch로 처리할 수 있습니다.
정리하면 비동기 함수는 결과 대신 결과를 나중에 전달할 Promise를 먼저 반환합니다. then과 async/await는 그 Promise의 결과를 받아 처리하는 방법입니다.