개발자
류준열
Array 메소드에 async await 넣으면 순서 보장 안된다.
회사에서 리포트 발행 기능을 만드는데, 리포트 페이지가 뒤죽박죽으로 발행되었다.
// 첫 코드
const processPages = async () => {
const promises = (previewPDFs:HTMLElement[])
.map(async (page) => await makePdf(page));
await Promise.all(promises);
};
processPages()
.than(result=>pdf.save(result))
.catch((err) => message.error(err));
위 코드에서 순서가 보장되지 않는 이유는 다음과 같다.
- 위 제목처럼 Array 메소드는 async 콜백을 기다리지 않는다. (MDN forEach)
- Promise.all은 병렬 처리되기 때문 (MDN Promise)
배열 [1,2,3,4,5,6,7,8,9,10]
으로 예시코드를 작성해보았다.
map으로 비동기 처리를 한 코드
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async () => {
const promises = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(async (num) => {
await sleep(500); // 각 인덱스 기본 0.5초 대기
if (num === 3) {
await sleep(3000); // num이 3일 때 추가로 3초 대기
}
console.log(num);
});
await Promise.all(promises);
})();
3을 제외한 각 요소마다 0.5초 sleep을 주었는데 아래와 같이 1,2,4,5,6,7,8,9,10은 단숨에 찍히고 3은 3초후에 찍혔다.
// 찍힌 콘솔 // 1 // 2 // 4 // 5 // 6 // 7 // 8 // 9 // 10 // 3 // 3초 후
이유는 다음과 같다.
-
.map()
이[1,2,...10]
배열을 순회하며 10개의 async를 거의 동시에 순차적으로 호출한다. -
호출된 10개의 async 함수는 sleep(500)을 만나 10개의 0.5초 타이머가 거의 동시에 시작된다.
-
약 0.5초가 지나면 9개(num===3 제외)의
sleep(500) Promise
가 거의 동시에 완료된다.
이어서 각 console.log(num)이 실행되어 1,2,4,5,6,7,8,9,10이 단숨에 출력된다. -
n===3인 경우 0.5초 대기 후
sleep(3000)
을 추가로 만나 3초를 더 기다렸다가 console.log(num)을 실행한다. -
Promise.all
의 역할은 10개의 Promise가 완료될때까지 기다리는 역할을 한다. 여기서는 가장 오래 걸리는 작업(n===3의 3.5초)이 끝날 때까지 기다린다.
이러한 원리로 pdf출력때는 오래 걸리는 3페이지를 기다리지 않고 4페이지가 먼저 출력되었던 것이다.
Promise.all 제거 및 for문으로 변경
변경한 코드는 다음과 같다.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async () => {
for (const num of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
await sleep(500); // 각 인덱스 기본 0.5초 대기
if (num === 3) {
await sleep(3000); // num이 3일 때 추가로 3초 대기
}
console.log(num); // 작업 완료 후 출력
}
})();
for문으로 변경했을때는 순서가 보장되기 때문에 다음과 같이 콘솔이 찍힌다.
// 1 (0.5초 후) // 2 (0.5초 후) // 3 (3.5초 후) // 4 (0.5초 후) // 5 (0.5초 후) // 6 (0.5초 후) // 7 (0.5초 후) // 8 (0.5초 후) // 9 (0.5초 후) // 10 (0.5초 후)
왜 배열 메소드는 비동기를 기다려주지 않을까?
쉽게말하면 n개의 각 async이 비동기로 실행되기 때문이다.
이 글을 참고했습니다.
forEach의 polyfill을 보면 다음과 같다.
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function (callback, thisArg) {
thisArg = thisArg || window;
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
forEach에 비동기 callback을 넣으면 다음과 같다.
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function (callback, thisArg) {
thisArg = thisArg || window;
for (var i = 0; i < this.length; i++) {
async (num) => {
if (num === 3) {
await wait(3000); // num이 3일 때 3초 대기
}
return await makePdf(num); // 나머지 숫자는 병렬로 실행
})(thisArg, this[i], i, this)}};
};
반복문안에서 비동기 함수를 실행만 시키고 각 인덱스마다 실행되는 콜백이 끝날때 까지 기다리지 않는 것이다.
각 인덱스마다 실행되는 콜백을 기다리려면 다음과 같이 되어야 한다.
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = async function (callback, thisArg) {
thisArg = thisArg || window;
for (var i = 0; i < this.length; i++) {
await callback.call(thisArg, this[i], i, this);
}
};
}
마무리
검색하며 블로그들을 보니까 배열 메소드는 비동기를 기다려주지 않기 때문에 Promise.all을 사용하라는 말이 많았다.
하지만 Promise.all는 전달받은 모든 Promise를 병렬로 처리하기 때문에 순서 보장에는 맞지 않는다.
예를 들어서 num===3에서 wait(3000)이 실행되어 Pending 상태에 놓이더라도 다른 Promise들은 계속 진행되는 것이다.
그렇기 때문에 순서도 보장하려면 for문을 이용하는 것이 좋다. (for await ...of도 있다.)