자바스크립트 Promise 조합 정리: 병렬 처리, 타임아웃, 부분 성공 처리까지
개발/javascript&typescript1 2025. 12. 29. 08:031) finally: "성공/실패와 무관하게" 마지막 정리 구간
finally()는 then/catch로 처리된 결과가 성공이든 실패든 상관없이 "마지막에 반드시 실행"되는 정리 단계입니다.
1-1. 언제 쓰나
- 로딩 스피너 종료, 버튼 비활성화 해제
- 리소스 정리(타이머 해제, AbortController 정리, 파일 핸들 닫기 등)
- 로그/측정(성공 여부와 무관하게 종료 시점 기록)
1-2. 중요한 규칙 3가지
- finally는 값을 바꾸기보다 "정리"에 집중하는 게 안전합니다.
- finally에서 return을 하더라도, 보통은 기존 값이 유지됩니다(값 변환 목적이 아님).
- finally에서 에러를 던지거나, 실패하는 Promise를 반환하면 "원래 결과를 덮어쓰고" 실패가 됩니다.
- 즉, 정리 단계가 실패하면 전체 체인이 실패로 바뀔 수 있습니다.
1-3. 예시 (가장 흔한 형태)
setLoading(true);
fetch("/api/data")
.then(r => r.json())
.then(data => {
// 성공 처리
})
.catch(err => {
// 에러 처리
})
.finally(() => {
setLoading(false); // 무조건 정리
});
2) 네 가지 조합 API 한눈에 비교
2-1. 동작 요약
- Promise.all : 전부 성공해야 성공. 하나라도 실패하면 즉시 실패(빠른 실패).
- Promise.allSettled : 전부 끝날 때까지 기다리고, 성공/실패 결과를 "리스트로" 받음.
- Promise.race : 제일 먼저 끝난 것(성공/실패 포함)의 결과를 그대로 반환.
- Promise.any : 성공이 하나라도 나오면 그 성공으로 끝. 전부 실패하면 실패(묶음 에러).
2-2. 어떤 걸 선택해야 하나
- "다 성공해야 의미 있다" → all
- "실패해도 일단 전부 결과가 필요하다(리포트/집계)" → allSettled
- "가장 빨리 끝난 결과를 쓴다(타임아웃, 미러 서버 등)" → race
- "여러 후보 중 하나만 성공하면 된다(미러/대체 경로)" → any
3) Promise.all 실전 패턴
3-1. 병렬 처리로 성능 올리기 (대표 사례)
서로 독립적인 요청은 순차 await보다 병렬이 확실히 빠릅니다.
const [user, plan, notices] = await Promise.all([
fetchUser(),
fetchPlan(),
fetchNotices(),
]);
3-2. 주의: 하나 실패하면 전체가 실패한다
- all은 "성공 올인"입니다.
- 한 요청이 실패해도 나머지는 이미 실행 중일 수 있고, 네트워크 취소도 자동으로 되지 않습니다.
대응 팁
- 실패 허용이 목적이면 allSettled로 전환하거나
- 각 Promise에서 에러를 삼켜서 "항목별 실패를 값으로 바꾸는" 방식을 씁니다.
const safe = p => p.catch(err => ({ error: err }));
const [a, b, c] = await Promise.all([safe(p1), safe(p2), safe(p3)]);
4) Promise.allSettled 실전 패턴
4-1. "성공/실패를 포함한 전체 결과"가 필요한 경우
예: 여러 파일 업로드를 한 번에 걸고, 성공/실패 목록을 UI에 보여주기.
const results = await Promise.allSettled(files.map(upload));
const ok = results
.filter(r => r.status === "fulfilled")
.map(r => r.value);
const fail = results
.filter(r => r.status === "rejected")
.map(r => r.reason);
4-2. allSettled를 쓰면 좋은 상황
- "부분 성공"도 의미가 있을 때
- 실패한 항목만 재시도 로직을 태우고 싶을 때
- 배치 작업의 리포팅/로그가 필요할 때
5) Promise.race 실전 패턴
5-1. 타임아웃 구현 (가장 흔함)
race는 "먼저 끝난 것"이 이깁니다. 그래서 타임아웃 Promise와 경쟁시키는 형태가 자주 쓰입니다.
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms)
);
return Promise.race([promise, timeout]);
}
const data = await withTimeout(fetchData(), 3000);
5-2. 주의: race는 "승자만 반환"이지 "패자를 멈추게 하지" 않습니다
- 타임아웃이 이겼다고 해서 실제 네트워크 요청이 멈추진 않습니다.
- 진짜 취소가 필요하면 AbortController 같은 취소 메커니즘을 같이 써야 합니다.
6) Promise.any 실전 패턴
6-1. "성공 하나만 나오면 된다"
예: 여러 미러 서버 중 하나라도 응답이 오면 사용.
const fastestOk = await Promise.any([
fetchFromMirror1(),
fetchFromMirror2(),
fetchFromMirror3(),
]);
6-2. 전부 실패하면 AggregateError
- any는 "전부 실패" 시 여러 에러를 묶은 AggregateError로 실패합니다.
- 디버깅/로그를 위해 에러 목록을 꺼내 보고 싶을 때가 있습니다.
try {
await Promise.any(tasks);
} catch (e) {
// e.errors 에 실패 원인들이 들어있는 환경이 많습니다(표준/런타임별 차이 가능)
}
7) 조합 패턴: 실무에서 자주 쓰는 "레시피"
7-1. "필수 + 선택" 같이 돌리기
- 필수 작업은 실패하면 전체 중단
- 선택 작업은 실패해도 무시하고 결과만 있으면 사용
const safe = p => p.catch(() => null);
const [profile, banner, recommendations] = await Promise.all([
fetchProfile(), // 필수
safe(fetchBanner()), // 선택
safe(fetchRecommendations()) // 선택
]);
7-2. "부분 성공 집계 + 실패만 재시도"
const results = await Promise.allSettled(jobs.map(runJob));
const failedJobs = results
.map((r, i) => (r.status === "rejected" ? jobs[i] : null))
.filter(Boolean);
if (failedJobs.length) {
await Promise.allSettled(failedJobs.map(runJob)); // 재시도도 allSettled로
}
7-3. "가장 빠른 성공 + 타임아웃"
- any로 "성공"을 기다리되, race로 전체 타임아웃을 걸기도 합니다.
const result = await Promise.race([
Promise.any([a(), b(), c()]), // 성공 하나만
new Promise((_, r) => setTimeout(() => r(new Error("timeout")), 2000))
]);
최종 정리
- finally는 값 변환이 아니라 "정리" 용도에 집중하는 게 가장 안전합니다. finally에서 던진 에러는 원래 결과를 덮어쓸 수 있다는 점만 기억하면 실수 확 줄어듭니다.
- 병렬 처리 기본은 all, 결과 리포팅까지 필요하면 allSettled.
- 타임아웃/가장 빠른 결과는 race지만 "취소가 되진 않는다"는 점을 같이 챙겨야 합니다.
- "성공 하나만 있으면 됨"은 any, 다만 전부 실패 시 묶음 실패가 난다는 걸 고려해야 합니다.
- 실무 조합은 보통 "필수는 all, 선택은 safe 처리", 그리고 "부분 실패를 allSettled로 수집해서 재시도"가 가장 많이 쓰입니다.
반응형




