본문 바로가기

Promise와 async/await

Promise의 대기와 이행, 거부 상태와 then 체인의 값 전달, async 함수와 await가 실행을 이어 가는 순서를 출력으로 추적하고, unknown으로 받는 거부 값과 회복과 전달의 차이, 목적에 따라 Promise.all과 allSettled, race, any를 고르는 기준을 타입과 함께 다룹니다.

자바스크립트의 코드는 한 실행 흐름에서 한 번에 하나씩 실행됩니다. 그런데 네트워크 요청이나 파일 읽기, 타이머처럼 결과가 나중에 나오는 작업을 기다리느라 그 흐름을 멈추면, 화면이 굳거나 서버가 다른 요청에 응답하지 못합니다. 그래서 이런 작업은 결과를 기다리지 않고 시작만 해 두었다가, 결과가 나오면 이어서 처리하는 비동기(asynchronous) 방식으로 다룹니다.

예전에는 결과가 나오면 부를 함수를 넘기는 콜백(callback)으로 이를 처리했지만, 콜백이 겹겹이 들어가면 읽기 어려웠습니다. ES2015의 Promise와 ES2017의 async, await는 비동기 작업을 값처럼 다루고 순서대로 읽히게 해 주며, 타입스크립트는 Promise<T>로 그 결과의 타입을 함께 검사합니다. 이 글은 Promise의 상태와 체인, async 함수가 실행을 이어 가는 순서, 오류의 처리, 여러 작업을 함께 기다리는 방법을 차례로 봅니다.

Promise의 상태

Promise는 비동기 작업의 결과를 나중에 받을 수 있게 담아 두는 객체입니다. 처음에는 대기(pending) 상태이고, 작업이 성공하면 결과 값과 함께 이행(fulfilled) 상태로, 실패하면 거부 사유와 함께 거부(rejected) 상태로 바뀝니다. 한 번 이행되거나 거부된 Promise는 다시 바뀌지 않으며, 이렇게 결과가 정해진 것을 정착(settled)했다고 합니다.

const myPromise = new Promise<string>((resolve, reject) => {
  console.log("executor 실행");
  setTimeout(() => {
    resolve("데이터를 가져왔습니다!");
  }, 100);
});

myPromise
  .then((message) => console.log("성공:", message))
  .catch((reason: unknown) => console.log("실패:", reason));

console.log("Promise 작업 시작");
executor 실행
Promise 작업 시작
성공: 데이터를 가져왔습니다!

new Promise에 넘기는 함수를 실행자(executor)라고 하며, 생성자는 이 함수를 그 자리에서 바로 부르면서 resolve와 reject 두 함수를 넘겨줍니다. 그래서 executor 실행이 가장 먼저 출력되고, 타이머를 등록한 뒤 다음 줄의 Promise 작업 시작이 출력됩니다. 약 0.1초 뒤 타이머가 resolve를 부르면 Promise가 이행되고, then에 등록한 함수가 그 값을 받아 실행됩니다.

Promise 정착 뒤 reaction microtask가 실행을 재개한다 — 상태 추적

Promise 정착과 반응 작업 등록/실행, await 함수의 정지/재개가 별개 상태다. resolved와 fulfilled를 혼동하지 않게 보여 줄 가치가 있다.

pending에서 성공·거부 두 상태로 분기하고 각각 reaction job을 통해 continuation으로 연결된다. thenable 채택은 즉시 fulfillment와 구별한다.

PROMISE / STATE AND REACTION

정착 상태와 then·await의 후속 실행을 구분한다

executor는 Promise를 만들 때 동기 호출됩니다. Promise는 pending에서 fulfilled 또는 rejected로 한 번 정착하며, then/await의 후속 실행은 Promise reaction job으로 예약됩니다.

정착 상태와 then·await의 후속 실행을 구분한다pending에서 성공·거부 두 상태로 분기하고 각각 reaction job을 통해 continuation으로 연결된다. thenable 채택은 즉시 fulfillment와 구별한다.pending아직 정착 전fulfilled성공 값rejected거부 이유reaction job후속 실행 예약후속 실행then / await continuation
Promise의 정착 분기와 후속 실행Pending에서 성공하면 Fulfilled, 실패하면 Rejected로 정착한다. 두 정착 결과에서 Reaction job으로 연결되어 then 또는 await의 이어지는 코드가 실행된다. 세로로 배치한 성공·실패 상자는 서로 잇는 단계가 아니라 선택적인 두 결과다.Pending아직 정착 전Fulfilled성공 값Rejected거부 이유Reaction job후속 실행 예약후속 실행then / await이어지는 코드

CREATE

executor는 동기 호출

교재의 new Promise는 타이머를 등록하고 곧 반환합니다. 타이머 작업이 끝나는 시점과 executor 반환 시점은 다릅니다.

RESOLUTION

resolve는 thenable을 채택한다

resolve에 다른 Promise/thenable을 주면 그 최종 상태를 따릅니다. 첫 resolve 호출 후에도 pending일 수 있으며 뒤의 reject로 선택을 바꿀 수 없습니다.

REACTION

이미 정착해도 후속 실행은 예약

then 콜백과 await 뒤 코드는 현재 동기 실행을 막지 않습니다. await는 해당 async 함수의 후속 실행을 중단·재개합니다.

ERROR AND FINALLY

후속 Promise의 결과도 확인

catch가 값을 반환하면 회복할 수 있습니다. finally는 정상 완료하면 기존 결과를 유지하지만 throw나 거부로 새 실패를 만들 수 있습니다. Promise<T>는 거부 이유 타입을 지정하지 않습니다.

Promise 정착은 작업 취소와 다릅니다. 실제 취소 지원과 정리 책임을 별도로 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명catch가 값을 반환하면 회복할 수 있습니다. finally는 정상 완료하면 기존 결과를 유지하지만 throw나 거부로 새 실패를 만들 수 있습니다. Promise<T>는 거부 이유 타입을 지정하지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
pending과 awaitPromise: pending / 함수: continuation 대기Promise는 pending이고 async 함수가 await 뒤를 멈춘다현재 함수 실행과 나중 결과를 구분한다
정착과 반응 등록Promise: fulfilled 또는 rejected / reaction: 실행 전fulfillment 또는 rejection이 정해지고 반응 작업이 준비된다resolved가 pending thenable을 따르는 경우 아직 fulfilled가 아닐 수 있다
재개함수: 재개 / 결과: 값 또는 오류 처리checkpoint에서 then/await continuation이 값 또는 throw를 읽는다정착과 후속 사용자 코드 실행은 같은 순간이 아니다

Promise 정착 뒤 reaction microtask가 실행을 재개한다 — 상태 추적

Promise 정착과 반응 작업 등록/실행, await 함수의 정지/재개가 별개 상태다. resolved와 fulfilled를 혼동하지 않게 보여 줄 가치가 있다.

pending에서 성공·거부 두 상태로 분기하고 각각 reaction job을 통해 continuation으로 연결된다. thenable 채택은 즉시 fulfillment와 구별한다.

PROMISE / STATE AND REACTION

정착 상태와 then·await의 후속 실행을 구분한다

executor는 Promise를 만들 때 동기 호출됩니다. Promise는 pending에서 fulfilled 또는 rejected로 한 번 정착하며, then/await의 후속 실행은 Promise reaction job으로 예약됩니다.

정착 상태와 then·await의 후속 실행을 구분한다pending에서 성공·거부 두 상태로 분기하고 각각 reaction job을 통해 continuation으로 연결된다. thenable 채택은 즉시 fulfillment와 구별한다.pending아직 정착 전fulfilled성공 값rejected거부 이유reaction job후속 실행 예약후속 실행then / await continuation
Promise의 정착 분기와 후속 실행Pending에서 성공하면 Fulfilled, 실패하면 Rejected로 정착한다. 두 정착 결과에서 Reaction job으로 연결되어 then 또는 await의 이어지는 코드가 실행된다. 세로로 배치한 성공·실패 상자는 서로 잇는 단계가 아니라 선택적인 두 결과다.Pending아직 정착 전Fulfilled성공 값Rejected거부 이유Reaction job후속 실행 예약후속 실행then / await이어지는 코드

CREATE

executor는 동기 호출

교재의 new Promise는 타이머를 등록하고 곧 반환합니다. 타이머 작업이 끝나는 시점과 executor 반환 시점은 다릅니다.

RESOLUTION

resolve는 thenable을 채택한다

resolve에 다른 Promise/thenable을 주면 그 최종 상태를 따릅니다. 첫 resolve 호출 후에도 pending일 수 있으며 뒤의 reject로 선택을 바꿀 수 없습니다.

REACTION

이미 정착해도 후속 실행은 예약

then 콜백과 await 뒤 코드는 현재 동기 실행을 막지 않습니다. await는 해당 async 함수의 후속 실행을 중단·재개합니다.

ERROR AND FINALLY

후속 Promise의 결과도 확인

catch가 값을 반환하면 회복할 수 있습니다. finally는 정상 완료하면 기존 결과를 유지하지만 throw나 거부로 새 실패를 만들 수 있습니다. Promise<T>는 거부 이유 타입을 지정하지 않습니다.

Promise 정착은 작업 취소와 다릅니다. 실제 취소 지원과 정리 책임을 별도로 정합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명catch가 값을 반환하면 회복할 수 있습니다. finally는 정상 완료하면 기존 결과를 유지하지만 throw나 거부로 새 실패를 만들 수 있습니다. Promise<T>는 거부 이유 타입을 지정하지 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
pending과 awaitPromise: pending / 함수: continuation 대기Promise는 pending이고 async 함수가 await 뒤를 멈춘다현재 함수 실행과 나중 결과를 구분한다
정착과 반응 등록Promise: fulfilled 또는 rejected / reaction: 실행 전fulfillment 또는 rejection이 정해지고 반응 작업이 준비된다resolved가 pending thenable을 따르는 경우 아직 fulfilled가 아닐 수 있다
재개함수: 재개 / 결과: 값 또는 오류 처리checkpoint에서 then/await continuation이 값 또는 throw를 읽는다정착과 후속 사용자 코드 실행은 같은 순간이 아니다

위 그림처럼 then에 등록한 함수는 Promise가 정착하는 순간 그 자리에서 실행되지 않고, 지금 실행 중인 코드가 끝난 뒤 실행되도록 예약됩니다. 이렇게 예약된 작업을 마이크로태스크(microtask)라고 하며, 이미 이행된 Promise에 then을 붙여도 그 함수는 지금의 코드가 모두 끝난 다음에 실행됩니다. 또 resolve에 다른 Promise를 넘기면 그 Promise의 결과를 따르므로, resolve를 불렀다고 곧바로 이행되는 것은 아닙니다. 마이크로태스크가 타이머 같은 다른 작업과 어떤 순서로 실행되는지는 웹 기초 교재의 비동기 프로그래밍에서 자세히 다룹니다.

then 체인

then은 새 Promise를 돌려주므로, 여러 단계의 작업을 차례로 이어 붙일 수 있습니다. 각 then의 함수가 돌려준 값이 다음 then의 인수가 되고, 돌려준 값이 Promise라면 그 Promise가 정착할 때까지 기다린 뒤 결과를 넘깁니다.

function delay<T>(ms: number, value: T): Promise<T> {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms));
}

function fetchData(url: string): Promise<string> {
  return delay(100, `Fetched data from ${url}`);
}

function processData(data: string): Promise<string> {
  console.log(`[2] 처리할 데이터: ${data}`);
  return delay(150, data.toUpperCase());
}

fetchData("api/users")
  .then((data) => processData(data))
  .then((processed) => console.log(`[3] 저장할 데이터: ${processed}`))
  .catch((reason: unknown) => console.log("체인 오류:", reason));

console.log("[1] 비동기 체인 시작");
[1] 비동기 체인 시작
[2] 처리할 데이터: Fetched data from api/users
[3] 저장할 데이터: FETCHED DATA FROM API/USERS

processData는 fetchData의 결과를 받아야 시작할 수 있으므로, 이 체인은 두 작업을 차례로 실행합니다. 두 번째 then의 함수가 processData(data)를 돌려주었기 때문에, 세 번째 then은 그 Promise가 이행된 값인 대문자 문자열을 받습니다. 만약 return을 빠뜨리면 다음 단계에는 undefined가 넘어가고, 시작한 작업이 실패해도 그 실패는 이 체인에 이어지지 않습니다.

체인의 어느 단계에서든 거부되거나 함수가 예외를 던지면, 그 뒤의 then은 건너뛰고 가장 가까운 catch가 그 사유를 받습니다. finally에 등록한 함수는 성공과 실패에 관계없이 정착한 뒤 실행되며, 정상적으로 끝나면 앞의 결과를 그대로 넘깁니다.

function mightFail(shouldFail: boolean): Promise<string> {
  return shouldFail ? Promise.reject(new Error("네트워크 오류")) : Promise.resolve("데이터");
}

mightFail(true)
  .then((data) => console.log("데이터:", data))
  .catch((reason: unknown) => console.log("오류:", reason instanceof Error ? reason.message : reason))
  .finally(() => console.log("작업 종료"));
오류: 네트워크 오류
작업 종료

거부되었으므로 첫 then은 건너뛰고 catch가 오류의 메시지를 출력한 뒤, finally가 실행됩니다. mightFail(false)로 바꾸면 데이터: 데이터와 작업 종료가 출력됩니다. 거부된 Promise에 아무 처리도 붙이지 않으면, Node.js는 처리되지 않은 거부(unhandled rejection)로 보고 오류를 출력한 뒤 프로세스를 끝냅니다.

async 함수와 await

async를 붙인 함수는 언제나 Promise를 돌려줍니다. 함수 안에서 값을 return하면 그 값으로 이행되는 Promise가, 예외를 던지면 그 예외로 거부되는 Promise가 됩니다. 그리고 그 안에서 await를 쓰면 Promise가 정착할 때까지 그 함수의 나머지 부분을 멈추었다가, 결과를 받아 이어서 실행합니다. 다음 예제와 뒤의 Promise.all 예제는 앞 절의 delay 함수를 쓰므로, 그 함수를 같은 파일에 함께 두고 실행합니다.

async function fetchUserData(): Promise<string> {
  console.log("[1] 사용자 데이터 요청");
  const response = await delay(100, "사용자 정보: Alice");
  console.log("[3] 사용자 데이터 도착");
  return response;
}

async function processUser(): Promise<void> {
  const userData = await fetchUserData();
  await delay(50, undefined);
  console.log(`[4] ${userData} 처리 완료`);
}

console.log("[0] 호출 시작");
processUser();
console.log("[2] 호출 직후");
[0] 호출 시작
[1] 사용자 데이터 요청
[2] 호출 직후
[3] 사용자 데이터 도착
[4] 사용자 정보: Alice 처리 완료

processUser()를 부르면 그 안에서 fetchUserData()가 곧바로 실행되어 [1]이 출력되고, 첫 await에서 두 함수 모두 멈춘 채 호출한 쪽으로 Promise를 돌려줍니다. 그래서 [2]가 먼저 출력되고, 타이머가 끝난 뒤 멈추었던 부분이 이어서 실행됩니다. await가 멈추는 것은 그 async 함수의 나머지 부분일 뿐, 프로그램 전체가 아니라는 점이 중요합니다.

await는 async 함수 안에서 쓰며, ES 모듈에서는 파일의 최상위에서도 쓸 수 있습니다. 서로 기다릴 필요가 없는 작업까지 await로 하나씩 기다리면 전체 시간이 그 합만큼 길어지므로, 그런 작업은 뒤에서 볼 Promise.all로 함께 기다립니다.

거부 값과 회복

Promise<T>의 T는 이행될 때의 값의 타입일 뿐, 거부될 때의 사유는 문자열이든 Error든 아무 값이나 될 수 있습니다. 그래서 strict에 포함된 useUnknownInCatchVariables는 catch의 변수를 unknown으로 다루게 하며, 사유의 속성을 읽으려면 5장에서 본 것처럼 먼저 좁혀야 합니다.

async function safeMessage(work: Promise<string>): Promise<string> {
  try {
    return await work;
  } catch (reason) {
    const message = reason instanceof Error ? reason.message : String(reason);
    return `대체:${message}`;
  }
}

safeMessage(Promise.reject(new Error("접속 실패"))).then(console.log);
safeMessage(Promise.reject("접속 실패")).then(console.log);
대체:접속 실패
대체:접속 실패

try 안에서 await한 Promise가 거부되면 그 사유가 catch로 넘어옵니다. Error면 메시지를 읽고, 문자열 같은 다른 값이면 String으로 바꾸므로, 두 호출 모두 같은 문자열로 이행됩니다. catch에서 값을 돌려주면 실패를 대체 값으로 회복한 것이고, 기록만 하고 실패를 계속 알리려면 throw reason으로 다시 던져야 합니다. 오류를 기록한 것과 호출한 쪽에 실패를 전한 것은 서로 다른 일입니다.

타입이 외부의 값을 검증해 주지 않는다는 점도 같습니다. fetch로 받은 응답의 response.json()은 any를 돌려주므로, async function fetchProduct(): Promise<Product>처럼 반환 타입을 적어도 실제 데이터가 그 모양인지는 확인되지 않습니다. 응답은 unknown으로 받아 검증한 뒤에 그 타입으로 다룹니다.

여러 작업을 함께 기다리기

서로 기다릴 필요가 없는 작업은 모두 먼저 시작해 두고, 그 Promise들을 한꺼번에 기다립니다. Promise.all은 모든 Promise가 이행되면 결과를 입력과 같은 순서의 배열로 돌려주고, 하나라도 거부되면 그 사유로 곧바로 거부됩니다.

interface Post {
  id: number;
  title: string;
}

interface PostComment {
  id: number;
  text: string;
}

async function loadPostAndComments(postId: number): Promise<void> {
  const [post, comments] = await Promise.all([
    delay<Post>(100, { id: postId, title: `Post ${postId} Title` }),
    delay<PostComment[]>(150, [
      { id: 1, text: "Comment 1" },
      { id: 2, text: "Comment 2" },
    ]),
  ]);
  console.log("Post:", post);
  console.log("Comments:", comments);
}

loadPostAndComments(1);
Post: { id: 1, title: 'Post 1 Title' }
Comments: [ { id: 1, text: 'Comment 1' }, { id: 2, text: 'Comment 2' } ]

두 작업이 함께 진행되므로 전체는 0.1초와 0.15초를 더한 시간이 아니라, 더 긴 0.15초쯤에 끝납니다. 타입스크립트는 배열의 각 위치의 타입을 지켜 post를 Post로, comments를 PostComment[]로 추론합니다. Promise.all이 작업을 시작해 주는 것은 아니며, 배열을 만들면서 두 함수를 부르는 순간 이미 작업이 시작되었습니다.

목적에 따라 쓰는 조합 함수(combinator)가 다릅니다. Promise.allSettled는 모든 Promise가 정착할 때까지 기다려 각각의 성공과 실패를 기록한 배열을, Promise.race는 가장 먼저 정착한 Promise의 결과를, Promise.any는 가장 먼저 이행된 값을 돌려주며 모두 거부되어야만 거부됩니다.

필요한 완료 의미로 Promise 조합기를 고른다 — 상태 추적

A/B/C의 30/10/20 ms 정착 예제가 있어 각 결합 Promise의 대기·성공·실패가 시간에 따라 달라진다. 정책 표 전체를 보존하고 실제 값의 추적으로 보강할 수 있다.

네 Promise 조합기의 성공·실패 결과와 빈 입력, 취소 계약을 6카드로 비교한다.

PROMISE / RESULT SEMANTICS

Promise 조합기의 성공·실패·완료 의미를 비교한다

같은 Promise 목록이라도 필요한 결과가 다르면 조합기가 달라집니다. all의 첫 실패, allSettled의 전체 정착, race의 첫 정착, any의 첫 성공을 구별합니다.

동일한 이미 시작된 입력 Promise들을 네 독립 정책으로 관찰하며 기다리는 조건과 반환 내용을 비교한다.inputs→all: 전부 성공이 필요한 경우。inputs→settled: 모든 결과를 모으는 경우。all→all-result: 첫 B 거부가 결과를 정함。settled→settled-result: 마지막 A 후 전체 정착。inputs→race: 첫 정착 관찰。inputs→any: 첫 성공 관찰。race→race-result: 첫 정착 B 실패。any→any-result: B 실패를 지나 C 성공정책정책이미 시작된 A·B·C이미 시작된 A · B · CA 성공30 · B 실패10 · C 성공20allall전부 성공 필요allSettledallSettled전부 정착 대기첫 거부: B 오류첫 거부: B 오류작업 취소 안 함입력 순서 결과입력 순서 결과A · B · C각 성공 · 실패racerace첫 정착anyany첫 성공B 오류: 거부B 오류: 거부성공 · 거부 경주C 값: 성공C 값: 성공모두 거부 시모음 오류
동일한 이미 시작된 입력 Promise들을 네 독립 정책으로 관찰하며 기다리는 조건과 반환 내용을 비교한다.inputs→all: 전부 성공이 필요한 경우。inputs→settled: 모든 결과를 모으는 경우。all→all-result: 첫 B 거부가 결과를 정함。settled→settled-result: 마지막 A 후 전체 정착。inputs→race: 첫 정착 관찰。inputs→any: 첫 성공 관찰。race→race-result: 첫 정착 B 실패。any→any-result: B 실패를 지나 C 성공정책정책이미 시작된 A·B·C이미 시작된 A · B · CA 성공30 · B 실패10 · C성공20allall전부성공필요allSettledallSettled전부정착대기첫 거부: B 오류첫 거부:B 오류작업취소 안함입력 순서 결과입력순서결과A · B · C각 성공 ·실패racerace첫 정착anyany첫 성공B 오류: 거부B 오류:거부성공 ·거부경주C 값: 성공C 값:성공모두거부 시모음오류

빈 입력: all/allSettled→[], any→AggregateError, race→계속 pending. then/await는 인라인 실행이 아니다. 취소는 별도 협력 필요.

코드·조건·비교표와 보충 설명

ALL

모든 성공 값

모두 fulfilled면 입력 순서의 값 배열입니다. 하나라도 rejected면 그 첫 거부 이유로 거부합니다. 나머지 작업이 끝날 때까지 반드시 기다리는 방식은 아닙니다.

ALL SETTLED

모든 정착 결과

모두 정착한 뒤 입력 순서의 status/value 또는 reason을 받습니다. 입력 Promise의 실패도 결과 항목으로 보존합니다.

RACE

첫 정착

가장 먼저 정착한 성공 값 또는 거부 이유를 따릅니다. 빠른 실패도 결과가 될 수 있습니다.

ANY

첫 성공

처음 fulfilled된 값을 따릅니다. 전부 거부하면 입력 순서의 reasons를 가진 AggregateError로 거부합니다.

EMPTY INPUT

빈 목록도 의미가 다르다

all/allSettled는 빈 배열로 fulfilled, any는 AggregateError로 rejected, race는 pending으로 남습니다. then/await 후속 실행은 동기 inline 실행이 아닙니다.

CANCELLATION

작업 취소는 별도 계약

먼저 결과가 나와도 다른 작업은 자동 취소되지 않습니다. 필요한 경우 작업이 지원하는 AbortSignal 등의 협력 계약을 연결합니다.

Promise 조합은 결과를 집계합니다. CPU 병렬 실행이나 외부 작업 취소를 자동으로 보장하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 캡션네 Promise 조합기의 성공·실패 결과와 빈 입력, 취소 계약을 카드 6개로 비교한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
세 Promise pendingA/C: A 값/C 값 대기 / B: B 실패 대기A 30 ms/C 20 ms 성공과 B 10 ms 거부를 모두 시작한다조합 메서드는 이미 시작한 작업의 결과를 결합한다
B 거부 시점all/race: B 실패로 rejected / any/allSettled: 아직 pendingB 실패가 첫 settlement다race는 첫 성공이 아니라 첫 정착을 따른다
C 이행 시점any: C 값 fulfilled / allSettled: A 대기C 값이 첫 fulfilled 결과다any는 앞의 거부를 넘어 첫 성공을 채택한다
A 이행 뒤allSettled: A 성공/B 실패/C 성공 / all/race/any: 기존 결과 유지allSettled가 입력 순서 A/B/C로 결과를 내고 다른 조합 결과는 유지된다정착 속도와 결과 배열 순서는 다르며 작업은 자동 취소되지 않는다

필요한 완료 의미로 Promise 조합기를 고른다 — 상태 추적

A/B/C의 30/10/20 ms 정착 예제가 있어 각 결합 Promise의 대기·성공·실패가 시간에 따라 달라진다. 정책 표 전체를 보존하고 실제 값의 추적으로 보강할 수 있다.

네 Promise 조합기의 성공·실패 결과와 빈 입력, 취소 계약을 6카드로 비교한다.

PROMISE / RESULT SEMANTICS

Promise 조합기의 성공·실패·완료 의미를 비교한다

같은 Promise 목록이라도 필요한 결과가 다르면 조합기가 달라집니다. all의 첫 실패, allSettled의 전체 정착, race의 첫 정착, any의 첫 성공을 구별합니다.

동일한 이미 시작된 입력 Promise들을 네 독립 정책으로 관찰하며 기다리는 조건과 반환 내용을 비교한다.inputs→all: 전부 성공이 필요한 경우。inputs→settled: 모든 결과를 모으는 경우。all→all-result: 첫 B 거부가 결과를 정함。settled→settled-result: 마지막 A 후 전체 정착。inputs→race: 첫 정착 관찰。inputs→any: 첫 성공 관찰。race→race-result: 첫 정착 B 실패。any→any-result: B 실패를 지나 C 성공정책정책이미 시작된 A·B·C이미 시작된 A · B · CA 성공30 · B 실패10 · C 성공20allall전부 성공 필요allSettledallSettled전부 정착 대기첫 거부: B 오류첫 거부: B 오류작업 취소 안 함입력 순서 결과입력 순서 결과A · B · C각 성공 · 실패racerace첫 정착anyany첫 성공B 오류: 거부B 오류: 거부성공 · 거부 경주C 값: 성공C 값: 성공모두 거부 시모음 오류
동일한 이미 시작된 입력 Promise들을 네 독립 정책으로 관찰하며 기다리는 조건과 반환 내용을 비교한다.inputs→all: 전부 성공이 필요한 경우。inputs→settled: 모든 결과를 모으는 경우。all→all-result: 첫 B 거부가 결과를 정함。settled→settled-result: 마지막 A 후 전체 정착。inputs→race: 첫 정착 관찰。inputs→any: 첫 성공 관찰。race→race-result: 첫 정착 B 실패。any→any-result: B 실패를 지나 C 성공정책정책이미 시작된 A·B·C이미 시작된 A · B · CA 성공30 · B 실패10 · C성공20allall전부성공필요allSettledallSettled전부정착대기첫 거부: B 오류첫 거부:B 오류작업취소 안함입력 순서 결과입력순서결과A · B · C각 성공 ·실패racerace첫 정착anyany첫 성공B 오류: 거부B 오류:거부성공 ·거부경주C 값: 성공C 값:성공모두거부 시모음오류

빈 입력: all/allSettled→[], any→AggregateError, race→계속 pending. then/await는 인라인 실행이 아니다. 취소는 별도 협력 필요.

코드·조건·비교표와 보충 설명

ALL

모든 성공 값

모두 fulfilled면 입력 순서의 값 배열입니다. 하나라도 rejected면 그 첫 거부 이유로 거부합니다. 나머지 작업이 끝날 때까지 반드시 기다리는 방식은 아닙니다.

ALL SETTLED

모든 정착 결과

모두 정착한 뒤 입력 순서의 status/value 또는 reason을 받습니다. 입력 Promise의 실패도 결과 항목으로 보존합니다.

RACE

첫 정착

가장 먼저 정착한 성공 값 또는 거부 이유를 따릅니다. 빠른 실패도 결과가 될 수 있습니다.

ANY

첫 성공

처음 fulfilled된 값을 따릅니다. 전부 거부하면 입력 순서의 reasons를 가진 AggregateError로 거부합니다.

EMPTY INPUT

빈 목록도 의미가 다르다

all/allSettled는 빈 배열로 fulfilled, any는 AggregateError로 rejected, race는 pending으로 남습니다. then/await 후속 실행은 동기 inline 실행이 아닙니다.

CANCELLATION

작업 취소는 별도 계약

먼저 결과가 나와도 다른 작업은 자동 취소되지 않습니다. 필요한 경우 작업이 지원하는 AbortSignal 등의 협력 계약을 연결합니다.

Promise 조합은 결과를 집계합니다. CPU 병렬 실행이나 외부 작업 취소를 자동으로 보장하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 캡션네 Promise 조합기의 성공·실패 결과와 빈 입력, 취소 계약을 카드 6개로 비교한다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
세 Promise pendingA/C: A 값/C 값 대기 / B: B 실패 대기A 30 ms/C 20 ms 성공과 B 10 ms 거부를 모두 시작한다조합 메서드는 이미 시작한 작업의 결과를 결합한다
B 거부 시점all/race: B 실패로 rejected / any/allSettled: 아직 pendingB 실패가 첫 settlement다race는 첫 성공이 아니라 첫 정착을 따른다
C 이행 시점any: C 값 fulfilled / allSettled: A 대기C 값이 첫 fulfilled 결과다any는 앞의 거부를 넘어 첫 성공을 채택한다
A 이행 뒤allSettled: A 성공/B 실패/C 성공 / all/race/any: 기존 결과 유지allSettled가 입력 순서 A/B/C로 결과를 내고 다른 조합 결과는 유지된다정착 속도와 결과 배열 순서는 다르며 작업은 자동 취소되지 않는다

위 그림은 같은 세 Promise에 네 조합 함수를 붙였을 때 각각 언제 무엇을 돌려주는지 비교합니다. 전부 성공해야 쓸 수 있다면 all을, 성공과 실패를 모두 기록해야 한다면 allSettled를, 가장 빠른 결과가 필요하다면 race를, 하나만 성공하면 된다면 any를 고릅니다. 어느 조합 함수도 나머지 작업을 멈추어 주지는 않으므로, 정말로 취소해야 한다면 그 작업이 지원하는 취소 방법을 따로 씁니다. Promise의 규칙은 ECMAScript 명세의 Promise 절에 있습니다.

더 깊이: Node.js에서 Promise의 작업이 실행되는 때

Node.js에는 Promise의 마이크로태스크 말고도 process.nextTick()으로 예약하는 다음 틱 큐(next tick queue)가 따로 있습니다. Node.js의 process 문서에 따르면 다음 틱 큐를 비울 때마다 그 직후에 마이크로태스크 큐를 비웁니다. 그래서 CommonJS 모듈에서는 process.nextTick()의 콜백이 언제나 then이나 queueMicrotask()의 콜백보다 먼저 실행됩니다. 그런데 ES 모듈은 이미 마이크로태스크 큐를 비우는 과정의 일부로 실행되므로, 같은 코드를 ES 모듈에서 실행하면 순서가 뒤집혀 then과 queueMicrotask()의 콜백이 먼저 실행됩니다. 문서는 특별한 이유가 없다면 여러 실행 환경에서 같은 뜻을 갖는 queueMicrotask()를 쓰라고 권합니다.

처리되지 않은 거부도 시점이 정해져 있습니다. 같은 문서에 따르면 unhandledRejection 이벤트는 Promise가 거부되었는데 이벤트 루프의 한 바퀴 안에 오류 처리 함수가 붙지 않았을 때 발생합니다. 이 이벤트를 아무도 처리하지 않으면 처리되지 않은 예외로 다시 던져지는데, 이것이 --unhandled-rejections 플래그의 기본 모드인 throw의 동작이며 본문에서 본 것처럼 프로세스가 끝나는 이유입니다. 그래서 Promise를 만들어 두고 다른 작업을 오래 기다린 뒤에야 catch를 붙이는 코드는, 그사이에 거부되면 이 예외와 종료를 겪을 수 있습니다.

정리

  • Promise는 대기에서 이행이나 거부로 한 번만 정착하며, 실행자 함수는 생성할 때 바로 실행되고 then의 함수는 지금의 코드가 끝난 뒤 실행됩니다.
  • then은 함수가 돌려준 값을 다음 단계로 넘기고, Promise를 돌려주면 그 결과를 기다리며, 거부와 예외는 가장 가까운 catch로 갑니다.
  • async 함수는 언제나 Promise를 돌려주고, await는 그 함수의 나머지 부분만 멈추었다가 이어서 실행합니다.
  • 거부 사유는 아무 값이나 될 수 있으므로 unknown으로 받아 좁히며, catch에서 값을 돌려주는 회복과 다시 던지는 전달을 구분합니다.
  • 서로 기다릴 필요가 없는 작업은 먼저 시작해 조합 함수로 기다리며, 필요한 결과에 따라 all과 allSettled, race, any를 고릅니다.

연습 문제

1. 실행 순서

다음 코드의 출력 순서를 적어 보세요. ready는 이미 이행되었는데도 E가 D보다 늦게 나오는 이유는 무엇인가요?

console.log("A");
const ready = new Promise<number>((resolve) => {
  console.log("B");
  resolve(3);
});
async function runTrace(): Promise<void> {
  console.log("C");
  const value = await ready;
  console.log("E", value);
}
runTrace();
console.log("D");
해설 보기

출력은 A, B, C, D, E 3의 순서입니다. 실행자 함수는 Promise를 만들 때 바로 실행되어 B를 찍고, async 함수도 부르는 즉시 첫 await 앞의 C를 찍습니다. ready는 이미 이행되었지만 await 뒤의 부분은 지금 실행 중인 코드가 끝난 뒤로 예약되므로, 호출한 쪽의 D가 먼저 출력됩니다.

2. 네 조합 함수의 결과

먼저 시작한 세 작업 가운데 B는 10ms에 "B 실패"로 거부되고, C는 20ms에 "C 값"으로, A는 30ms에 "A 값"으로 이행됩니다. [A, B, C]에 all, allSettled, race, any를 각각 붙이면 무엇을 얻나요?

해설 보기

all과 race는 10ms에 B의 사유 "B 실패"로 거부됩니다. any는 처음 이행된 C의 "C 값"을 20ms에 돌려주고, allSettled는 30ms에 A가 정착한 뒤 A의 성공과 B의 실패, C의 성공을 입력의 순서대로 담은 배열을 돌려줍니다. 먼저 결과가 나온 조합 함수가 있어도 A와 C의 작업은 멈추지 않고 끝까지 진행됩니다.

3. finally에서 던진 예외

Promise.reject("원래 실패").catch(() => "회복").finally(() => { throw new Error("정리 실패"); })의 마지막 Promise는 어떤 상태인가요? finally가 정상적으로 끝난다면 어떻게 달라지나요?

해설 보기

catch가 "회복"으로 회복시켰지만, finally가 새 Error를 던졌으므로 마지막 Promise는 "정리 실패"라는 Error로 거부됩니다. 원래의 사유 "원래 실패"도, 회복한 값도 아닌 정리 단계의 오류가 결과가 되는 것입니다. finally가 정상적으로 끝나면 앞의 결과인 "회복"이 그대로 이어지며, finally가 돌려준 값이 결과를 바꾸지는 않습니다.

다음 글 제너레이터 함수에서는 실행을 멈추었다가 이어서 값을 하나씩 만들어 내는 제너레이터 함수와, 그것이 반복과 비동기에 어떻게 쓰이는지 다룹니다.