왜 JavaScript forEach()가 await와 함께 작동하지 않을까 (해결 방법)
Source: Dev.to
문제: Array.prototype.forEach 안에서 await 사용
JavaScript에서 가장 혼란스러운 async 문제 중 하나는 개발자가 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 프로미스가 해결되기 전에 실행됩니다. 이는 레이스 컨디션과 논리 오류를 초래하며, 결제 처리, 데이터베이스 쓰기, 이메일 캠페인 등 프로덕션 코드에서 특히 위험합니다.
forEach()가 await와 함께 작동하지 않는 이유
forEach()는 동기 작업을 위해 설계되었습니다.- 콜백의 반환값은 무시되며,
forEach()자체는undefined를 반환합니다. - 콜백이
async라 하더라도forEach()는 프로미스가 해결될 때까지 멈추지 않습니다. - 따라서 콜백 안의
await는 외부 흐름에 아무 영향도 주지 못합니다.
// 이것은 “각 작업을 하나씩 기다린다”는 의미가 아닙니다
users.forEach(async (user) => {
await sendEmail(user);
});
실제로는 “모두 즉시 시작하고 기다리지 않는다”는 뜻입니다.
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 호출
- 인증 흐름
- 순서가 중요한 모든 시나리오
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은 모든 프로미스가 해결된 뒤에 마지막 로그가 실행되도록 보장합니다.
흔한 실수: forEach에 await 사용하기
일부 개발자는 forEach 호출 자체에 await를 걸어 보려고 합니다:
await users.forEach(async (user) => {
await sendEmail(user);
});
하지만 이것도 실패합니다. forEach()는 undefined를 반환하므로 await가 기다릴 대상이 없기 때문입니다.
경험 법칙:
forEach와 await를 절대 함께 쓰지 마세요. 순차 작업은 for...of를, 병렬 작업은 Promise.all을 사용하세요.
요약
forEach()는await를 무시하고undefined를 반환합니다.- 순차 비동기 실행이 필요하면 **
for...of**를 사용하세요. - 병렬 비동기 실행이 필요하면
Promise.all(및map)을 사용하세요. - 기억하세요: “루프 자체를 기다리는가, 아니면 루프 안의 함수만 기다리는가?” — 이 질문이 버그를 빠르게 찾아냅니다.
이 패턴들을 이해하면 JavaScript 비동기 코드를 더 깔끔하게 작성할 수 있고, 추적하기 어려운 프로덕션 버그를 예방할 수 있습니다.