
await을 반복문 안에서 사용하는 건 코드가 쥐도새도 모르게 멈추거나 기대한 것보다 느리게 실행되는 걸 보기 전까지는 꽤나 직관적인 것처럼 느껴집니다. API 호출이 한번에 실행되는 대신에 하나하나 실행되거나 map() 함수와 await을 섞은 방법이 생각한 것처럼 잘 작동하지 않았다면 당장 여기 앉아보세요. 이야기해보자구요!
for 반복에서의 await유저 리스트를 데이터를 하나씩 가져오는 상황이라고 가정해봅시다.
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
이건 작동하긴 하지만, 순차적으로 작동합니다. fetchUser(2)는 fetchUser(1)이 끝나기 전에는 시작되지 않죠. 순서가 중요하다면 괜찮겠지만, 독립적인 네트워크 호출에 있어서는 비효율적입니다.
map() 안에선 await 하지 않기map() 함수 안에서 await를 사용하는 경우, 그 결과로 생성되는 Promise 객체들을 올바르게 처리하지 않아 자주 혼란스럽기도 합니다.
const users = [1, 2, 3];
const results = users.map(async id => {
const user = await fetchUser(id);
return user;
});
console.log(results); // [Promise, Promise, Promise] – 실제 데이터 아님
이 코드는 문법상으로나 동작상으로는 올바르게 작동하지만(프로미스 배열을 반환), 기대한 결과는 아닙니다. 이건 Promise 객체가 resolve될 때까지 기다리지 않습니다.
병렬적으로 호출하려면 이렇게 해야합니다.
const results = await Promise.all(users.map(id => fetchUser(id)));
이제 모든 요청이 병렬적으로 처리되고, result는 불러온 실제 유저 데이터를 포함하게 됩니다.
Promise.all()은 호출 하나만 실패해도 빠르게 실패한다Promise.all()을 사용할 때 하나만 거부되도 전체 동작이 실패하게 됩니다.
const results = await Promise.all(
users.map(id => fetchUser(id)) // fetchUser(2)에서 에러 발생 가능
);
fetchUser(2)가 에러(404나 네트워크 에러)가 나면 Promise.all() 호출 전부 거부되고 결과로는 아무것도 반환되지 않아요(성공한 시도를 포함해서).
⚠️ Note:
Promise.all()가 첫 에러를 발생시키면 다른 결과도 폐기됩니다. 나머지 Promise 객체가 작동하더라도 첫 에러만 전달됩니다. 각각 객체를 따로 다루지 않는다면 말이죠.
Promise.allSettled()를 사용하기const results = await Promise.allSettled(
users.map(id => fetchUser(id))
);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('✅ User:', result.value);
} else {
console.warn('❌ Error:', result.reason);
}
});
이렇게 한다면 몇몇이 실패하더라도 모든 결과를 확인할 수 있습니다.
const results = await Promise.all(
users.map(async id => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`Failed to fetch user ${id}`, err);
return { id, name: 'Unknown User' }; // 실패했을 때 대체값
}
})
);
이 방법은 --unhandled-rejections=strict 설정이 된 Node.js 같은 더 엄격한 환경에서 경고나 충돌을 발생시킬 수 있는 다루지 못한 promise 거부도 미연에 방지합니다.
for...of + await 사용(순차적 실행)만약 다음 동작이 이전 수행 결과에 영향을 받거나 API 제한이 있는 상황이라면 이 방법을 사용해보세요.
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
async 함수 컨텍스트를 사용하지 않았다면 이렇게 해보세요.
(async () => {
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
})();
Promise.all + map() 사용(병렬적 실행)동작이 독립적이거나 동시에 수행 가능할 때는 이렇게 해보세요.
const usersData = await Promise.all(users.map(id => fetchUser(id)));
더 안전한 배칭 작업을 위해선 Promise.allSettled()나 try/catch 문을 사용하세요.
요컨대 CPU-bound 작업, 병렬 작업이 별로 눈에 띄는 차이는 없을지도 모릅니다. 그러나 API 호출처럼 I/O가 많은 작업이라면 병렬 작업이 눈에 띄게 총 수행 시간을 줄여줄 수 있어요.
빠른 작업 속도가 필요하지만, API 제한을 반드시 준수해야한다면 p-limit처럼 스로틀링 도구를 사용해보세요.
import pLimit from 'p-limit';
const limit = pLimit(2); // 2개의 패칭을 한번에
const limitedFetches = users.map(id => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);
💡 톺아보기
await이 함수 밖에서 어떻게 작동하는지 보고 싶다면, ES modules에서 상위 레벨 await을 사용하는 법에 대한 포스트를 살펴보세요.
| 목표 | 패턴 | 동시성 |
|---|---|---|
| 순서 준수, 하나씩 실행 | for...of + await | 1 |
| 동시 실행, 순서 없음 | Promise.all + map() | ∞(튕기지 않을 경우) |
| 제한된 동시성 | p-limit, PromisePool 등 | N(설정에 따라) |
forEach()에서 절대 await 쓰지 않기이건 아주 흔한 함정이에요.
users.forEach(async id => {
const user = await fetchUser(id);
console.log(user); // ❌ await 작동 X
});
forEach() 반복문은 async 함수를 기다려주지 않아요. 정해진 타이밍이나 순서에 대한 보장 없이 작동하게 됩니다.
대신에, 이렇게 해보세요.
for...of + awaitPromise.all + map()JavaScript의 async 모델은 강력하지만 await을 반복문 안에서 쓰는 건 의도가 필요합니다. 당신의 필요에 따라서 async 로직을 구성하세요.
for...ofPromise.allallSettled() / try-catchp-limit 등