js의 forEach는 배열에서 사용할 수 있는 함수로, 배열의 각 요소를 순회하는 콜백함수를 전달하여 실행시키는 함수이다. forEach에는 다음과 같은 특징이 있다.
const array = [1, 2, 3, 4];
array.forEach(function(element, index, array) {
console.log('Element:', element);
console.log('Index:', index);
console.log('Array:', array);
});
그리고, 개발을 하던 중, 각 배열의 요소를 순회하며 하나씩의 인자를 가지고 비동기 요청을 해야하는 상황이 생겼다.
// 예시
tasks.forEach(async (task) => {
await promise(task);
});
각 배열을 순회하며 도는 콜백함수이기에, await
를 걸어두면 당연히 비동기 작업도 각 요소의 순서대로 작업이 이루어질 줄 알았다. 그러나 비동기 작업은 정상적으로 이루어지지 않았다.
조금 더 구글링을 한 결과, mdn에서는 정확히 forEach는 비동기작업을 지원해주지 않는다고 명시해놓았다.
forEach() expects a synchronous function — it does not wait for promises. Make sure you are aware of the implications while using promises (or async functions) as forEach callbacks.
const ratings = [5, 4, 5];
let sum = 0;
const sumFunction = async (a, b) => a + b;
ratings.forEach(async (rating) => {
sum = await sumFunction(sum, rating);
});
console.log(sum);
// Naively expected output: 14
// Actual output: 0
즉, forEach
는 본질적으로 동기 방식으로 작동한다. 이는 각 콜백이 비동기 작업을 포함하더라도, forEach
자체는 이를 기다리지 않고 즉시 다음 요소로 진행함을 의미한다. 결과적으로 비동기 작업의 완료 순서는 예측할 수 없게 된다. 예를 들어
const tasks = [1, 2, 3, 4];
tasks.forEach(async (task) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Task ${task} done`);
});
위 코드의 실행결과는 아래와 같을 수 있다. 하지만, 실제로는 거의 동시에 실행되며 순서가 섞일 수도 있기에 주의해야 한다.
Task 1 done
Task 2 done
Task 3 done
Task 4 done
// 또는
Task 1 done
Task 3 done
Task 2 done
Task 4 done
그렇다면 비동기작업을 순차적으로 처리하거나 모든 비동기 작업이 완료될 때 까지 기다리려면 forEach
가 아닌 다른 방법을 써야 한다. 어떠한 방법들이 있을까?
for ... of
루프는 비동기 작업을 순차적으로 처리하는 것을 보장하며 실행된다.
const tasks = [1, 2, 3, 4];
for (const task of tasks) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Task ${task} done`);
}
}
모든 비동기 작업이 순서 상관없이, 병렬로 실행되길 원한다면 Promise.all
을 사용할 수 있다.
const tasks = [1, 2, 3, 4];
const promises = tasks.map(task => new Promise(resolve => {
setTimeout(() => {
console.log(`Task ${task} done`);
resolve();
}, 1000);
}));
await Promise.all(promises);
비동기 작업의 뜻은, 다른 작업이 완료될 때까지 기다리지 않고 바로 다음 작업을 실행하는 방식이다. 예를 들어, 서버 요청, 파일 읽기, 타이머 등은 비동기 작업이다. 이러한 작업은 나중에 완료되지만, 그 사이에 다른 코드가 계속 실행될 수 있다.
forEach
는 동기적으로 작동한다. 즉, 배열의 각 요소에 대해 콜백 함수를 실행한 후, 그 함수가 완료되기를 기다리지 않고 다음 요소로 넘어간다. 비동기 작업을 콜백 함수 내에서 수행하더라도, forEach
는 이를 기다리지 않는다.