본문 바로가기

Promise와 async/await

Promise의 대기·성공·실패 상태와 async·await 오류 처리를 타입으로 표현하고 Promise.all로 독립 작업을 병렬 실행합니다.

하나의 자바스크립트 실행 에이전트에서는 코드가 한 번에 하나의 작업을 수행합니다. 호스트의 I/O와 별도 Worker 등은 이 실행 흐름과 구분합니다.

그런데 웹 애플리케이션에서는 네트워크 요청, 파일 I/O, 타이머처럼 오래 걸리거나 즉시 결과를 알 수 없는 비동기 작업이 자주 발생합니다.

이 비동기 흐름을 제대로 관리하지 못하면 UI가 멈추거나 애플리케이션 전체가 응답하지 않는 문제가 생길 수 있습니다.

과거에는 콜백 함수(Callback Function)로 비동기 작업을 처리했습니다.

하지만 콜백이 중첩되기 시작하면 이른바 콜백 지옥(Callback Hell)이 생기고, 가독성과 유지보수성이 빠르게 나빠졌습니다.

이 문제를 해결하기 위해 ES6(ECMAScript 2015)에서 Promise가 도입됐고, ES2017에서는 Promise를 더 직관적으로 다루도록 async/await 문법이 추가되었습니다.

타입스크립트는 자바스크립트의 비동기 기능을 타입 검사와 함께 사용할 수 있게 해주며, Promise 결과 타입과 오류 경로를 더 명확히 표현하도록 돕습니다.

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를 읽는다정착과 후속 사용자 코드 실행은 같은 순간이 아니다

처음 읽는다면 함수의 반환값과 콜백을 먼저 구별합니다. 일반 함수의 return은 현재 호출의 결과이고, Promise의 resolve는 나중에 소비할 결과를 정하는 함수입니다. await에서 멈추는 범위는 해당 async 함수의 이어지는 실행이며, 다른 코드까지 모두 멈추는 것은 아닙니다.

그림은 최종 정착과 이후 reaction의 실행을 구분합니다. resolve에 다른 Promise/thenable을 넘기면 그 상태를 따르므로 resolve 호출 즉시 fulfilled가 된다는 뜻은 아닙니다. Promise 생성자의 executor 자체는 동기적으로 실행됩니다.


Promise

Promise는 비동기 작업의 최종 완료(성공 또는 실패)와 그 결과값을 나타내는 객체입니다.

Promise는 다음 세 가지 상태 중 하나를 가집니다.

Pending (대기): 비동기 작업이 아직 완료되지 않은 초기 상태.

Fulfilled (이행): 비동기 작업이 성공적으로 완료된 상태.

결과값을 반환합니다.

Rejected (거부): 비동기 작업이 실패한 상태.

오류 값을 반환합니다.

Promise는 한 번 상태가 결정되면(Fulfilled 또는 Rejected) 다시 변경될 수 없습니다.

Promise 생성 및 사용 기본 구조
// Promise 생성: 생성자는 executor 함수 하나를 받고, executor에 resolve와 reject를 전달합니다.
const myPromise = new Promise<string>((resolve, reject) => {
  // 비동기 작업 수행 (예: setTimeout으로 2초 후 성공)
  setTimeout(() => {
    const success = true; // 실제로는 서버 응답이나 다른 조건에 따라 결정

    if (success) {
      resolve("데이터를 성공적으로 가져왔습니다!"); // 작업 성공 시 호출
    } else {
      reject("데이터를 가져오는 데 실패했습니다."); // 작업 실패 시 호출
    }
  }, 2000);
});

// Promise 사용: .then()으로 성공 시 처리, .catch()로 실패 시 처리
myPromise
  .then((message: string) => {
    console.log("성공:", message); // '데이터를 성공적으로 가져왔습니다!'
  })
  .catch((error: string) => {
    console.error("실패:", error); // '데이터를 가져오는 데 실패했습니다.'
  });

console.log("Promise 작업 시작 (비동기적으로 실행됩니다)"); // 이 부분이 먼저 출력됩니다.

위 코드의 첫 주석은 생성자와 executor의 인자를 구별해서 읽어야 합니다. new Promise에 넘기는 인자는 (resolve, reject) => { ... }라는 executor 함수 하나이며, 생성자가 그 함수를 호출할 때 resolve와 reject 두 함수를 전달합니다. 그래서 타이머 등록은 즉시 실행되지만 성공 메시지는 등록한 타이머의 콜백이 실행된 뒤에 나옵니다.

Promise 체이닝 (Chaining): .then() 메서드는 Promise를 반환하기 때문에, 여러 비동기 작업을 순차적으로 연결하여 처리할 수 있습니다.

이를 Promise 체이닝이라고 합니다.

각 .then()은 이전 Promise의 결과값을 다음 .then()으로 전달합니다.

function fetchData(url: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`[1] ${url} 에서 데이터 가져오기 완료`);
      resolve(`Fetched data from ${url}`);
    }, 1000);
  });
}

function processData(data: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`[2] 데이터 처리 완료: ${data}`);
      resolve(`Processed: ${data.toUpperCase()}`);
    }, 1500);
  });
}

function saveData(processedData: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`[3] 데이터 저장 완료: ${processedData}`);
      resolve("모든 작업 완료!");
    }, 800);
  });
}

// Promise 체이닝
fetchData("api/users")
  .then((data) => processData(data)) // 첫 번째 Promise의 결과가 다음 Promise의 인자로 전달
  .then((processedData) => saveData(processedData)) // 두 번째 Promise의 결과가 다음 Promise의 인자로 전달
  .then((finalMessage) => {
    console.log(finalMessage); // '모든 작업 완료!'
  })
  .catch((error) => {
    console.error("체인 중 오류 발생:", error);
  });

console.log("비동기 체인 시작...");

Promise 체이닝은 콜백 지옥을 해결하고, 비동기 코드의 흐름을 동기 코드처럼 순차적으로 이해하기 쉽게 만듭니다.

이 체인은 fetchData의 문자열을 받아야 processData를 호출하고, 처리된 문자열을 받아야 saveData를 호출합니다. 각 핸들러가 Promise를 반환하므로 다음 단계는 그 Promise의 정착을 기다립니다. 함수 호출만 하고 반환하지 않으면 다음 단계에 전달되는 값은 undefined이고, 시작한 작업의 실패도 이 체인에 연결되지 않습니다. 세 타이머의 대기값 합은 1000 + 1500 + 800 = 3300ms이지만, 실제 완료 시각에는 호스트의 실행 지연도 더해집니다. 입력이 서로 의존하는 이 작업들을 단순히 Promise.all로 바꾸면 같은 처리 순서를 보장할 수 없습니다.

오류 처리: 연결된 체인의 거부와 핸들러가 던진 예외는 다음 거부 처리기로 전파됩니다. 반환하거나 await하지 않은 별도 작업, 타이머 콜백의 독립적인 throw까지 이 체인이 자동으로 포착하지는 않습니다.

이는 여러 비동기 단계 중 어느 곳에서든 오류가 발생했을 때 중앙에서 오류를 관리할 수 있게 해줍니다.

function mightFailFetch(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const shouldFail = Math.random() > 0.5;
      if (shouldFail) {
        reject("네트워크 오류 발생!");
      } else {
        resolve("데이터 성공적으로 가져옴.");
      }
    }, 500);
  });
}

mightFailFetch()
  .then((data) => {
    console.log("데이터:", data);
    return Promise.resolve("다음 처리"); // 성공적으로 다음 .then()으로 전달
  })
  .then((nextData) => {
    console.log("다음 처리:", nextData);
    // return Promise.reject("처리 중 의도적인 오류!"); // 여기서 오류를 발생시킬 수도 있습니다.
  })
  .catch((error) => {
    console.error("오류 발생:", error); // 체인 중 발생한 모든 오류를 여기서 처리
  })
  .finally(() => { // Promise가 성공하든 실패하든 항상 실행
    console.log("작업 종료 (finally 블록)");
  });

finally()의 핸들러는 연결한 Promise가 성공 또는 실패로 정착했을 때 실행됩니다. 계속 pending인 Promise의 정리를 보장하는 타이머는 아닙니다. 핸들러가 정상적으로 끝나면 이전 결과를 따르지만, 예외를 던지거나 거부된 Promise를 반환하면 뒤의 Promise는 그 이유로 거부됩니다.


async/await

async/await는 Promise를 기반으로 비동기 코드를 작성하는 훨씬 더 간결하고 읽기 쉬운 문법입니다.

async 함수는 항상 Promise를 반환합니다. await는 async 함수 안이나 이를 지원하는 모듈의 최상위에서 사용할 수 있습니다.

await는 Promise가 Fulfilled 또는 Rejected 상태가 될 때까지 함수의 실행을 일시 중지합니다.

기본 구조
// async 함수는 항상 Promise를 반환합니다.
async function fetchUserData(): Promise<string> {
  console.log("[1] 사용자 데이터 가져오기 시작...");
  // await은 Promise가 완료될 때까지 기다립니다.
  const response = await new Promise<string>((resolve) => {
    setTimeout(() => {
      resolve("사용자 정보: Alice");
    }, 1500);
  });

  console.log("[2] 사용자 데이터 가져오기 완료:", response);
  return response;
}

async function processAndSaveUserData(): Promise<void> {
  try {
    const userData = await fetchUserData(); // fetchUserData가 완료될 때까지 기다림
    console.log("[3] 데이터 처리 중...");
    await new Promise<void>((resolve) => setTimeout(resolve, 1000)); // 처리 시간 시뮬레이션
    console.log(`[4] ${userData} 처리 완료.`);

    console.log("[5] 데이터 저장 중...");
    await new Promise<void>((resolve) => setTimeout(resolve, 800)); // 저장 시간 시뮬레이션
    console.log(`[6] ${userData} 저장 완료.`);
  } catch (error) {
    console.error("오류 발생:", error); // try-catch로 오류 처리
  } finally {
    console.log("[7] 모든 작업 완료 (async/await finally)");
  }
}

console.log("비동기 함수 호출 시작...");
processAndSaveUserData(); // async 함수를 호출하면 즉시 Promise가 반환됩니다.
console.log("비동기 함수 호출 완료 (실행은 계속됩니다)...");

async/await를 사용하면 Promise 체인보다 실행 순서와 예외 처리 위치를 한 블록 안에서 읽기 쉽습니다.

다만 병렬 실행이 필요한 작업까지 순차 await로 묶으면 전체 시간이 늘어날 수 있습니다.

오류 처리: async/await에서 오류는 try...catch 블록을 사용하여 동기 코드와 동일하게 처리할 수 있습니다.

await 표현식에서 Promise가 Rejected 상태가 되면, 해당 오류는 catch 블록으로 전달됩니다.

async function riskyOperation(): Promise<string> {
  const willFail = Math.random() > 0.5;
  if (willFail) {
    throw new Error("위험한 작업 실패!"); // async 함수에서 throw는 Promise.reject()와 동일
  }
  return "위험한 작업 성공!";
}

async function performRiskyTask(): Promise<void> {
  try {
    console.log("위험한 작업 시작...");
    const result = await riskyOperation(); // 실패하면 여기서 catch 블록으로 점프
    console.log("결과:", result);
  } catch (error: any) { // unknown으로 받으면 값의 종류를 확인한 뒤 속성을 읽어야 합니다.
    console.error("작업 중 오류:", error.message);
  } finally {
    console.log("위험한 작업 시도 종료.");
  }
}

performRiskyTask();
performRiskyTask(); // 여러 번 실행하여 성공/실패 확인

async 함수는 항상 Promise를 반환: async 함수가 명시적으로 Promise를 반환하지 않더라도, 항상 암묵적으로 Promise를 반환합니다.

async function greet(name: string): Promise<string> {
  return `Hello, ${name}!`; // Promise.resolve('Hello, Alice!') 와 동일
}

greet("Alice").then(message => console.log(message)); // Hello, Alice!

async function throwError(): Promise<never> { // 'never'는 이 함수가 값을 반환하지 않고 항상 예외를 던짐을 의미
  throw new Error("Something went wrong!"); // Promise.reject(new Error("...")) 와 동일
}

throwError().catch(error => console.error(error.message)); // Something went wrong!

Promise 체인과 async/await는 결국 같은 Promise 상태를 다루지만, 오류를 모으는 위치와 코드를 읽는 방식이 다릅니다.

실패 값은 임의의 값일 수 있으므로 unknown에서 좁혀 처리합니다. Promise<T>의 T는 성공 값 타입이며 거부 값의 타입을 제한하지 않습니다.

거부 타입과 회복한 결과를 구별합니다

Promise<string>이라고 적어도 reject에는 문자열·Error·다른 값이 올 수 있습니다. 위의 (error: string)은 이 예제가 직접 거부한 문자열을 설명하지만, Promise의 타입이 거부 값을 string으로 제한해 준 것은 아닙니다. 외부 작업을 처리한다면 unknown으로 받아 instanceof Error 등의 검사 뒤 .message를 읽습니다. 명시적인 catch (error: unknown) 표기는 TypeScript 4.0부터 가능하고, 4.4의 useUnknownInCatchVariables 옵션은 생략한 catch 변수에도 이 기본값을 적용합니다. 공식 catch 옵션 설명을 참고합니다.

catch에서 로그만 출력하고 정상 반환하면 뒤의 Promise는 보통 undefined로 이행합니다. 대체 값을 반환하면 그 값으로 회복하고, 다시 throw하면 거부가 이어집니다. 오류를 기록했다는 사실과 호출자에게 실패를 계속 전달했다는 사실은 다릅니다. finally에서 새 예외가 발생하면 앞 성공이나 회복 결과도 뒤 Promise의 실패로 바뀔 수 있습니다.

이 구분을 기억해 두면 체인 끝에 .catch()를 둘지, await가 있는 구간을 try/catch/finally로 감쌀지 판단하기 쉽습니다.


Promise와 async/await의 타입스크립트 적용

타입스크립트는 Promise와 async/await의 타입 추론을 매우 잘 지원합니다.

  • Promise 타입: Promise<T> 형태로 Promise가 성공적으로 이행될 때 반환할 값의 타입을 명시합니다.
    interface User {
      id: number;
      name: string;
    }
    
    function getUserById(id: number): Promise<User> {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({ id, name: `User-${id}` });
        }, 500);
      });
    }
    
    // TypeScript는 then 블록 내의 user가 User 타입임을 자동으로 추론합니다.
    getUserById(1)
      .then(user => {
        console.log(user.name); // 자동 완성 지원
        // console.log(user.email); // Error: 'email' 속성이 'User' 형식에 없습니다.
      });
  • async 함수 반환 타입: async 함수는 항상 Promise<T> 타입을 반환하므로, 명시적으로 반환 타입을 Promise<T>로 선언하는 것이 좋습니다. 그렇지 않으면 TypeScript가 함수의 실제 반환값(T)을 기반으로 Promise<T>를 추론합니다.
    async function fetchProduct(productId: number): Promise<{ id: number; name: string }> {
      const response = await fetch(`/api/products/${productId}`);
      const data = await response.json();
      return data; // 반환 타입 표시는 호출자의 타입 계약이며, data의 실제 구조를 검증하지는 않음
    }
    
    async function displayProduct(id: number): Promise<void> { // Promise<void> 반환
      try {
        const product = await fetchProduct(id); // product는 { id: number; name: string } 타입
        console.log(`Product: ${product.name}`);
      } catch (error) {
        console.error("Failed to fetch product:", error);
      }
    }
    
    displayProduct(123);
  • await의 타입 추론: await 키워드 뒤에 오는 Promise의 제네릭 타입은 await 표현식의 결과 타입으로 정확하게 추론됩니다.

fetchProduct의 반환 타입 표시는 response.json() 데이터를 검증하지 않습니다. 원문은 응답 상태와 런타임 스키마 검사를 생략했으므로 외부 데이터까지 타입 안전하다는 의미로 읽지 않습니다.


비동기 작업의 동시 진행: Promise.all

여러 작업의 성공 값을 모을 때 Promise.all을 사용합니다. 아래 예제는 배열을 만들면서 두 함수를 먼저 호출하고, 반환된 Promise들을 조합합니다. 조합기 자체가 함수를 시작하거나 CPU 병렬 실행을 보장하지는 않습니다.

하나라도 실패하면 전체 Promise.all이 Rejected 상태가 됩니다.

async function fetchPost(id: number): Promise<any> {
  console.log(`Fetching post ${id}...`);
  return new Promise(resolve => setTimeout(() => resolve({ id, title: `Post ${id} Title` }), 1000));
}

async function fetchComments(postId: number): Promise<any> {
  console.log(`Fetching comments for post ${postId}...`);
  return new Promise(resolve => setTimeout(() => resolve([{ id: 1, text: "Comment 1" }, { id: 2, text: "Comment 2" }]), 1500));
}

async function loadPostAndComments(postId: number): Promise<void> {
  try {
    console.log("Loading post and comments concurrently...");
    // 두 Promise를 동시에 시작하고 모두 완료될 때까지 기다립니다.
    const [post, comments] = await Promise.all([
      fetchPost(postId),
      fetchComments(postId)
    ]);

    console.log("\n--- All data loaded ---");
    console.log("Post:", post);
    console.log("Comments:", comments);
  } catch (error) {
    console.error("Error loading data:", error);
  }
}

loadPostAndComments(1);

Promise.all 외에도 다음과 같은 유틸리티 메서드들이 있습니다.

  • Promise.race: 여러 Promise 중 가장 먼저 완료되는 Promise의 결과(또는 오류)를 반환합니다.
  • Promise.allSettled (ES2020): 모든 Promise가 성공하거나 실패하더라도 기다리며, 각각의 결과를 객체 배열로 반환합니다. (일부 성공, 일부 실패인 경우 유용)
  • Promise.any (ES2021): 여러 Promise 중 가장 먼저 성공하는 Promise의 결과를 반환합니다. 모든 Promise가 실패해야만 Rejected 상태가 됩니다.

여러 Promise 도구는 이름이 비슷해도 실패 처리와 반환값의 의미가 다릅니다.

아래 보드처럼 "전부 필요", "가장 빠른 값", "모든 결과 기록", "가장 빠른 성공" 중 무엇을 원하는지 먼저 정하면 API 선택이 단순해집니다.

필요한 완료 의미로 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<T>가 어디에서 만들어지고, await 이후 어떤 타입으로 풀리며, 실패 경로의 오류 값이 어떻게 좁혀지는지 함께 추적해야 합니다.

서로 다른 결과 요구로 선택하기

세 작업 A·B·C를 모두 먼저 시작한 뒤 같은 목록 [A, B, C]에 네 조합기를 연결한다고 가정합니다. 외부 입력은 없으며 B가 10ms에 "B 실패"로 거부되고, C가 20ms에 "C 값"으로 성공하며, A가 30ms에 "A 값"으로 성공합니다. 이 시각은 문제의 관측 조건이고 실제 타이머의 정확한 실행 시각을 약속하는 수치는 아닙니다.

먼저 답을 가리고 all, allSettled, race, any의 결과와 정착 순서를 각각 적어 보세요. 또한 실패를 만나도 C와 A가 계속 실행되는 이유를 설명하고, 아래 체인의 마지막 출력도 예측하세요.

Promise.resolve(3)
  .then(value => value * 2)
  .then(value => { console.log("중간:", value); })
  .then(value => console.log("마지막:", value));
조합 결과와 반환값 확인

all과 race는 B의 거부를 결과로 받습니다. any는 C의 성공 값을 받고, allSettled는 A까지 정착한 뒤 A 성공·B 실패·C 성공을 입력 순서로 담습니다. 먼저 결과가 나온 조합기에도 작업 취소 기능은 없으므로 A·C의 실행은 별도로 계속됩니다. 실제 취소가 필요하면 그 작업이 지원하는 취소 계약을 연결해야 합니다.

체인의 첫 핸들러는 6을 반환합니다. 다음 핸들러는 중간: 6을 출력하지만 반환문이 없으므로 마지막 핸들러는 마지막: undefined를 출력합니다. 두 출력은 현재 동기 코드가 끝난 뒤 Promise reaction으로 이어집니다. 마지막 값에도 6이 필요하다면 중간 핸들러에 return value;를 추가합니다.

생성자 인자와 후속 핸들러의 반환 규칙은 ECMAScript의 Promise 생성자와 then 규칙에서도 확인할 수 있습니다.

연습: 동기 시작·재개·오류 회복 추적

아래 코드는 각각 독립적으로 실행한다고 가정합니다. 로그를 찍는 다른 코드·타이머·외부 요청은 없습니다. 이미 이행한 Promise에서도 await 뒤 코드는 현재 동기 실행을 끝낸 다음 재개됩니다.

동기 시작과 await 재개
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);
}
const traceDone = runTrace();
console.log("D");
거부 값을 좁혀 대체 결과로 회복
async function safeMessage(work: Promise<string>): Promise<string> {
  try {
    return await work;
  } catch (reason: unknown) {
    const message = reason instanceof Error ? reason.message : String(reason);
    return `대체:${message}`;
  }
}
  1. 첫 코드의 출력 순서를 쓰세요. executor와 async 함수의 await 이전 부분은 각각 언제 실행되며, D보다 E가 먼저 출력되지 않는 이유는 무엇인가요?
  2. 두 번째 함수에 Promise.reject("접속 실패")와 Promise.reject(new Error("접속 실패"))를 각각 넘기고 결과를 await합니다. 반환 Promise는 이행인가요, 거부인가요? 두 결과값과 좁히기의 역할을 설명하세요.
  3. Promise.reject("원래 실패").catch(() => "회복").finally(() => { throw new Error("정리 실패"); })의 마지막 Promise는 어떤 상태와 이유를 갖나요? finally가 정상 반환한다면 결과가 어떻게 달라지는지도 적으세요.
실행 순서와 회복 경계 풀이
  1. 출력은 A → B → C → D → E 3입니다. executor는 new Promise를 만드는 도중에 B를 찍고, async 함수도 호출 즉시 await 이전의 C를 찍습니다. ready는 이미 이행했지만 await의 나머지 실행은 현재 동기 흐름 뒤로 예약됩니다. 호출자는 traceDone이라는 Promise를 받으므로 D를 계속 출력할 수 있습니다. await가 에이전트 전체를 멈춘 것이 아닙니다.
  2. 둘 다 대체:접속 실패라는 문자열로 이행합니다. 문자열 이유에는 .message를 전제하지 않고 String으로 표현하며, Error이면 확인한 뒤 그 속성을 사용합니다. catch의 문자열 반환은 실패를 호출자에게 다시 던지는 정책이 아니라 대체 성공 값으로 회복하는 정책입니다. 실패를 유지하려면 기록 후 throw reason처럼 별도로 전달해야 합니다.
  3. catch가 반환한 회복 값까지는 이행하지만, finally의 새 Error 때문에 마지막 Promise는 거부됩니다. 이유는 원래 문자열이 아니라 정리 실패 Error입니다. finally가 정상 반환하면 회복 값 "회복"을 유지합니다. finally의 일반 반환값 자체가 성공 값을 교체하는 것은 아닙니다.