여러 비동기 작업을 동시에 처리해야 할 때, Promise.all만 쓰는 분이 많습니다. 하지만 allSettled, any, race를 상황에 맞게 선택하면 훨씬 견고한 비동기 코드를 작성할 수 있습니다.

Promise.all — 모두 성공해야

모든 Promise가 이행(fulfilled)되면 결과 배열을 반환합니다. 하나라도 거부(rejected) 되면 즉시 거부됩니다.

JS
const results = await Promise.all([
  fetch("/api/users"),
  fetch("/api/orders"),
  fetch("/api/products"),
]);
// 세 개 모두 성공해야 results를 받음

// 하나라도 실패하면?
try {
  await Promise.all([
    Promise.resolve("성공"),
    Promise.reject("실패!"),
    Promise.resolve("이것도 성공"),
  ]);
} catch (err) {
  console.log(err); // "실패!" — 첫 번째 거부 이유
  // 나머지 성공한 결과는 받을 수 없음
}

**사용 시점 **: 모든 데이터가 필요하고, 하나라도 실패하면 의미 없는 경우

Promise.allSettled — 결과와 무관하게 모두 대기

모든 Promise가 완료(성공 또는 실패)될 때까지 기다립니다. ** 절대 거부되지 않습니다.**

JS
const results = await Promise.allSettled([
  fetch("/api/users"),
  Promise.reject("네트워크 에러"),
  fetch("/api/products"),
]);

console.log(results);
// [
//   { status: "fulfilled", value: Response },
//   { status: "rejected", reason: "네트워크 에러" },
//   { status: "fulfilled", value: Response },
// ]

// 성공한 것만 필터링
const successes = results
  .filter((r) => r.status === "fulfilled")
  .map((r) => r.value);

// 실패한 것만 필터링
const failures = results
  .filter((r) => r.status === "rejected")
  .map((r) => r.reason);

** 사용 시점 **: 일부 실패해도 나머지 결과가 필요한 경우 (대시보드, 부분 로딩 등)

실전 예시 — 여러 API 부분 성공

JS
async function loadDashboard() {
  const [users, orders, stats] = await Promise.allSettled([
    fetchUsers(),
    fetchOrders(),
    fetchStats(),
  ]);

  return {
    users: users.status === "fulfilled" ? users.value : [],
    orders: orders.status === "fulfilled" ? orders.value : [],
    stats: stats.status === "fulfilled" ? stats.value : null,
    errors: [users, orders, stats]
      .filter((r) => r.status === "rejected")
      .map((r) => r.reason),
  };
}

Promise.race — 가장 빠른 하나

가장 먼저 완료되는 Promise의 결과를 반환합니다. 성공이든 실패든 ** 가장 빠른 것이 결과 **입니다.

JS
const result = await Promise.race([
  fetch("/api/server1"),
  fetch("/api/server2"),
  fetch("/api/server3"),
]);
// 가장 먼저 응답한 서버의 결과

// 타임아웃 패턴 — 가장 실용적인 활용
function fetchWithTimeout(url, timeout = 5000) {
  return Promise.race([
    fetch(url),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("타임아웃!")), timeout)
    ),
  ]);
}

try {
  const response = await fetchWithTimeout("/api/slow-endpoint", 3000);
  const data = await response.json();
} catch (err) {
  console.log(err.message); // "타임아웃!" (3초 내 응답 없으면)
}

** 사용 시점 **: 타임아웃 구현, 가장 빠른 미러 서버 선택

race의 주의점

JS
// 실패가 먼저 완료되면 거부됨
try {
  await Promise.race([
    new Promise((_, reject) => setTimeout(() => reject("빠른 실패"), 100)),
    new Promise((resolve) => setTimeout(() => resolve("느린 성공"), 1000)),
  ]);
} catch (err) {
  console.log(err); // "빠른 실패"
}

Promise.any — 가장 빠른 성공

가장 먼저 ** 성공 **하는 Promise의 결과를 반환합니다. 모두 실패하면 AggregateError를 던집니다.

JS
// 여러 CDN 중 가장 빠른 것
const fastest = await Promise.any([
  fetch("https://cdn1.example.com/data.json"),
  fetch("https://cdn2.example.com/data.json"),
  fetch("https://cdn3.example.com/data.json"),
]);
// 가장 먼저 성공한 응답

// 실패는 무시됨
const result = await Promise.any([
  Promise.reject("실패1"),
  Promise.resolve("성공!"),
  Promise.reject("실패2"),
]);
console.log(result); // "성공!"

// 모두 실패하면?
try {
  await Promise.any([
    Promise.reject("실패1"),
    Promise.reject("실패2"),
    Promise.reject("실패3"),
  ]);
} catch (err) {
  console.log(err instanceof AggregateError); // true
  console.log(err.errors); // ["실패1", "실패2", "실패3"]
}

** 사용 시점 **: 여러 소스 중 하나만 성공하면 되는 경우 (CDN, 백업 서버)

비교 표

메서드완료 조건거부 조건빈 배열
all모두 성공하나라도 실패즉시 이행 []
allSettled모두 완료절대 거부 안 됨즉시 이행 []
race첫 번째 완료첫 번째가 실패면영원히 대기
any첫 번째 성공모두 실패AggregateError

실전 패턴 — 동시성 제한

Promise.all은 모든 요청을 동시에 보내므로, 요청이 너무 많으면 서버에 부담이 됩니다.

JS
// 동시 실행 수를 제한하는 유틸리티
async function promisePool(tasks, concurrency = 3) {
  const results = [];
  const executing = new Set();

  for (const task of tasks) {
    const promise = task().then((result) => {
      executing.delete(promise);
      return result;
    });
    executing.add(promise);
    results.push(promise);

    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

// 사용
const urls = Array.from({ length: 100 }, (_, i) => `/api/item/${i}`);
const tasks = urls.map((url) => () => fetch(url).then((r) => r.json()));
const allData = await promisePool(tasks, 5); // 동시에 5개씩

에러 처리 패턴

JS
// all — try/catch 필수
try {
  const [a, b, c] = await Promise.all([taskA(), taskB(), taskC()]);
} catch (err) {
  // 어떤 task가 실패했는지 알기 어려움
}

// allSettled — 개별 에러 처리 가능 (권장)
const results = await Promise.allSettled([taskA(), taskB(), taskC()]);
results.forEach((result, i) => {
  if (result.status === "rejected") {
    console.error(`Task ${i} 실패:`, result.reason);
  }
});

** 기억하기 **: all은 "전부 아니면 실패", allSettled는 "결과 무관하게 전부 대기", race는 "가장 빠른 것(성공이든 실패든)", any는 "가장 빠른 성공"입니다. 실무에서는 allSettled가 가장 안전한 선택인 경우가 많습니다.

댓글 로딩 중...