JavaScript 비동기 처리: Promise.all vs Promise.race 실전 사용기
문제 상황
대시보드 페이지에서 5개의 서로 다른 API를 호출해야 하는 상황이었다. 초기에는 순차적으로 호출했는데, 총 로딩 시간이 3초를 넘어가면서 사용자 경험이 좋지 않았다.
const userData = await fetchUser();
const orderData = await fetchOrders();
const statsData = await fetchStats();
// 너무 느리다
Promise.all 도입
모든 API가 독립적이었기 때문에 Promise.all을 사용해 병렬 처리했다.
const [userData, orderData, statsData, revenueData, noticeData] =
await Promise.all([
fetchUser(),
fetchOrders(),
fetchStats(),
fetchRevenue(),
fetchNotices()
]);
로딩 시간이 가장 느린 API 기준으로 줄어들어 약 1.2초로 개선되었다.
Promise.all의 문제점
하지만 곧 문제가 발생했다. 5개 중 하나라도 실패하면 전체가 실패 처리되었고, 사용자는 빈 화면만 보게 되었다. 특히 공지사항 API가 간헐적으로 타임아웃이 발생했는데, 이것 때문에 핵심 데이터까지 표시되지 않는 상황이었다.
해결 방법
필수 데이터와 부가 데이터를 분리했다.
try {
// 필수 데이터는 Promise.all로
const [userData, orderData, statsData] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchStats()
]);
// 부가 데이터는 개별 처리
const revenueData = await fetchRevenue().catch(() => null);
const noticeData = await fetchNotices().catch(() => []);
setState({ userData, orderData, statsData, revenueData, noticeData });
} catch (error) {
// 필수 데이터 실패시만 에러 처리
handleError(error);
}
Promise.race 활용
이미지 CDN이 여러 개 있을 때 가장 빠른 응답을 사용하는 케이스에서 Promise.race를 활용했다.
const imageUrl = await Promise.race([
fetchFromCDN1(imageId),
fetchFromCDN2(imageId),
fetchFromCDN3(imageId)
]);
타임아웃 구현에도 유용했다.
const fetchWithTimeout = (promise, ms) => {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
};
정리
- Promise.all: 모든 결과가 필요하고, 하나라도 실패하면 전체 실패 처리가 맞을 때
- Promise.race: 가장 빠른 응답 하나만 필요할 때, 타임아웃 구현
- 실무에서는 필수/부가 데이터를 구분해서 처리하는 것이 안정적이었다