
현대 웹 개발에서 비동기 처리는 필수적입니다. 단순히 데이터를 불러오는 것을 넘어, 여러 개의 API 호출을 어떻게 효율적으로 관리하느냐가 애플리케이션의 성능과 사용자 경험(UX)을 결정짓습니다. 오늘은 자바스크립트 비동기 패턴의 양대 산맥인 Promise.all()과 Promise.race()의 심도 있는 차이점을 분석하고, 시나리오별 실무 적용 예제를 살펴보겠습니다.
1. 비동기 병렬 처리의 이해
자바스크립트는 싱글 스레드 언어이지만, 브라우저나 Node.js 환경에서 제공하는 Web APIs를 통해 비동기 작업을 병렬로 처리할 수 있습니다. 이때 여러 개의 프로미스(Promise) 객체를 하나로 묶어 관리해야 하는 상황이 발생하며, Promise.all()과 Promise.race()는 이 과정을 최적화하는 핵심 정적 메서드입니다.
2. Promise.all() vs Promise.race() 비교 분석
두 메서드는 모두 프로미스 배열(iterable)을 인자로 받지만, 결과를 반환하는 '결정적 타이밍'과 '실패 시 동작 방식'에서 확연한 차이를 보입니다.
| 비교 항목 | Promise.all() | Promise.race() |
|---|---|---|
| 주요 목적 | 모든 작업의 성공적인 완료가 필요할 때 | 가장 빠른 응답이 필요할 때 (타임아웃 등) |
| 반환 값 | 모든 프로미스의 결과값 배열 | 가장 먼저 완료(settled)된 단일 값 |
| 성공 조건 | 모든 프로미스가 성공(resolve)해야 함 | 가장 먼저 끝난 프로미스가 성공해야 함 |
| 실패 처리 | 하나라도 실패(reject)하면 즉시 거부됨 | 가장 먼저 끝난 프로미스가 실패하면 거부됨 |
| 실무 예시 | 대시보드 데이터 일괄 호출, 다중 이미지 업로드 | 응답 지연 제한(Timeout), 미러 서버 요청 |
3. 실무 적용을 위한 Deep Dive Example 7가지
단순한 이론을 넘어, 실제 현업 개발 환경에서 바로 복사하여 적용할 수 있는 수준 높은 예제들을 소개합니다.
예제 1: 다중 API 엔드포인트에서 데이터 일괄 수집 (Promise.all)
사용자 정보, 게시글 목록, 알림 설정을 동시에 가져와 페이지를 렌더링해야 할 때 사용합니다.
async function fetchUserDashboard(userId) {
try {
const [profile, posts, settings] = await Promise.all([
fetch(`/api/users/${userId}`).then(res => res.json()),
fetch(`/api/users/${userId}/posts`).then(res => res.json()),
fetch(`/api/users/${userId}/settings`).then(res => res.json())
]);
console.log("대시보드 데이터 로드 완료:", { profile, posts, settings });
return { profile, posts, settings };
} catch (error) {
console.error("데이터 로드 중 오류 발생 (하나라도 실패 시 중단):", error);
}
}
예제 2: 네트워크 요청 타임아웃 구현 (Promise.race)
서버 응답이 5초 이상 걸릴 경우 에러를 발생시키거나 기본값을 반환하도록 강제합니다.
function requestWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url).then(res => res.json());
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("요청 시간 초과!")), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
// 사용 예시
requestWithTimeout('/api/large-data')
.then(data => console.log(data))
.catch(err => console.error(err.message));
예제 3: 다중 파일 업로드 및 상태 관리 (Promise.all)
여러 파일을 S3 등에 업로드하고 모든 업로드가 완료된 후 DB에 기록할 때 필수적입니다.
async function uploadFiles(files) {
const uploadPromises = files.map(file => {
const formData = new FormData();
formData.append('file', file);
return fetch('/api/upload', { method: 'POST', body: formData }).then(res => res.json());
});
try {
const results = await Promise.all(uploadPromises);
alert(`${results.length}개의 파일이 모두 업로드되었습니다.`);
} catch (error) {
alert("일부 파일 업로드에 실패했습니다. 전체 프로세스를 취소합니다.");
}
}
예제 4: 가장 빠른 서버 응답 선택 (Promise.race)
전 세계 여러 리전(Region)에 복제된 서버 중 가장 응답 속도가 빠른 서버의 데이터를 사용합니다.
async function getFastestResponse() {
const endpoints = [
'https://kr.api.example.com/data',
'https://us.api.example.com/data',
'https://eu.api.example.com/data'
];
try {
const fastestData = await Promise.race(endpoints.map(url => fetch(url).then(res => res.json())));
console.log("최단 시간 응답 수신 성공:", fastestData);
} catch (error) {
console.error("모든 서버가 응답에 실패했거나 첫 응답이 에러입니다.");
}
}
예제 5: Promise.all을 활용한 의존성 없는 트랜잭션 로직
서로 연관은 없지만 모두 성공해야만 다음 단계로 넘어가는 비즈니스 로직입니다.
async function processOrder(orderId) {
// 재고 확인과 결제 승인을 동시에 진행
const [inventoryStatus, paymentStatus] = await Promise.all([
checkInventory(orderId),
validatePaymentToken(orderId)
]);
if (inventoryStatus.ok && paymentStatus.authorized) {
return finalizeOrder(orderId);
}
}
예제 6: 사용자 입력 대기 vs 자동 만료 (Promise.race)
사용자가 확인 버튼을 누르는 것과 10초 카운트다운 중 먼저 발생하는 이벤트를 처리합니다.
function waitForUserAction() {
const userClick = new Promise(resolve => {
document.getElementById('confirmBtn').onclick = () => resolve('clicked');
});
const autoClose = new Promise(resolve => {
setTimeout(() => resolve('timeout'), 10000);
});
Promise.race([userClick, autoClose]).then(result => {
if (result === 'timeout') {
console.log("시간 초과로 팝업을 닫습니다.");
} else {
console.log("사용자가 확인을 눌렀습니다.");
}
});
}
예제 7: 동적 모듈 로딩 (Promise.all)
애플리케이션 초기 구동 시 필요한 여러 라이브러리나 컴포넌트를 병렬로 로드합니다.
async function initApp() {
console.time("Init");
try {
const [chartLib, mapLib] = await Promise.all([
import('./libs/charts.js'),
import('./libs/maps.js')
]);
chartLib.render();
mapLib.load();
console.timeEnd("Init");
} catch (err) {
console.error("모듈 로딩 실패", err);
}
}
4. 고급 개발자를 위한 팁: 에러 핸들링 전략
Promise.all()의 단점은 하나만 실패해도 전체가 reject된다는 점입니다. 이를 방지하기 위해 각 프로미스에 .catch()를 붙여 '실패해도 진행'하게 하거나, ES2020에서 도입된 Promise.allSettled()를 사용하는 것이 대안이 될 수 있습니다.
'Java Script' 카테고리의 다른 글
| [JAVA SCRIPT] 비동기 통신의 혁명 AJAX란 무엇인가? 실무 해결 방법 7가지와 기술 차이 분석 (0) | 2026.05.04 |
|---|---|
| [JAVA SCRIPT] 자바스크립트란 무엇인가요? 현대 웹 생태계의 심장 (0) | 2026.02.23 |
| [JAVA SCRIPT] var, let, const의 차이점 : 모던 자바스크립트의 변수 설계 철학 (0) | 2026.02.23 |
| [JAVA SCRIPT] 왜 요즘은 var를 사용하지 말라고 하나요? 레거시의 함정과 모던 솔루션 (0) | 2026.02.23 |
| [JAVA SCRIPT] 변수 호이스팅(Hoisting)의 심층 이해와 모던 자바스크립트의 설계 철학 (0) | 2026.02.23 |