Promise.allSettled, any, race — 병렬 비동기 제어 전략
여러 비동기 작업을 동시에 처리해야 할 때,
Promise.all만 쓰는 분이 많습니다. 하지만allSettled,any,race를 상황에 맞게 선택하면 훨씬 견고한 비동기 코드를 작성할 수 있습니다.
Promise.all — 모두 성공해야
모든 Promise가 이행(fulfilled)되면 결과 배열을 반환합니다. 하나라도 거부(rejected) 되면 즉시 거부됩니다.
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가 완료(성공 또는 실패)될 때까지 기다립니다. ** 절대 거부되지 않습니다.**
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 부분 성공
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의 결과를 반환합니다. 성공이든 실패든 ** 가장 빠른 것이 결과 **입니다.
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의 주의점
// 실패가 먼저 완료되면 거부됨
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를 던집니다.
// 여러 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은 모든 요청을 동시에 보내므로, 요청이 너무 많으면 서버에 부담이 됩니다.
// 동시 실행 수를 제한하는 유틸리티
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개씩
에러 처리 패턴
// 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가 가장 안전한 선택인 경우가 많습니다.
댓글 로딩 중...