JavaScript 비동기 처리: Promise.all vs Promise.race 실전 활용
문제 상황
대시보드 페이지에서 5개의 독립적인 API를 호출하는 코드가 있었다. 순차적으로 호출하니 로딩 시간이 15초 가까이 걸렸다. 병렬 처리가 필요했다.
Promise.all 적용
가장 먼저 떠오른 건 Promise.all이었다.
const fetchDashboardData = async () => {
try {
const [users, orders, stats, notifications, settings] = await Promise.all([
fetchUsers(),
fetchOrders(),
fetchStats(),
fetchNotifications(),
fetchSettings()
]);
setDashboardData({ users, orders, stats, notifications, settings });
} catch (error) {
// 하나라도 실패하면 전체 실패
console.error('Dashboard load failed:', error);
}
};
로딩 시간은 3초로 단축됐다. 하지만 문제가 있었다. 통계 API 하나가 간헐적으로 타임아웃되면 전체 대시보드가 빈 화면으로 나왔다.
Promise.allSettled 고려
Promise.allSettled는 ES2020에서 추가될 예정이라 현재 프로젝트에선 사용할 수 없었다. polyfill을 고려했지만 다른 방법을 찾았다.
부분 실패 허용 패턴
각 Promise를 catch로 감싸서 실패 시 기본값을 반환하도록 변경했다.
const safePromise = (promise, defaultValue) =>
promise.catch(err => {
console.warn('API call failed, using default:', err);
return defaultValue;
});
const [users, orders, stats, notifications, settings] = await Promise.all([
safePromise(fetchUsers(), []),
safePromise(fetchOrders(), []),
safePromise(fetchStats(), {}),
safePromise(fetchNotifications(), []),
safePromise(fetchSettings(), {})
]);
이제 일부 API가 실패해도 나머지 데이터는 정상 표시된다.
Promise.race 활용
검색 자동완성 기능에선 Promise.race가 유용했다. 사용자가 빠르게 타이핑하면 이전 요청을 무시해야 했다.
let searchId = 0;
const searchAutoComplete = async (query) => {
const currentSearchId = ++searchId;
const result = await fetchSuggestions(query);
// 최신 검색만 반영
if (currentSearchId === searchId) {
setSuggestions(result);
}
};
실제로는 race보다 요청 ID 비교 패턴이 더 명확했다.
정리
- Promise.all: 모든 결과가 필요할 때, 하나라도 실패하면 전체 실패 처리가 맞을 때
- 부분 실패 허용: 독립적인 데이터 소스들, UX상 일부 데이터만으로도 의미가 있을 때
- Promise.race: 타임아웃 구현, 여러 소스 중 가장 빠른 응답 사용
비동기 처리는 단순히 문법만 아는 것이 아니라 실패 케이스까지 고려해야 프로덕션 레벨 코드가 된다.