왜 JavaScript forEach()가 await와 함께 작동하지 않을까 (해결 방법)
Source: Dev.to
The Issue: await Inside Array.prototype.forEach
JavaScript에서 가장 혼란스러운 비동기 문제 중 하나는 개발자가 forEach() 콜백 안에 await를 넣고 반복이 순차적으로 실행될 것이라고 기대할 때 발생합니다.
const users = ["John", "Emma", "Michael"];
users.forEach(async (user) => {
await sendEmail(user);
console.log(`Email sent to ${user}`);
});
console.log("All emails processed");
예상되는 결과
Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed
실제로 일어나는 일
All emails processed
Email sent to John
Email sent to Emma
Email sent to Michael
forEach()는 콜백 안의 비동기 작업을 기다리지 않습니다. 모든 반복을 즉시 시작하고 바로 다음 코드로 넘어가기 때문에 console.log("All emails processed")가 sendEmail 프로미스가 해결되기 전에 실행됩니다. 이는 레이스 컨디션과 논리 오류를 초래하며, 결제 처리, 데이터베이스 쓰기, 이메일 캠페인 등 프로덕션 코드에서 특히 위험합니다.
Why forEach() Doesn’t Work With await
forEach()는 동기 작업을 위해 설계되었습니다.- 콜백의 반환값은 무시되며,
forEach()자체는undefined를 반환합니다. - 콜백이
async라 하더라도forEach()는 프로미스가 해결될 때까지 멈추지 않습니다. - 따라서 콜백 안의
await는 외부 흐름에 아무 영향을 주지 못합니다.
// 이것은 “각 작업을 하나씩 기다린다”는 의미가 아닙니다
users.forEach(async (user) => {
await sendEmail(user);
});
실제로는 “모두 즉시 시작하고 기다리지 않는다”는 뜻입니다.
Safe Sequential Execution with for...of
항목을 하나씩 순차적으로 처리하려면 async 함수 안에서 for...of 루프를 사용하는 것이 가장 간단합니다.
const users = ["John", "Emma", "Michael"];
async function processUsers() {
for (const user of users) {
await sendEmail(user);
console.log(`Email sent to ${user}`);
}
console.log("All emails processed");
}
processUsers();
출력
Email sent to John
Email sent to Emma
Email sent to Michael
All emails processed
for...of는 await를 존중하여 각 반복이 다음 반복이 시작되기 전에 끝나도록 보장합니다. 이 패턴은 다음과 같은 경우에 이상적입니다:
- 결제 처리
- 데이터베이스 쓰기
- 이메일 전송
- 파일 업로드
- 레이트‑리밋이 적용된 API 호출
- 인증 흐름
- 순서가 중요한 모든 시나리오
Parallel Execution with Promise.all
순서가 중요하지 않고 작업을 동시에 실행하고 싶을 때는 Array.prototype.map과 함께 Promise.all을 사용합니다.
const users = ["John", "Emma", "Michael"];
await Promise.all(
users.map(async (user) => {
await sendEmail(user);
console.log(`Email sent to ${user}`);
})
);
console.log("All emails processed");
모든 sendEmail 호출이 한 번에 시작되지만, await Promise.all은 모든 프로미스가 해결된 뒤에야 최종 로그를 실행합니다.
Common Mistake: await on forEach
일부 개발자는 forEach 호출 자체에 await를 시도합니다:
await users.forEach(async (user) => {
await sendEmail(user);
});
이 역시 실패합니다. forEach()는 undefined를 반환하므로 await가 기다릴 대상이 없기 때문입니다.
경험 법칙:
forEach와 await를 절대 함께 사용하지 마세요. 순차적인 작업은 for...of를, 병렬 작업은 Promise.all을 사용하세요.
Summary
forEach()는await를 무시하고undefined를 반환합니다.- 순차적인 비동기 실행이 필요할 때는
for...of를 사용하세요. - 병렬 비동기 실행이 필요할 때는
Promise.all(및map)을 사용하세요. - 기억하세요: “루프 자체를 await하고 있는가, 아니면 루프 안의 함수만 await하고 있는가?” — 이 질문은 버그를 빠르게 찾아냅니다.
이러한 패턴을 이해하면 JavaScript 비동기 코드를 더 깔끔하게 만들 수 있고, 추적하기 어려운 프로덕션 버그를 예방할 수 있습니다.