본문 바로가기
Javascript

[Javascript; 자바스크립트] Promise.allSettled 완벽 가이드: 병렬 비동기 처리의 새로운 패러다임

by daddydontsleep 2024. 10. 29.
728x90
728x90

사진: Unsplash 의 Samsung Memory

Promise.allSettled 완벽 가이드: 병렬 비동기 처리의 새로운 패러다임

목차

  1. 소개
  2. Promise.allSettled vs Promise.all
  3. Promise.allSettled의 동작 방식
  4. 실전 예제
  5. 성능 고려사항
  6. 브라우저 지원 및 폴리필
  7. 모범 사례와 패턴
  8. 결론

소개

현대 웹 애플리케이션에서 여러 비동기 작업을 동시에 처리해야 하는 상황은 매우 흔합니다. 여러 API를 호출하거나, 다수의 파일을 처리하거나, 데이터베이스 쿼리를 병렬로 실행하는 등의 작업이 그 예입니다. Promise.allSettled()는 이러한 복잡한 비동기 시나리오를 우아하게 처리할 수 있게 해주는 강력한 도구입니다.

ES2020에서 도입된 Promise.allSettled()는 기존의 Promise.all()이 가진 한계를 보완하여, 모든 프로미스의 실행 결과를 보장받을 수 있게 해줍니다.

Promise.allSettled vs Promise.all

Promise.all의 한계

Promise.all()은 여러 프로미스를 병렬로 처리할 때 널리 사용되어 왔지만, 중요한 제한사항이 있습니다:

// Promise.all 예제
const promises = [
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3')
];

Promise.all(promises)
    .then(results => console.log('모든 요청 성공:', results))
    .catch(error => console.error('하나라도 실패하면 여기서 캐치:', error));

위 코드의 문제점은 하나의 프로미스라도 실패하면 전체가 실패로 처리된다는 것입니다. 실제 운영 환경에서는 일부 작업의 실패가 전체 프로세스를 중단시키지 않아야 하는 경우가 많습니다.

Promise.allSettled의 장점

Promise.allSettled()는 이러한 한계를 극복하여 다음과 같은 이점을 제공합니다:

  • 모든 프로미스의 완료를 기다립니다
  • 성공과 실패 여부에 관계없이 모든 결과를 받을 수 있습니다
  • 각 프로미스의 상태와 결과값을 상세히 확인할 수 있습니다
// Promise.allSettled 예제
const promises = [
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3')
];

Promise.allSettled(promises)
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`요청 ${index + 1} 성공:`, result.value);
            } else {
                console.log(`요청 ${index + 1} 실패:`, result.reason);
            }
        });
    });

Promise.allSettled의 동작 방식

Promise.allSettled()는 프로미스 배열을 입력받아 새로운 프로미스를 반환합니다. 반환된 프로미스는 모든 입력 프로미스가 완료(성공 또는 실패)되었을 때 이행됩니다.

반환값의 구조

각 결과 객체는 다음과 같은 구조를 가집니다:

// 성공한 경우
{
    status: 'fulfilled',
    value: /* 성공 결과값 */
}

// 실패한 경우
{
    status: 'rejected',
    reason: /* 실패 사유 */
}

실전 예제

1. 여러 API 동시 호출

async function fetchUserData() {
    const endpoints = [
        'https://api.example.com/user/profile',
        'https://api.example.com/user/posts',
        'https://api.example.com/user/friends'
    ];

    const promises = endpoints.map(url => fetch(url));

    const results = await Promise.allSettled(promises);

    const successResults = results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);

    const failedResults = results
        .filter(result => result.status === 'rejected')
        .map(result => result.reason);

    return {
        successful: successResults,
        failed: failedResults
    };
}

2. 파일 업로드 처리

async function uploadMultipleFiles(files) {
    const uploadPromises = files.map(file => {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('file', file);

            fetch('/upload', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(resolve)
            .catch(reject);
        });
    });

    const results = await Promise.allSettled(uploadPromises);

    // 업로드 결과 분석
    const summary = {
        total: files.length,
        successful: 0,
        failed: 0,
        failedFiles: []
    };

    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            summary.successful++;
        } else {
            summary.failed++;
            summary.failedFiles.push({
                name: files[index].name,
                error: result.reason
            });
        }
    });

    return summary;
}

3. 데이터 동기화 시나리오

async function syncData() {
    const tables = ['users', 'posts', 'comments', 'likes'];

    const syncPromises = tables.map(table => {
        return new Promise(async (resolve, reject) => {
            try {
                // 마지막 동기화 시간 확인
                const lastSync = await getLastSyncTime(table);

                // 변경된 데이터 가져오기
                const changes = await fetchChanges(table, lastSync);

                // 로컬 데이터베이스 업데이트
                await updateLocalDB(table, changes);

                resolve({ table, changesCount: changes.length });
            } catch (error) {
                reject({ table, error });
            }
        });
    });

    const results = await Promise.allSettled(syncPromises);

    // 동기화 결과 보고서 생성
    const report = {
        timestamp: new Date().toISOString(),
        results: results.map(result => {
            if (result.status === 'fulfilled') {
                return {
                    table: result.value.table,
                    status: 'success',
                    changesApplied: result.value.changesCount
                };
            } else {
                return {
                    table: result.reason.table,
                    status: 'error',
                    error: result.reason.error.message
                };
            }
        })
    };

    return report;
}

성능 고려사항

Promise.allSettled()를 사용할 때 고려해야 할 성능 관련 사항들:

  1. 메모리 사용량

    • 모든 프로미스의 결과를 메모리에 보관하므로, 대량의 프로미스를 처리할 때는 메모리 사용량을 모니터링해야 합니다.
  2. 동시성 제어

    • 너무 많은 프로미스를 동시에 실행하면 시스템 리소스에 부담이 될 수 있습니다.
    • 필요한 경우 배치 처리를 고려하세요.
async function processManyItemsInBatches(items, batchSize = 5) {
    const results = [];

    for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchPromises = batch.map(item => processItem(item));

        const batchResults = await Promise.allSettled(batchPromises);
        results.push(...batchResults);
    }

    return results;
}

브라우저 지원 및 폴리필

대부분의 최신 브라우저는 Promise.allSettled()를 지원하지만, 이전 버전의 브라우저를 지원해야 하는 경우 다음과 같은 폴리필을 사용할 수 있습니다:

if (!Promise.allSettled) {
    Promise.allSettled = function(promises) {
        return Promise.all(promises.map(p => Promise.resolve(p).then(
            value => ({
                status: 'fulfilled',
                value
            }),
            reason => ({
                status: 'rejected',
                reason
            })
        )));
    };
}

모범 사례와 패턴

1. 에러 처리 패턴

async function robustDataFetch() {
    const results = await Promise.allSettled(promises);

    // 에러 로깅
    results
        .filter(result => result.status === 'rejected')
        .forEach(({ reason }) => {
            console.error('작업 실패:', reason);
            // 에러 모니터링 시스템에 보고
            errorReportingService.log(reason);
        });

    // 성공한 결과만 반환
    return results
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value);
}

2. 재시도 패턴

async function fetchWithRetry(url, retries = 3) {
    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

    for (let i = 0; i < retries; i++) {
        try {
            return await fetch(url);
        } catch (error) {
            if (i === retries - 1) throw error;
            await delay(1000 * Math.pow(2, i)); // 지수 백오프
        }
    }
}

async function robustMultiFetch(urls) {
    const promises = urls.map(url => fetchWithRetry(url));
    return Promise.allSettled(promises);
}

결론

Promise.allSettled()는 현대 웹 애플리케이션에서 복잡한 비동기 작업을 처리하는 데 필수적인 도구입니다. 모든 프로미스의 결과를 보장받을 수 있고, 부분적인 실패를 우아하게 처리할 수 있다는 장점이 있습니다.

주요 사용 사례:

  • 여러 독립적인 API 호출
  • 파일 업로드 처리
  • 데이터 동기화
  • 배치 작업 처리

Promise.allSettled()를 효과적으로 활용하면 더 안정적이고 견고한 애플리케이션을 구축할 수 있습니다. 특히 일부 작업의 실패가 전체 프로세스를 중단시키지 않아야 하는 상황에서 매우 유용합니다.

728x90
300x250