기본 Promise 메서드 복습
실전 패턴을 다루기 전에 Promise.all, Promise.allSettled, Promise.race, Promise.any의 차이를 정확히 이해해야 합니다. 각 메서드는 여러 Promise를 조합하는 방식이 다릅니다.
// 성공/실패하는 Promise 생성 헬퍼
const success = (value, ms) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (reason, ms) =>
new Promise((_, reject) => setTimeout(() => reject(reason), ms));
// Promise.all — 모두 성공해야 함 (하나라도 실패하면 즉시 reject)
async function allExample() {
try {
const results = await Promise.all([
success("A", 100),
success("B", 200),
fail("C 실패", 150), // 이것 때문에 전체 실패
]);
} catch (error) {
console.log("all 실패:", error); // all 실패: C 실패
}
}
// Promise.allSettled — 모두 완료될 때까지 대기 (실패해도 계속)
async function allSettledExample() {
const results = await Promise.allSettled([
success("A", 100),
fail("B 실패", 200),
success("C", 150),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("성공:", result.value);
} else {
console.log("실패:", result.reason);
}
});
// 성공: A
// 실패: B 실패
// 성공: C
}
// Promise.race — 가장 먼저 완료된 것 반환
// Promise.any — 가장 먼저 성공한 것 반환
| 메서드 | 하나 실패 시 | 모두 실패 시 | 반환 |
|---|---|---|---|
all | 즉시 reject | reject | 결과 배열 |
allSettled | 계속 진행 | 모든 결과 반환 | 상태+값 배열 |
race | 실패가 먼저면 reject | reject | 첫 번째 결과 |
any | 계속 진행 | AggregateError | 첫 성공 결과 |
동시성 제한 — Promise Pool
수백 개의 비동기 작업을 동시에 실행하면 서버에 과부하가 걸립니다. 동시 실행 수를 제한하는 Pool 패턴이 필요합니다.
async function promisePool(tasks, concurrency = 5) {
/**
* 동시 실행 수를 제한하여 Promise를 실행합니다.
* @param {Function[]} tasks - () => Promise 형태의 함수 배열
* @param {number} concurrency - 최대 동시 실행 수
* @returns {Promise<any[]>} 결과 배열 (입력 순서 보장)
*/
const results = new Array(tasks.length);
let currentIndex = 0;
async function worker() {
while (currentIndex < tasks.length) {
const index = currentIndex++;
try {
results[index] = await tasks[index]();
} catch (error) {
results[index] = { error };
}
}
}
// concurrency 수만큼 워커 생성
const workers = Array.from(
{ length: Math.min(concurrency, tasks.length) },
() => worker()
);
await Promise.all(workers);
return results;
}
// 사용 예시: 100개의 API 요청을 동시 5개씩 실행
async function fetchAllUsers() {
const userIds = Array.from({ length: 100 }, (_, i) => i + 1);
const tasks = userIds.map(
(id) => () => fetchUser(id) // () => Promise 형태
);
console.time("100개 요청");
const results = await promisePool(tasks, 5);
console.timeEnd("100개 요청");
// 100개 요청: 2034ms (동시 5개씩, 각 요청 100ms 가정)
const succeeded = results.filter((r) => !r?.error).length;
console.log(`성공: ${succeeded}, 실패: ${100 - succeeded}`);
}
// 시뮬레이션용 fetch 함수
async function fetchUser(id) {
await new Promise((r) => setTimeout(r, 100));
if (id % 17 === 0) throw new Error(`User ${id} not found`);
return { id, name: `User_${id}` };
}
재시도 패턴 — Exponential Backoff
네트워크 요청이 일시적으로 실패할 때 지수 백오프(Exponential Backoff)로 재시도하는 패턴입니다.
async function withRetry(fn, options = {}) {
/**
* 실패 시 지수 백오프로 재시도합니다.
* @param {Function} fn - 실행할 비동기 함수
* @param {object} options - 재시도 설정
*/
const {
maxRetries = 3, // 최대 재시도 횟수
baseDelay = 1000, // 기본 대기 시간 (ms)
maxDelay = 30000, // 최대 대기 시간 (ms)
backoffFactor = 2, // 백오프 배수
shouldRetry = () => true, // 재시도 여부 판단 함수
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(attempt);
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(error)) {
break;
}
// 지수 백오프 + 지터(jitter)
const delay = Math.min(
baseDelay * Math.pow(backoffFactor, attempt),
maxDelay
);
const jitter = delay * 0.2 * Math.random(); // 20% 지터
const waitTime = delay + jitter;
console.log(
`[재시도] ${attempt + 1}/${maxRetries} ` +
`(${waitTime.toFixed(0)}ms 후): ${error.message}`
);
await new Promise((r) => setTimeout(r, waitTime));
}
}
throw lastError;
}
// 사용 예시
async function fetchDataWithRetry() {
const data = await withRetry(
async (attempt) => {
console.log(`요청 시도 ${attempt + 1}`);
const response = await fetch("https://api.example.com/data");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
{
maxRetries: 3,
baseDelay: 1000,
// 4xx 에러는 재시도하지 않음 (클라이언트 에러)
shouldRetry: (error) => !error.message.startsWith("HTTP 4"),
}
);
return data;
}
타임아웃 패턴
특정 시간 내에 완료되지 않으면 에러를 발생시키는 패턴입니다.
function withTimeout(promise, ms, message) {
/**
* Promise에 타임아웃을 적용합니다.
* @param {Promise} promise - 타임아웃을 적용할 Promise
* @param {number} ms - 타임아웃 시간 (밀리초)
* @param {string} message - 타임아웃 에러 메시지
*/
const timeout = new Promise((_, reject) => {
const timer = setTimeout(() => {
reject(new Error(message || `${ms}ms 타임아웃 초과`));
}, ms);
// Promise가 먼저 완료되면 타이머 정리
promise.finally(() => clearTimeout(timer));
});
return Promise.race([promise, timeout]);
}
// 사용 예시
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch("https://api.example.com/slow-endpoint"),
5000,
"API 응답 5초 타임아웃"
);
return data.json();
} catch (error) {
console.error("요청 실패:", error.message);
// API 응답 5초 타임아웃
}
}
복합 패턴 — 재시도 + 타임아웃 + 동시성 제한
실전에서는 여러 패턴을 조합하여 사용합니다.
async function robustFetch(urls, options = {}) {
/**
* 여러 URL을 안정적으로 가져옵니다.
* 재시도, 타임아웃, 동시성 제한을 모두 적용합니다.
*/
const {
concurrency = 3,
timeout = 5000,
maxRetries = 2,
} = options;
const tasks = urls.map(
(url) => async () => {
return withRetry(
async () => {
const response = await withTimeout(
fetch(url),
timeout,
`${url} 타임아웃`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${url}`);
}
return { url, data: await response.json() };
},
{ maxRetries }
);
}
);
const results = await promisePool(tasks, concurrency);
const succeeded = results.filter((r) => !r?.error);
const failed = results.filter((r) => r?.error);
console.log(`완료: ${succeeded.length}성공, ${failed.length}실패`);
return { succeeded, failed };
}
// 사용 예시
// const urls = Array.from({ length: 50 }, (_, i) =>
// `https://api.example.com/items/${i}`
// );
// const { succeeded, failed } = await robustFetch(urls, {
// concurrency: 5,
// timeout: 3000,
// maxRetries: 2,
// });
실전 팁
allSettled우선: 여러 독립적인 요청에서는all대신allSettled을 사용합니다. 하나의 실패가 전체를 망치지 않습니다- 지수 백오프: 재시도 간격은 지수적으로 늘리고, 지터(jitter)를 추가하여 동시 재시도로 인한 서버 부하를 방지합니다
- 취소 가능한 작업:
AbortController를 사용하면 fetch 요청을 취소할 수 있습니다 - 메모리 주의: 수천 개의 Promise를 동시에 생성하면 메모리가 급증합니다. 반드시 동시성을 제한합니다
- 에러 분류: 재시도 가능한 에러(5xx, 네트워크)와 불가능한 에러(4xx, 인증)를 구분합니다
- 로깅: 재시도, 타임아웃 발생 시 로그를 남겨 문제를 추적할 수 있게 합니다