프론트엔드

Promise 객체와 자바스크립트의 비동기 처리에 대하여

2026. 04. 14. 화요일 오후 9시 12분

Promise 객체와 비동기 처리

자바스크립트를 사용하다 보면 '비동기'라는 벽에 부딪히곤 합니다. 분명 코드는 위에서 아래로 작성했는데, 실행 결과는 제멋대로인 것처럼 느껴질 때가 있죠. 저 또한 처음 비동기를 접했을 때, API 호출 결과가 오기도 전에 다음 코드가 실행되어 undefined를 뿜어내던 화면을 보며 당황했던 기억이 납니다.

이번 포스트에서는 자바스크립트 비동기 처리의 핵심인 Promise와 이를 더 우아하게 사용하는 async/await에 대해 알아보겠습니다.


Promise 객체란?

프로미스(Promise)는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 쉽게 비유하자면, 식당에서 주문 후 받은 **'진동벨'**과 같습니다.

  • 음식을 주문하면(비동기 함수 호출), 점원은 진동벨(Promise)을 줍니다.
  • 아직 음식은 나오지 않았지만(Pending), 언젠가 음식이 준비되거나(Fulfilled) 재료가 떨어져 주문이 취소(Rejected)될 것임을 약속받은 상태입니다.
  • 우리는 진동벨을 가지고 자리에 가서 다른 일을 하다가, 벨이 울리면 음식을 받으러 가면 됩니다.

자바스크립트의 비동기 함수는 동기 함수처럼 결과값을 즉시 리턴하지 않습니다. 대신 미래에 값을 제공하겠다는 '약속'인 Promise 객체를 즉시 리턴합니다.

Promise의 세 가지 상태

Promise 객체의 상태 전이도"Promise 객체의 상태 전이도"

프로미스 객체는 작업의 진행 상황에 따라 세 가지 상태 중 하나를 가집니다.

  1. Pending (대기): 이행하거나 거부되지 않은 초기 상태입니다.
  2. Fulfilled (이행): 비동기 작업이 성공적으로 완료된 상태입니다.
  3. Rejected (거부): 비동기 작업이 실패한 상태입니다.

이미 FulfilledRejected가 된 상태를 **Settled(결정됨)**라고 부릅니다. 한번 Settled된 프로미스는 다시 상태가 변하지 않습니다. 또한, 이미 결정된 프로미스에 핸들러를 추가하더라도 결과는 보장되므로 레이스 컨디션을 걱정할 필요가 없습니다.


Then 메서드: 약속을 이행하는 방법

then은 프로미스 객체의 핵심 메서드입니다. 비동기 작업이 성공했을 때와 실패했을 때 실행할 콜백 함수를 등록할 수 있습니다.

1promise.then(onFulfilled, onRejected);
2

Then의 반환값과 체이닝(Chaining)

then 메서드는 언제나 새로운 Promise 객체를 반환합니다. 이 특징 덕분에 여러 비동기 작업을 순차적으로 연결하는 '체이닝'이 가능해집니다.

반환된 새로운 프로미스의 운명은 핸들러가 반환하는 값에 따라 달라집니다.

  1. 값을 반환할 때: 해당 값을 결과로 하여 즉시 Fulfilled 상태가 됩니다.
  2. 아무것도 반환하지 않을 때: undefined를 결과로 하여 Fulfilled 상태가 됩니다.
  3. 에러를 던질 때 (throw): 해당 에러를 사유로 하여 Rejected 상태가 됩니다.
  4. 또 다른 프로미스를 반환할 때: 반환된 프로미스가 처리될 때까지 기다린 후, 그 결과를 그대로 따릅니다.

Thenables

자바스크립트 공식 Promise가 등장하기 전에도 여러 라이브러리에서 자체적인 비동기 구현체를 사용했습니다. 이들은 최소한 then 메서드를 가진 객체 인터페이스를 구현했는데, 이를 Thenable이라고 부릅니다. 자바스크립트는 기존 라이브러리들과의 상호운용성을 위해, Thenable 인터페이스를 따르는 객체라면 무엇이든 프로미스처럼 취급해 줍니다.


콜백 지옥에서 탈출하기

Promise가 없던 시절에는 비동기 결과를 처리하기 위해 콜백 함수를 직접 인자로 넘겨야 했습니다.

1// 과거의 방식: 함수를 호출할 때 성공/실패 콜백을 함께 넘깁니다.
2createAudioFileAsync(audioSettings, successCallback, failureCallback);
3

문제는 비동기 작업이 연속될 때 발생합니다. 흔히 말하는 **'콜백 지옥(Callback Hell)'**이죠.

1// 콜백 지옥: 코드가 오른쪽으로 점점 밀려나며 가독성이 바닥을 칩니다.
2doSomethingCB((result) => {
3  doSomethingElseCB(
4    result,
5    (newResult) => {
6      doThirdThingCB(
7        newResult,
8        (finalResult) => {
9          console.log(`최종 결과: ${finalResult}`);
10        },
11        failureCallbackCB,
12      );
13    },
14    failureCallbackCB,
15  );
16}, failureCallbackCB);
17

Promise를 사용하면 이 구조를 선형적으로 바꿀 수 있습니다.

1// Promise Chaining: 훨씬 읽기 편해졌습니다.
2doSomething()
3  .then((result) => doSomethingElse(result))
4  .then((newResult) => doThirdThing(newResult))
5  .then((finalResult) => console.log(`최종 결과: ${finalResult}`))
6  .catch((error) => console.error(error));
7

async / await: 비동기를 동기처럼

asyncawait는 ES2017에서 도입된 문법으로, 비동기 코드를 마치 동기 코드처럼 읽히게 만들어줍니다.

async 함수

  • async 키워드가 붙은 함수는 항상 Promise를 반환합니다.
  • 함수 내부에서 일반 값을 리턴하면, 자바스크립트는 이를 자동으로 Fulfilled 프로미스로 감싸줍니다.

await 표현식

  • await는 오직 async 함수 내부에서만 사용할 수 있습니다.
  • 프로미스가 처리될 때까지 함수의 실행을 잠시 중단시키고, 프로미스의 결과값을 리턴합니다.
  • 실제 스레드가 멈추는 것이 아니라, 해당 함수만 일시 정지 상태가 되어 이벤트 루프가 다른 작업을 처리할 수 있게 합니다.
1async function process() {
2  try {
3    // 마치 동기 코드처럼 한 줄씩 실행되는 것처럼 보입니다.
4    const result = await doSomething();
5    const newResult = await doSomethingElse(result);
6    const finalResult = await doThirdThing(newResult);
7
8    console.log(`최종 결과: ${finalResult}`);
9  } catch (error) {
10    // 일반적인 try-catch로 비동기 에러를 잡을 수 있습니다!
11    console.error(error);
12  }
13}
14

이전의 then 체이닝보다 훨씬 직관적이며, 무엇보다 try-catch를 통해 비동기 에러 처리를 일관되게 할 수 있다는 점이 가장 큰 매력입니다.

설명이 좀 거창했을지도 모르겠습니다만, 쉽게 생각하면 비동기는 **"나중에 알려줄게!"**라고 약속하는 것이고, Promise와 async/await는 그 약속을 **"어떻게 하면 더 편하게 받을 수 있을까?"**에 대한 고민의 산물이라고 이해하시면 좋을 것 같습니다.