대시보드를 만들었습니다. 뿌듯했습니다. Promise.all로 API 세 개를 동시에 불러오는 코드가 딱 세 줄이었고, 코드 리뷰에서도 “깔끔하다"는 피드백을 받았어요.

사진: Unsplash의 Edson Junior
const [user, notifications, activity] = await Promise.all([
fetchUser(),
fetchNotifications(),
fetchActivity(),
]);그런데 어느 날부터 대시보드가 통째로 흰 화면이 됩니다. 알고 보니 fetchNotifications가 서버 문제로 가끔 실패하고 있었어요. 나머지 두 개는 멀쩡히 성공했는데도요.
Promise.all은 하나라도 실패하면 전체를 실패로 처리합니다. 알림 하나 때문에 대시보드 전체가 날아간 셈이죠.
왜 이런 일이 생기는지 이해하려면, 먼저 Promise와 Promise.all이 어떻게 동작하는지 짚고 넘어가야 합니다.
Promise와 Promise.all, 간단히 짚고 가기#
Promise는 “나중에 완료될 작업의 결과"를 담는 객체입니다. 서버에 데이터를 요청하면 즉시 결과가 오는 게 아니라, 잠시 후에 오죠. Promise는 그 결과가 오기 전까지 “대기 중(pending)” 상태를 유지하다가, 성공하면 “이행(fulfilled)”, 실패하면 “거부(rejected)” 상태가 됩니다.
// fetch()는 Promise를 반환합니다
const promise = fetch("/api/user");
// await으로 결과를 기다립니다
const response = await fetch("/api/user");그런데 API 요청이 여러 개라면 어떨까요?
// 하나씩 기다리면 순서대로 실행됩니다 — 느립니다
const user = await fetchUser(); // 300ms 대기
const posts = await fetchPosts(); // 300ms 대기
// 총 600ms+
Promise.all은 여러 Promise를 동시에 실행하고, 모두 완료될 때까지 기다립니다. 순차 실행 대신 병렬 실행이 가능해져 훨씬 빠르죠.
// Promise.all로 동시에 실행 — 빠릅니다
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
// 300ms 대기 (가장 오래 걸리는 것 기준)
이런 상황에서 자주 쓰입니다:
- 페이지 초기 로딩: 사용자 정보, 알림, 피드 등 여러 데이터를 한 번에 가져올 때
- 연관 없는 API 병렬 호출: 서로 의존하지 않는 요청들을 동시에 처리할 때
- 배치 작업: 파일 여러 개를 동시에 업로드하거나 변환할 때
서로 의존 관계가 없는 비동기 작업이라면 순서대로 기다릴 필요가 없습니다. Promise.all은 그럴 때 쓰는 도구예요.
여기까지는 완벽해 보입니다. 문제는 실패했을 때 생깁니다.
Promise.all: 전부 성공하거나, 하나라도 실패하면 끝#
Promise.all은 전달된 Promise들이 모두 성공할 때 결과를 반환합니다.
const results = await Promise.all([
Promise.resolve("A"),
Promise.resolve("B"),
Promise.resolve("C"),
]);
console.log(results); // ["A", "B", "C"]
순서는 보장됩니다. 세 번째 Promise가 가장 먼저 끝나도 결과 배열의 순서는 입력 순서 그대로예요.
하지만 하나라도 실패하면 즉시 reject됩니다.
try {
const results = await Promise.all([
Promise.resolve("A"),
Promise.reject(new Error("B가 실패했습니다")),
Promise.resolve("C"),
]);
} catch (error) {
console.log(error.message); // "B가 실패했습니다"
// A와 C의 결과는 알 수 없습니다
}중요한 건 나머지 Promise는 취소되지 않는다는 점입니다. 내부적으로는 계속 실행되지만, 결과를 받을 방법이 없어요. 마치 결과 보고서를 찢어버린 것처럼요.
Promise.allSettled: 모두 끝날 때까지 기다립니다#
Promise.allSettled는 성공이든 실패든 모든 Promise가 완료될 때까지 기다립니다.
const results = await Promise.allSettled([
Promise.resolve("A"),
Promise.reject(new Error("B가 실패했습니다")),
Promise.resolve("C"),
]);
console.log(results);
// [
// { status: "fulfilled", value: "A" },
// { status: "rejected", reason: Error("B가 실패했습니다") },
// { status: "fulfilled", value: "C" },
// ]
각 결과는 status 필드로 성공/실패를 구분합니다.
- 성공:
{ status: "fulfilled", value: 결과값 } - 실패:
{ status: "rejected", reason: 에러 }
이 구조 덕분에 각각의 결과를 개별적으로 처리할 수 있어요.
const results = await Promise.allSettled([
fetchUser(),
fetchNotifications(),
fetchActivity(),
]);
const [userResult, notifResult, activityResult] = results;
const user = userResult.status === "fulfilled" ? userResult.value : null;
const notifications =
notifResult.status === "fulfilled" ? notifResult.value : [];
const activity =
activityResult.status === "fulfilled" ? activityResult.value : [];알림을 못 불러와도 대시보드는 정상적으로 표시됩니다. 알림 부분만 “불러오기 실패” 메시지를 보여주면 되죠.

제작: 나노바나나
언제 뭘 써야 할까?#
둘의 차이를 알았으니, 기준을 정해봅시다.
Promise.all을 쓸 때
모든 결과가 다 있어야 의미가 있을 때입니다.
// 주문 처리: 재고 확인 + 결제 + 배송 예약이 모두 성공해야 진행
const [stock, payment, shipping] = await Promise.all([
checkStock(items),
processPayment(card),
reserveShipping(address),
]);재고는 있는데 결제가 실패했다면? 배송을 예약할 이유가 없죠. 하나라도 실패하면 전체를 취소해야 할 때 Promise.all이 적합합니다.
Promise.allSettled를 쓸 때
각 작업이 서로 독립적이고, 일부 실패해도 나머지를 처리해야 할 때입니다.
// 이미지 여러 장 업로드: 일부 실패해도 성공한 건 처리
const uploadResults = await Promise.allSettled(
images.map((img) => uploadImage(img))
);
const succeeded = uploadResults
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);
const failed = uploadResults
.filter((r) => r.status === "rejected")
.map((r) => r.reason);
console.log(`${succeeded.length}개 업로드 성공, ${failed.length}개 실패`);10장 중 2장이 실패했다고 8장을 버릴 수는 없잖아요.
둘의 차이가 명확해졌으니, 이제 자신 있게 쓸 수 있겠죠? 저도 그렇게 생각했습니다.
흔한 실수 패턴#
Promise.all에서 에러 처리를 빠뜨리기#
// ❌ 하나라도 실패하면 unhandled rejection 에러
const results = await Promise.all([api1(), api2(), api3()]);
// ✅ try-catch로 감싸야 합니다
try {
const results = await Promise.all([api1(), api2(), api3()]);
} catch (error) {
// 어떤 Promise가 실패했는지는 알 수 없습니다
console.error("하나 이상의 요청이 실패했습니다", error);
}Promise.allSettled 쓰고 status 체크를 안 하기#
const results = await Promise.allSettled([api1(), api2()]);
// ❌ status 체크 없이 바로 value 접근
const data = results.map((r) => r.value); // 실패한 건 value가 undefined
// ✅ status 먼저 확인
const data = results
.filter((r) => r.status === "fulfilled")
.map((r) => r.value);allSettled를 쓴다고 끝이 아닙니다. 결과를 쓰기 전에 status 확인이 필수예요.
정리#
| 상황 | 선택 |
|---|---|
| 모든 결과가 다 있어야 의미 있을 때 | Promise.all |
| 일부 실패해도 성공한 것만 처리하면 될 때 | Promise.allSettled |
| 에러를 개별적으로 처리해야 할 때 | Promise.allSettled |
Promise.all: 하나라도 실패 → 즉시 reject. 전부 성공해야 할 때.Promise.allSettled: 모두 완료 후 결과 배열 반환. 독립적인 작업 처리에.
처음 대시보드를 만들 때로 돌아간다면, 알림 API에는 allSettled를 썼을 겁니다. 사용자 정보처럼 없으면 페이지 자체가 의미 없는 것만 all로 묶어서요.
코드를 직접 실행하면서 동작을 확인하고 싶다면 데모 페이지에서 해보세요.
