function test() {
const promiseFunction = () =>
new Promise((resolve) => setTimeout(() => resolve("result"), 1000));
Array(10)
.fill(0)
.forEach(async () => {
const result = await promiseFunction();
console.log(result);
});
}
test();
위 코드는 분명 Promise
async/await
를 걸어 비동기로 작업해 result
가 1초마다 한 번씩 출력될 것 같지만, 1초 후 한꺼번에 10개가 출력된다.
정답부터 말하자면 아래처럼 forEach
를 for문
또는 for...of문
으로 변경하면 순차적으로 동작한다.
function test() {
const promiseFunction = () =>
new Promise((resolve) => setTimeout(() => resolve("result"), 1000));
let array = Array(10).fill(0);
// (1) test를 async로 감싸는 대신, for문을 async 즉시실행함수로 감싸도 된다
(async () => {
// (2) forEach 대신 for ... of를 사용한다
for (let element of array) {
const result = await promiseFunction();
console.log(result);
}
})();
}
test();
forEach
는 배열 요소를 돌면서 callback을 실행할 뿐, 한 callback이 끝날 때까지 기다렸다가 다음 callback을 실행하는 것이 아니다. forEach
는 10개의 요소를 차례로 돌며 async
함수를 실행한다. 실행된 async
함수 10개가 한꺼번에 1초를 기다렸다가 resolve 된 result
를 콘솔에 출력하는 것이다.
즉, forEach
는 자신이 실행하는 callback 함수가 비동기 작업을 하는지 아닌지는 관심 없는 것이다.
배열의 요소들에 대해 실행 순서가 보장되어야 할 경우 순차 처리 (위에서 언급한 for...of문
등)을 사용한다.
실행 순서가 중요하지 않으며 성능이 중요한 경우에는 병렬 처리를 해주는 것이 더 효율적일 것이다.
예를 들어, 복수의 파일을 다운로드할 때 굳이 순서가 중요하지 않다면 복수의 URL에 요청을 보내 병렬으로 처리하는 것이 효율적이다.
const target_url = ["ur11", "ur12", "url3"];
// 다운로드에 약 1초가 걸리는 비동기 함수라고 가정
function async_download(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(url);
resolve();
}, 1000);
});
}
async function parallel(array) {
const promises = array.map((url) => async_download(url));
await Promise.all(promises);
console.log("all done :)");
}
parallel(target_url);
map
은 array의 각 요소를 돌면서 async_download 함수를 병렬적으로 실행한 결과 (promise)를 새로운 promises 배열에 담는다.🤔 그럼 앞서 소개된 forEach 사용 코드도 병렬 처리 아닌가? 왜 굳이 Promise.all을 사용해야 하지?
forEach를 사용한 아래 코드를 보면 확실한 차이를 알 수 있다.
const target_url = ["ur11", "ur12", "url3"];
function async_download(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(url);
resolve();
}, 1000);
});
}
async function parallel(array) {
array.forEach(async (url) => {
await async_download(url);
});
// all done은 언제 찍힐까?
await console.log("all done :)");
}
parallel(target_url);
Promise.all에서 그랬던 것처럼, 병렬 처리가 완료된 후 ”all done”이 출력되길 의도했으나,
실상은 “all done”이 가장 먼저 출력되고 있다.
모든 비동기 작업이 끝나고 수행되길 원했던 함수가 맨 처음 실행되고 있다. 왜일까?
forEach는 배열을 돌며 callback을 호출하기만 하면 맡은 임무를 다 한 것으로 생각하고 종료된다. 사실 호출한 callback들은 pending 상태로 resolve 되지 않았지만, forEach 입장에서는 할 일을 다 한 것이다.
forEach는 callback만 실행하고 끝나버리기에 비동기 작업의 처리 상태를 추적하지 못하고, 따라서 이후의 흐름을 제어하기도 어렵다.
하지만 map과 promise.all을 사용하면 callback들이 return하는 promise들을 새로운 배열에 담아두었다가 모든 promise가 resolved 되는 타이밍을 감지할 수 있다.
따라서 배열의 요소들에 비동기 작업을 실시한 후 (순차든, 병렬이든) 어떤 작업을 해야 한다면 forEach가 아닌 map과 Promise.all을 사용하는 것이 좋다.
for
또는 for...of
문을 통해,map
+ Promise.all
을 통해 구현할 수 있다.참고 출처 : hanameee님의 배열 비동기 글