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: 타임아웃 구현, 여러 소스 중 가장 빠른 응답 사용

비동기 처리는 단순히 문법만 아는 것이 아니라 실패 케이스까지 고려해야 프로덕션 레벨 코드가 된다.