본문으로 건너뛰기
김신건의 로그

[Javascript] Promise.all / race / any / allSettled

· 수정 · 📖 약 2분 · 607자/단어 #javascript #promise #async #concurrency
settled, JS Promise.all, Promise.race, Promise.any, Promise.allSettled

정의

Promise 의 정적 메서드 4 형제. 여러 Promise 를 동시에 다루는 도구.

메서드fulfilled 조건rejected 조건결과
all모두 fulfilled하나라도 rejected값 배열 / 첫 rejection
allSettled모두 정착절대 안 됨{status, value/reason} 배열
race가장 먼저 fulfilled가장 먼저 rejected첫 정착값/사유
any하나라도 fulfilled모두 rejected첫 fulfilled / AggregateError

Promise.all

const [users, posts, comments] = await Promise.all([
    fetch('/api/users').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
]);

모두 성공해야 결과. 하나라도 실패하면 즉시 throw, 나머지는 계속 실행되지만 결과 못 받음.

try {
    const results = await Promise.all([p1, p2, p3]);
} catch (e) {
    // 가장 먼저 실패한 reason
}

Promise.allSettled

const results = await Promise.allSettled([p1, p2, p3]);
results.forEach((r, i) => {
    if (r.status === 'fulfilled') console.log(i, r.value);
    else console.log(i, r.reason);
});

모두 정착 (성공/실패 무관) 까지 대기. 부분 실패가 OK 일 때.

Promise.race

// 타임아웃 패턴
function timeout(ms) {
    return new Promise((_, reject) =>
        setTimeout(() => reject(new Error('timeout')), ms));
}

const result = await Promise.race([
    fetch('/api/slow'),
    timeout(3000),
]);

가장 먼저 정착 (성공/실패 무관) 한 Promise 결과.

Promise.any (ES2021)

// 여러 CDN mirror 중 하나라도 응답하면
const data = await Promise.any([
    fetch('https://mirror1/data'),
    fetch('https://mirror2/data'),
    fetch('https://mirror3/data'),
]);

하나라도 성공 하면 그 값. 모두 실패하면 AggregateError (errors 배열).

try {
    const data = await Promise.any([...]);
} catch (e) {
    if (e instanceof AggregateError) {
        e.errors.forEach(err => console.error(err));
    }
}

비교 매트릭스

상황권장
모두 필요all
부분 실패 OKallSettled
가장 빠른 응답 (성공/실패 무관)race
하나라도 성공any
타임아웃race + timeout Promise

흔히 만나는 패턴

concurrent fetch

const users = await Promise.all(
    userIds.map(id => fetch(`/api/users/${id}`).then(r => r.json()))
);

배열의 map 콜백에서 Promise 반환 → all 로 모음.

concurrent with limit

// 동시 5 개로 제한
async function pool(items, fn, concurrency = 5) {
    const results = [];
    for (let i = 0; i < items.length; i += concurrency) {
        const chunk = items.slice(i, i + concurrency);
        const r = await Promise.all(chunk.map(fn));
        results.push(...r);
    }
    return results;
}

await pool(urls, fetch, 5);

라이브러리: p-limit, p-queue.

timeout wrapper

function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('timeout')), ms))
    ]);
}

const r = await withTimeout(fetch('/api'), 5000);

부분 실패 통계

const results = await Promise.allSettled(
    items.map(item => process(item))
);
const success = results.filter(r => r.status === 'fulfilled').length;
const failed = results.length - success;
console.log(`${success}/${results.length} succeeded`);

함정

1. Promise.all 의 fail-fast

Promise.all([slow_p, fail_p])
// fail_p 가 즉시 throw → slow_p 의 결과는 못 받음
// 그러나 slow_p 는 background 에서 계속 실행됨 (cancel 안 됨)

진짜 cancel 은 AbortController 사용.

2. 빈 배열

Promise.all([])             // [] (즉시 resolve)
Promise.allSettled([])      // []
Promise.race([])             // 영원히 pending ⚠️
Promise.any([])              // AggregateError

race / any 에 빈 배열 주의.

3. await Promise.all vs 순차

// ❌ 순차 (2 sec)
const a = await fetch('/a');   // 1 sec
const b = await fetch('/b');   // 1 sec

// ✓ 동시 (1 sec)
const [a, b] = await Promise.all([
    fetch('/a'),
    fetch('/b'),
]);

의존성 없으면 항상 all 로 묶기.

4. 큰 배열의 메모리

Promise.all(arr.map(fn))     // 100,000 개면 모두 동시 시작

concurrency limit 권장.

선택 가이드 시각화

flowchart TD
    Start["Promise 조합 필요"]
    Start --> Q1{"모두 필요한가?"}
    Q1 -->|"예"| Q2{"부분 실패 허용?"}
    Q2 -->|"아니오"| All["Promise.all\n하나 실패 즉시 reject"]
    Q2 -->|"예"| AllSettled["Promise.allSettled\n모두 정착 후 결과 배열"]
    Q1 -->|"하나만 필요"| Q3{"먼저 오는 게 좋음?"}
    Q3 -->|"성공 우선"| Any["Promise.any\n첫 fulfill, 모두 fail → AggregateError"]
    Q3 -->|"정착 우선"| Race["Promise.race\n첫 정착 (성공/실패 무관)"]

내부 동작 - 타임라인 예제

p1 = 100ms 후 resolve, p2 = 50ms 후 reject, p3 = 200ms 후 resolve 라 가정.

gantt
    title Promise 조합 타임라인
    dateFormat X
    axisFormat %Lms
    section p1
    pending     :0, 100
    resolved    :milestone, 100, 0
    section p2
    pending     :0, 50
    rejected    :milestone, 50, 0
    section p3
    pending     :0, 200
    resolved    :milestone, 200, 0
    section all
    fail at p2 reject :crit, 50, 150
    section allSettled
    wait all    :0, 200
    section race
    settle at p2 :50, 150
    section any
    settle at p1 :100, 100
메서드정착 시점결과
all50ms (p2 reject)reject reason of p2
allSettled200ms (p3 resolve)[fulfilled, rejected, fulfilled]
race50ms (p2 reject)reject reason of p2
any100ms (p1 resolve)p1 value

AbortController 와 함께

Promise.all 은 fail-fast 이지만 나머지 Promise 는 cancel 되지 않는다. 실제로 취소하려면 AbortController 가 필요.

async function fetchAll(urls) {
    const controller = new AbortController();
    const { signal } = controller;

    try {
        const results = await Promise.all(
            urls.map(url => fetch(url, { signal }))
        );
        return results;
    } catch (e) {
        controller.abort();  // 나머지 요청 취소
        throw e;
    }
}

p-limit 패턴 (concurrency 제어)

Node.js 에서 대용량 작업 시 동시 실행 수를 제한.

import pLimit from 'p-limit';

const limit = pLimit(5);  // 동시 5개

const input = Array.from({ length: 100 }, (_, i) => `https://api.example.com/item/${i}`);
const results = await Promise.all(
    input.map(url => limit(() => fetch(url).then(r => r.json())))
);

내부적으로 Promise.all 에 전달되는 Promise 는 100개지만, 실제 동시 실행은 최대 5개로 제한. p-queue 는 우선순위까지 추가.

오류 수집 패턴

allSettled 결과를 구분해서 성공과 실패를 각각 처리.

const results = await Promise.allSettled(jobs.map(execute));

const { fulfilled, rejected } = results.reduce(
    (acc, r) => {
        if (r.status === 'fulfilled') acc.fulfilled.push(r.value);
        else acc.rejected.push(r.reason);
        return acc;
    },
    { fulfilled: [], rejected: [] }
);

console.log(`success: ${fulfilled.length}, failed: ${rejected.length}`);
if (rejected.length > 0) {
    // 일부 실패 로깅 / 재시도
    logger.error('partial failures', rejected);
}

재시도 wrapper 패턴

function withRetry(fn, retries = 3, delay = 500) {
    return async function (...args) {
        let lastError;
        for (let i = 0; i < retries; i++) {
            try {
                return await fn(...args);
            } catch (e) {
                lastError = e;
                if (i < retries - 1) {
                    await new Promise(res => setTimeout(res, delay * 2 ** i));
                }
            }
        }
        throw lastError;
    };
}

const safeProcess = withRetry(process, 3, 200);
const results = await Promise.allSettled(items.map(safeProcess));

Node.js 특유의 util.promisify

콜백 기반 API 를 Promise 로 변환.

import { promisify } from 'node:util';
import { readFile } from 'node:fs';

const readFileAsync = promisify(readFile);

const files = await Promise.all([
    readFileAsync('./a.json', 'utf8'),
    readFileAsync('./b.json', 'utf8'),
]);
const [a, b] = files.map(JSON.parse);

React 에서의 패턴

// 여러 API 동시 호출
function useDashboard(userId) {
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        let cancelled = false;
        Promise.all([
            fetch(`/api/user/${userId}`).then(r => r.json()),
            fetch(`/api/user/${userId}/posts`).then(r => r.json()),
        ])
            .then(([user, posts]) => {
                if (!cancelled) setData({ user, posts });
            })
            .catch(e => {
                if (!cancelled) setError(e);
            });
        return () => { cancelled = true; };
    }, [userId]);

    return { data, error };
}

cancelled 플래그로 unmount 후 state 설정 방지.

참고

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기