개발

자바스크립트 반복문에서 비동기 처리를 동기적으로 처리하기

동고킴 2021. 12. 5. 12:35
반응형

반복문에서 비동기 처리를 동기적으로 처리해보자.


아래처럼 메뉴를 주문받으면 제조 시간이 랜덤으로 0~4초가 걸리는 함수가 있다.
여러 건의 주문을 받았을 때, 주문을 동기 처리하여 결과를 한 번에 반환하려면 어떻게 해야 할까?

const order = menu => {
  const time = Math.floor(Math.random() * 4) * 100
  const result = `${menu}, ${time}`
  console.log("order", result)
  return new Promise(resolve => setTimeout(() => resolve(result), time))
}

const menus = [ 'americano', 'latte', 'cake', 'milk' ]


아래처럼 forEach를 사용하면 원하는 결과가 나오지 않는다. forEach와 async/await을 사용하였지만 forEach문 안에 있는 order 함수가 동기 처리가 아닌 비동기 처리가 된다. 왜 그럴까?

const menus = [ 'americano', 'latte', 'cake', 'milk' ]

console.time("소요시간")
let results = []
menus.forEach(async menu => {
  await order(menu).then(res => results.push(res))
})
console.log("results", results)
console.timeEnd("소요시간")

order americano, 300
order latte, 200
order cake, 100
order milk, 200
results [] ​​​​​at ​​​​​​quokka.js:14:0​
소요시간: 0.971ms 

forEach 동작 원리

기대했던 결과가 나오지 않은 이유가 왜일까.
이유는 forEach의 동작 원리에 있다. MDN에서 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을 호출한다. 콜백 인자로 들어간 익명 함수 전체를 기다려주지 않고 익명함수 내에서만 동기 처리가 된 것이다. 즉, async/await은 forEach문 안에서 사용할 수 없다.
그럼 반복문 안에서 어떻게 비동기 처리를 동기적으로 처리할 수 있을까?

비동기 순차처리 (Sequence)

forEach 대신 for ..of 문(또는 for문)을 사용하면 반복문 안에서 비동기 처리를 할 수 있다. 반복문 안에서 로직의 실해 순서가 보장되어야 할 때 사용하면 된다.

const menus = [ 'americano', 'latte', 'cake', 'milk' ]

console.time("순차 소요시간")

let results = []
for (let menu of menus) {
  await order(menu).then(res => results.push(res))
}

console.log("results : ", results)
console.timeEnd("순차 소요시간")

order americano, 300
order latte, 100
order cake, 100
order milk, 100
results :  [ 'americano, 300', 'latte, 100', 'cake, 100', 'milk, 100' ] 
순차 소요시간: 633.123ms 

비동기 병렬 처리 (Parallel)

만약 반복문 안에서 로직의 실행 순서가 보장되지 않아도 된다면 Promise.all()을 사용하여 병렬로 처리하면 된다. Promise.all()을 사용하면 앞의 요청의 기다리지 않고 병렬로 처리한다.

const menus = [ 'americano', 'latte', 'cake', 'milk' ]

console.time("병렬 소요시간")

let results = []
const promises = menus.map(async menu => await order(menu).then(res => results.push(res)))
await Promise.all(promises)

console.log("results : ", results)
console.timeEnd("병렬 소요시간")

order americano, 200
order latte, 300
order cake, 100
order milk, 300
results :  [ 'cake, 100', 'americano, 200', 'latte, 300', 'milk, 300' ] 
순차 소요시간: 306.99ms 


1) map을 사용하여 return 된 promise(pending 상태)들을 promises 배열에 담기
2) Promise.all()이 pending 상태인 promises들이 모두 resolve 될 때까지 기다림
3) Promise.all()이 resolve 되면 수행 완료

map을 사용한 이유는 새로운 배열에 pending 상태인 promise를 저장하기 위해서다. map도 forEach문과 마찬가지로 내부에 await이 존재하지 않고 단순히 callback을 실행시키며 다음 요소로 넘어간다.
ES5 array methods map, filter, reduce 등은 callbak이 async 하더라도 전체 method는 async 하지 않는다.

참고

반응형