Promise 조합 — all·allSettled·race·any

Promise.all, allSettled, race, any 네 가지 조합 API의 동작 차이, 각각의 적합한 사용 사례, 타임아웃 패턴, allSettled로 부분 실패 처리하는 실용 패턴을 정리합니다.

· 5 min read · PALDYN Team

지난 글에서 Promise 체이닝과 에러 처리를 살펴봤습니다. 여러 Promise를 조합해서 병렬로 실행하거나 경쟁시킬 때는 JavaScript가 제공하는 네 가지 정적 메서드를 사용합니다.

네 가지 조합 API 한눈에 보기

Promise 조합 API 비교

APIfulfilled 조건rejected 조건
Promise.all전부 fulfilled하나라도 rejected
Promise.allSettled전부 settled (항상)없음
Promise.race가장 먼저 settled가장 먼저 settled(rejected)
Promise.any가장 먼저 fulfilled전부 rejected

Promise.all — 전부 성공해야 할 때

const [user, posts, comments] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
  fetchComments(id),
]);

세 요청이 병렬로 실행되며, 하나라도 실패하면 즉시 rejected 됩니다. 순서는 입력 배열 순서를 따릅니다.

중요한 특성: 하나가 rejected 되더라도 나머지 Promise 자체는 계속 실행됩니다. 취소 메커니즘은 없습니다(AbortController 사용 필요).

// 에러 처리
try {
  const results = await Promise.all([fetchA(), fetchB()]);
} catch (err) {
  // 먼저 실패한 하나의 에러만 받음
  console.error('어느 하나 실패:', err);
}

Promise.allSettled — 모두 완료될 때까지 기다릴 때

ES2020 추가. 하나가 실패해도 나머지를 기다린 후, 모든 결과를 { status, value/reason } 배열로 반환합니다.

const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);

const succeeded = results
  .filter(r => r.status === 'fulfilled')
  .map(r => r.value);

const failed = results
  .filter(r => r.status === 'rejected')
  .map(r => r.reason);

console.log('성공:', succeeded.length, '실패:', failed.length);

allSettled는 절대 rejected가 되지 않으므로 .catch 없이 사용해도 안전합니다.

Promise.race — 가장 빠른 결과가 필요할 때

가장 먼저 settled(fulfilled 또는 rejected)된 Promise의 결과를 반환합니다.

// 타임아웃 구현
function withTimeout(promise, ms) {
  const timer = new Promise((_, reject) =>
    setTimeout(() => reject(new Error(`${ms}ms 초과`)), ms)
  );
  return Promise.race([promise, timer]);
}

const data = await withTimeout(fetchData(), 5000);

타임아웃과 allSettled 실용 패턴

주의: race의 패배자 Promise는 계속 실행됩니다. 타임아웃 시 fetch를 실제로 취소하려면 AbortController를 함께 사용해야 합니다.

function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timer));
}

Promise.any — 하나라도 성공하면 되는 경우

ES2021 추가. 가장 먼저 fulfilled 된 결과를 반환합니다. 모두 rejected 되어야만 AggregateError로 rejected 됩니다.

// 여러 CDN 중 가장 빠른 응답 사용
const script = await Promise.any([
  fetch('https://cdn1.example.com/lib.js'),
  fetch('https://cdn2.example.com/lib.js'),
  fetch('https://cdn3.example.com/lib.js'),
]);

// 모두 실패하면
try {
  await Promise.any([fail1(), fail2()]);
} catch (e) {
  console.log(e instanceof AggregateError); // true
  console.log(e.errors); // [err1, err2]
}

Promise.race는 먼저 rejected 되어도 그 에러가 전파되지만, Promise.any는 하나라도 fulfilled 되면 그걸로 resolved 됩니다.

빈 배열 처리

각 API가 빈 배열을 받으면 어떻게 되는지 알아두면 버그를 예방할 수 있습니다.

await Promise.all([]);        // fulfilled([]) — 빈 배열로 즉시
await Promise.allSettled([]); // fulfilled([]) — 즉시
await Promise.race([]);       // 영원히 pending (주의!)
await Promise.any([]);        // 즉시 rejected(AggregateError)

Promise.race([])는 영원히 pending 상태로 남으므로 빈 배열을 넘기지 않도록 주의해야 합니다.

병렬 처리 시 주의점

// 직렬 실행 (느림 — 순서 보장 필요 시 사용)
const a = await fetchA();
const b = await fetchB();

// 병렬 실행 (빠름 — 의존성 없을 때 사용)
const [a, b] = await Promise.all([fetchA(), fetchB()]);

서로 독립적인 비동기 작업은 항상 Promise.all로 병렬화하세요. 각 작업의 실행 시간이 nms라면, 직렬은 n * countms, 병렬은 max(n1, n2, ...)ms가 걸립니다.


지난 글: Promise 체이닝과 에러 처리 — .then·.catch·.finally

다음 글: async/await 내부 동작 — 제너레이터와 Promise의 결합


읽어주셔서 감사합니다. 😊