자바스크립트 forEach async await 순차 처리 오류 원인과 올바른 비동기 반복문 해결 가이드
자바스크립트 Array.forEach 내부에서 async await를 사용했을 때 순차 처리가 되지 않고 완료 시점을 놓치는 원인을 규명하고, for...of 루프를 통한 안전한 순차 처리 구현 및 Promise.all 병렬 처리 전환 방법을 단계별로 정리합니다.
forEach 콜백에 async를 적용할 때 발생하는 순차 처리 실패 증상
자바스크립트에서 배열의 각 요소를 순회하며 외부 API 호출이나 데이터베이스 입출력 같은 비동기 작업을 순차적으로 실행하려는 상황을 가정해 보겠습니다. 많은 초급 개발자나 입문자가 익숙한 Array.prototype.forEach 구문을 선택하고 콜백 함수 앞에 async 키워드를 붙인 뒤 내부에서 await fetch()를 호출하는 방식을 시도합니다. 그러나 코드를 실행해 보면 기대했던 것처럼 첫 번째 요청이 끝나고 두 번째 요청이 실행되는 순차적 대기가 전혀 이루어지지 않습니다.
실제로 나타나는 대표적인 증상은 배열 요소의 개수만큼 비동기 작업이 사실상 동시에 시작되어 네트워크 요청이 한 번에 쏟아지는 현상입니다. 또한 forEach 반복문 바로 아래에 작성해 둔 후속 로직이 반복문 내부의 비동기 작업이 모두 끝나기도 전에 먼저 실행되어 버립니다. 이로 인해 데이터가 아직 준비되지 않은 상태에서 화면을 렌더링하거나 최종 완료 알림을 띄우면서 undefined 오류가 발생하고 데이터 정합성이 깨지는 문제가 빈번하게 보고됩니다.

forEach 메서드가 비동기 프로미스 반환값을 기다리지 않는 원인
이 오류가 발생하는 근본적인 원인은 Array.prototype.forEach의 내부 명세와 설계 동작에 있습니다. forEach는 전달받은 콜백 함수를 배열의 각 인덱스마다 단순히 동기적으로 호출할 뿐이며, 콜백 함수가 반환하는 결과값을 완전히 무시하고 항상 undefined를 반환합니다. ECMAScript 표준에 정의된 forEach 구현 알고리즘에는 콜백 함수가 프로미스를 반환하더라도 이를 resolve할 때까지 기다려 주는(await) 로직이 전혀 포함되어 있지 않습니다.
따라서 콜백 함수를 async로 선언하면 해당 함수는 실행될 때마다 프로미스 객체를 즉시 반환하지만, forEach는 이 반환된 프로미스를 처리하지 않고 다음 인덱스의 콜백을 연이어 동기적으로 호출합니다. 결과적으로 배열 안의 모든 비동기 콜백 함수가 이벤트 루프의 마이크로태스크 큐에 한꺼번에 등록되며, 외부 스코프 입장에서는 forEach 메서드가 비동기 작업의 완료 여부와 무관하게 즉각 종료된 것으로 간주됩니다. 이는 자바스크립트 엔진의 버그가 아니라 forEach 본래의 동기 순회 특성 때문입니다.

실행 시점 로그와 네트워크 탭을 통한 비동기 누락 점검 순서
해당 오류가 의심될 때는 코드를 무작정 수정하기 전에 실행 순서와 시간 간격을 명확히 검증하는 절차를 거쳐야 합니다. 가장 간단한 확인 순서는 각 반복 회차의 시작 시점과 종료 시점에 타임스탬프를 기록하는 것입니다. console.time()과 console.timeEnd(), 또는 Date.now()를 활용해 각 작업이 호출되는 시각을 콘솔에 출력해 보면, 앞선 작업이 끝나기 전에 다음 작업의 시작 로그가 연달아 찍히는 것을 직접 확인할 수 있습니다.
브라우저 개발자 도구의 네트워크(Network) 패널을 열어 워터폴(Waterfall) 차트를 점검하는 방법도 필수적입니다. 요청들이 계단식으로 순차 진행되지 않고 일렬로 동시에 출발하는 막대 그래프 형태로 나타난다면 비동기 대기 처리가 누락되었음을 뜻합니다. 마지막으로 반복문 바로 다음에 배치된 완료 안내 로그가 개별 작업 완료 로그보다 상단에 먼저 출력되는지 확인하여 외부 실행 흐름이 중단되지 않고 조기 종료되는 문제를 확정 진단합니다.

for...of 반복문과 전통적인 for 루프를 활용한 올바른 순차 처리 구현
배열 요소를 하나씩 차례대로 대기하면서 순차적으로 처리해야 할 때는 forEach 대신 표준 for...of 반복문이나 전통적인 for (let i = 0; ...) 루프를 사용해야 합니다. for...of 구문은 async 함수 내부에서 직접 작성될 때 await 키워드를 올바르게 인식합니다. 자바스크립트 실행 엔진은 반복 블록 내부의 await 표현식을 만나면 해당 프로미스가 해결될 때까지 다음 반복 회차로 넘어가지 않고 일시 정지 상태를 유지합니다.
예를 들어 async function processList(items) { for (const item of items) { await uploadData(item); } } 형태로 작성하면, uploadData의 실행이 완전히 완료된 뒤에야 다음 요소의 작업이 시작됩니다. 반복문이 모두 종료된 후에 실행해야 하는 후속 코드가 있다면 for...of 블록 바깥 하단에 배치하는 것만으로 전체 작업의 완료 시점을 정확히 보장받을 수 있습니다. 인덱스 번호가 직접 필요한 작업이라면 일반적인 인덱스 기반 for 루프를 사용해도 동일한 순차 대기 효과를 얻을 수 있습니다.

순차와 병렬 처리의 선택 기준 및 ESLint 정적 분석을 통한 재발 방지
모든 비동기 반복 작업을 굳이 순차적으로 실행할 필요는 없습니다. 앞선 작업의 응답 데이터가 다음 작업의 입력값으로 쓰여야 하거나, 외부 API의 초당 요청 제한(Rate Limit)을 준수해야 하는 상황에서는 for...of를 통한 순차 처리가 적합합니다. 반면 서로 독립적인 데이터를 단순 조회하는 경우라면 Array.prototype.map과 Promise.all()을 조합하여 모든 요청을 병렬로 동시에 처리하고 전체 완료를 한 번에 기다리는 편이 성능상 훨씬 유리합니다.
이러한 실수를 팀 차원에서 원천 차단하기 위해서는 정적 코드 분석 도구인 ESLint의 no-await-in-loop 규칙이나 no-async-promise-executor 같은 룰을 프로젝트 설정에 적용하는 것이 좋습니다. 또한 배열 고차 함수 콜백에 비동기 함수를 전달할 때 경고를 띄워주는 플러그인을 활성화해 두면 코드 리뷰 단계 이전에 잠재적인 순서 뒤섞임 오류를 조기에 감지하고 방지할 수 있습니다.