JavaScript에서 병렬 비동기 처리하기: Promise.all과 Promise.allSettled

2024년 12월 10일

여러 사용자의 프로필을 한 번에 가져오거나, 다수의 파일을 동시에 업로드하는 등 실제 웹 개발에서는 여러 비동기 작업을 동시에 처리해야 하는 경우가 자주 있습니다. 이러한 상황에서 각 작업을 순차적으로 처리하면 전체 실행 시간이 크게 늘어날 수 있습니다.

비동기 작업을 병렬로 처리하면 이러한 문제를 효과적으로 해결할 수 있습니다. JavaScript에서는 Promise.allPromise.allSettled를 통해 여러 비동기 작업을 동시에 처리할 수 있습니다.

먼저 Promise의 기본 개념을 살펴보겠습니다.

Promise 기본 개념

Promise는 비동기 작업의 최종 완료(또는 실패)와 그 결과값을 나타내는 객체입니다. Promise는 세가지 상태를 가집니다.

  1. 대기(pending): 초기 상태, 이행하거나 거부되지 않은 상태
  2. 이행(fulfilled): 작업이 성공적으로 완료된 상태
  3. 거부(rejected): 작업이 실패한 상태
//Promise 생성 예제
const promise = new Promise((resolve, reject) => {
	
	//여기에 비동기 작업 수행을 true 라고 간주
	const success = true;
	
	if(success){
		resolve('작업 성공!'); //상태 - fulfilled 
	} else {
		reject('작업 실패...'); //상태 - rejected;
	}
});

위에서 만든 promise를 - then catch 방식으로 결과를 받아볼 수 있고

promise.then(result => {
	console.log(result); //'작업 성공!'
}).catch(error => {
	console.log(error); //'작업 실패...'
});

익숙한 async/await 방식으로 결과를 받을 수도 있습니다.

try {
  const result = await promise;
  console.log(result);
} catch (error) {
  console.log(error);
}

Promise.all로 동시 요청 처리하기

Promise.all은 여러 Promise를 병렬로 실행하고 모든 작업이 완료될 때까지 기다립니다.

개별적으로는 이렇게 fetch 요청을 하는 코드를

const result1 = await fetch("https://api.example.com/1");
const result2 = await fetch("https://api.example.com/2");
const result3 = await fetch("https://api.example.com/3");

Promise.all 로 묶어서 병렬로 요청할 수 있습니다.

const results = await Promise.all([
  fetch("https://api.example.com/1"),
  fetch("https://api.example.com/2"),
  fetch("https://api.example.com/3"),
]);

Promise.all은 다음과 같은 특징이 있습니다.

1 입력과 출력

  • 입력: Promise 배열
  • 출력: 모든 Promise의 결과값을 담은 배열을 반환하는 새로운 Promise
  1. 결과 순서를 보장합니다.
    • 반환되는 결과 배열의 순서는 입력된 Promise 배열의 순서와 동일합니다.
    • 실제 완료 순서와 관계없이 원본 순서를 유지합니다.
  2. 에러 처리
    • 🚨 하나의 Promise라도 실패하면 전체가 실패로 처리됩니다.
    • 첫 번째로 발생한 에러가 catch 블록으로 전달됩니다.

Promise.all의 일반적인 사용 패턴

URL 배열을 Promise 배열로 매핑하여 Promise.all로 실행할 수 있습니다.

예시 - URL 배열로 API 요청하기

const urls = [
    "https://api.example.com/1",
    "https://api.example.com/2",
		"https://api.example.com/3"
];

try {
// fetch를 사용해 url을 프라미스로 매핑
    const requests = urls.map(url => fetch(url));
    const responses = await Promise.all(requests);

// 응답을 JSON으로 변환
    const jsonDataArray = await Promise.all(responses.map(res => res.json()));
    console.log(jsonDataArray);
} catch (error) {
    console.log('에러 발생:', error);
}

예시 - Parameter만 변경하여 여러 요청 보내기

// 여러 사용자 ID로 데이터 조회하기
async function fetchUserData(userIds) {
    const baseUrl = 'https://api.example.com/users';

    try {
        const requests = userIds.map(id =>
            fetch(`${baseUrl}/${id}`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json'
                }
            })
        );

        const responses = await Promise.all(requests);
        const users = await Promise.all(responses.map(res => res.json()));

        return users;
    } catch (error) {
        console.error('데이터 조회 실패:', error);
        throw error;
    }
}

// 사용 예시
const userIds = [1001, 1002, 1003];
fetchUserData(userIds)
    .then(users => console.log('사용자 데이터:', users))
    .catch(error => console.log('에러:', error));

Promise.all의 에러 처리 특성

Promise.all은 "실패 즉시 거부(fail-fast)" 방식으로 동작합니다. 주요 특징은 다음과 같습니다:

  1. 하나의 Promise라도 실패하면 전체가 실패로 처리됩니다.
  2. 첫 번째 발생한 에러가 catch 블록으로 전달됩니다.
  3. 다른 Promise들의 결과는 무시됩니다(성공했더라도).

이러한 특성은 모든 작업이 성공적으로 완료되어야 하는 경우에 유용하지만, 일부 실패를 허용하고 성공한 일부 결과를 사용하고 싶은 경우에는 Promise.allSettled를 사용하는 것이 더 적절합니다.

Promise.allSettled로 부분 실패 허용하기

때로는 일부 작업이 실패하더라도 성공한 결과만이라도 활용하고 싶을 수 있습니다. Promise.allSettled는 각 Promise의 성공/실패 여부와 관계없이 모든 Promise의 처리가 완료될 때까지 기다립니다. 각 Promise의 최종 상태와 결과값을 모두 반환한다는 점이 Promise.all과의 가장 큰 차이점입니다.

// Promise.allSettled 사용 예시
const promises = [
    fetch('https://api.example.com/users/1'),
    fetch('https://api.example.com/users/2'),
    fetch('https://api.example.com/users/error'),// 실패할 수 있는 요청
];

const results = await Promise.allSettled(promises);

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

각 Promise의 결과는 다음과 같은 형태로 반환됩니다:

[
    { status: "fulfilled", value: result1 },
    { status: "fulfilled", value: result2 },
    { status: "rejected", reason: error }
]

예제

여러 서비스의 데이터를 취합하여 대시보드를 구성하는 예제를 살펴보겠습니다

async function fetchDashboardData(userId) {
    const endpoints = {
        profile: `/api/user/${userId}`,
        orders: `/api/user/${userId}/orders`,
        reviews: `/api/user/${userId}/reviews`,
        notifications: `/api/user/${userId}/notifications`
    };

    const results = await Promise.allSettled(
        Object.entries(endpoints).map(([key, url]) => 
            fetch(url).then(res => res.json())
        )
    );

    const dashboard = {};

    results.forEach((result, index) => {
        const key = Object.keys(endpoints)[index];
        if (result.status === 'fulfilled') {
            dashboard[key] = result.value;
        } else {
            dashboard[key] = null;
            console.error(`${key} 데이터 조회 실패:`, result.reason);
        }
    });

    return dashboard;
}

결론

Promise.allPromise.allSettled는 각각의 장단점이 있습니다. 모든 작업의 성공이 필수적인 경우는 Promise.all을, 일부 실패를 허용하는 경우는 Promise.allSettled를 사용하면 효과적입니다. 이들을 적절히 활용하면 비동기 작업을 더 효율적으로 처리할 수 있으며, 더 나은 사용자 경험을 제공할 수 있습니다.

참고자료


TAGS
JAVASCRIPT