
JavaScript에서 비동기(Asynchronous) 처리는 필수적이다. 콜백 지옥을 해결하기 위해 Promise가 등장했고, 그 Promise를 더 읽기 쉽게 만들기 위해 async/await 문법이 도입되었다. 하지만 async/await만으로는 모든 비동기 시나리오를 효율적으로 처리할 수 없다. 특히 여러 개의 비동기 작업을 동시에 처리해야 할 때, 우리는 Promise.all과 Promise.allSettled라는 강력한 도구를 사용해야 한다.
그럼 이 세 가지 개념이 서로 어떤 특징을 가지고, 어떤 차이점이 있는지, 언제 사용하는지에 대해 알아보자.
async/await비동기 코드를 동기적으로
async/await는 Promise를 기반으로 동작하는 문법적 설탕(Syntactic Sugar)이다. 이 문법의 핵심적인 존재 이유는 단 하나, 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 만들어 가독성을 극대화하는 것이다.
.then() 체이닝 방식에 비해, async/await는 try...catch 구문을 사용하여 코드의 흐름이 위에서 아래로 자연스럽게 이어지므로 로직을 파악하기가 훨씬 쉽다.
// Promise (.then)
function fetchDataWithPromise() {
fetch('api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
}
// async/await
async function fetchDataWithAsync() {
try {
const response = await fetch('api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
async/await의 가장 큰 함정은, 각 await 키워드가 해당 Promise가 완료될 때까지 코드 실행을 '일시 정지'시킨다는 점이다. 만약 서로 의존성이 없는 여러 비동기 작업을 await로 순차적으로 처리하면, 심각한 성능 저하를 유발할 수 있다.
async function fetchMultipleData() {
// fetchUser(1)이 끝나기를 기다렸다가 (1초 소요)
const user = await fetchUser(1);
// 그 다음에 fetchPosts()를 실행하고 기다린다 (1초 소요)
const posts = await fetchPosts();
// 총 소요 시간: 2초
return { user, posts };
}
Promise.all병렬 처리로 성능 극대화
Promise.all은 위와 같은 비효율을 해결하기 위해 존재한다. 서로 의존성이 없는 여러 개의 Promise를 동시에(in parallel) 실행하고, 모든 Promise가 완료될 때까지 한 번만 기다린다.
Promise.all은 여러 비동기 작업을 병렬로 처리하여, 전체 실행 시간을 가장 오래 걸리는 작업 하나에 맞춰준다.
async function fetchMultipleDataParallel() {
// 두 요청을 동시에 시작한다.
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts()
]);
// 총 소요 시간: 1초
return { user, posts };
}
Promise.all의 가장 큰 특징이자 단점은, 전달된 Promise 중 단 하나라도 실패(reject)하면, 즉시 전체가 실패하고 다른 Promise의 성공 결과는 모두 무시된다는 점이다.
try {
const results = await Promise.all([
Promise.resolve('성공1'),
Promise.reject('실패!'), // 이 Promise가 실패하는 순간
Promise.resolve('성공2') // 이 Promise의 결과는 받지 못한다.
]);
} catch (error) {
console.error(error); // "실패!"
}
"모든 작업이 반드시 성공해야만 다음 단계로 넘어갈 수 있는" 시나리오에 적합하다.
Promise.allSettled실패에 관대한 병렬 처리
Promise.allSettled는 Promise.all의 'All or Nothing' 문제를 해결하기 위해 등장했다. 전달된 모든 Promise가 성공하든 실패하든 상관없이, 모든 작업이 끝날 때까지 기다렸다가 각 Promise의 결과를 모아서 반환한다.
Promise.allSettled는 절대 실패(reject)하지 않는다. 대신, 각 결과에 대해 { status: 'fulfilled', value: ... } 또는 { status: 'rejected', reason: ... } 형태의 객체 배열을 반환한다. 이를 통해 개발자는 각 작업의 성공/실패 여부를 직접 확인하고 개별적으로 처리할 수 있다.
const results = await Promise.allSettled([
Promise.resolve('성공1'),
Promise.reject('실패!'),
Promise.resolve('성공2')
]);
/*
results 결과:
[
{ status: 'fulfilled', value: '성공1' },
{ status: 'rejected', reason: '실패!' },
{ status: 'fulfilled', value: '성공2' }
]
*/
const successfulResults = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value); // ['성공1', '성공2']
"여러 작업 중 일부가 실패하더라도, 성공한 작업의 결과는 반드시 필요한" 시나리오에 매우 유용하다.
| 구분 | async/await (순차적) | Promise.all | Promise.allSettled |
|---|---|---|---|
| 실행 방식 | 순차적 (Sequential) | 병렬적 (Parallel) | 병렬적 (Parallel) |
| 에러 처리 | 하나가 실패하면 즉시 catch 블록으로 이동 | 하나라도 실패하면 전체가 즉시 실패 | 모든 작업이 끝난 후, 각 결과의 status로 성공/실패 판단 |
| 주요 사용처 | 비동기 작업 간에 순서(의존성)가 있을 때 | 모든 작업이 반드시 성공해야 할 때 | 일부 작업이 실패해도 나머지 결과가 필요할 때 |
async/await는 비동기 코드의 가독성을 위한 기본 문법이며, Promise.all과 Promise.allSettled는 서로 의존성이 없는 여러 비동기 작업을 동시에 처리하여 성능을 최적화하기 위한 강력한 도구이다. 이들은 서로 경쟁하는 관계가 아니라, 함께 사용될 때 비로소 비동기 코드의 가독성과 성능을 모두 잡을 수 있는 최고의 파트너가 된다.