컴퓨터 공부/🕸️ Web

Javascript 에서 forEach 함수는 비동기함수를 기다리지 않아요!

letzgorats 2023. 11. 29. 20:17

🚨 문제 상황

배열에 담긴 요소들을 하나씩 뽑아내는 작업을 할 때 아래와 같은 코드가 있다고 해보자.

const allu = new Array(10).fill('dachshund');
const result = []

const asyncFunction = (i) => {
  return new Promise(resolve => {
    setTimeout(() => {
      result.push(i)
      resolve()
    }, 1000);
  })
}

allu.forEach(async (_,i)=>{
    await asyncFunction(i)
})

console.log(result)

 

위 코드처럼 작성하면 result 값에 원하던 값이 들어가있지 않고 초기의 배열인 빈값이 출력됐다.

forEach는 내부에 들어있는 순차적으로배열을 돌며 callback 함수를 실행하기 때문에 callback이 동기인지 비동기인지에 따라 달라지지 않는다고 한다. 따라서 forEach는 비동기를 기다려주지 않는다!

 

내부 callback에서도 비동기 처리를 기다려주지 않고 result를 출력했을 때도 내부의 비동기처리를 기다려주지 않기 때문에 초기값이 출력된 것이다.

더보기

Note : forEach expects a synchronous function.
forEach does not wait for promises. Make sure you are aware of the implications while using promises (or async functions) as forEach callback.
mdn web docs

위와 같이 mdn web docs 의 Array.prototype.forEach() 설명에서는 forEach는 비동기 함수를 기다려주지 않는다고 명시되어 있다. forEach 뿐만 아니라 reduce, map, filter 등의 Array Prototype 메서드에서는 이러한 현상이 나타난다.

map, reduce, filter 순서

위 사진처럼 map, reduce, filter에서도 동일한 현상이 나타났다.

 

💚 해결 방안

1. for...of 

더보기

Note :  for...of 명령문은 반복가능한 객체 (Array, Map, Set, String, TypedArray, arguments 객체 등을 포함)에 대해서 반복하고 각 개별 속성값에 대해 실행되는 문이 있는 사용자 정의 반복 후크를 호출하는 루프를 생성합니다.
mdn web docs

for...of 명령문은 각 항목에 대해 비동기 처리를 기다리기 때문에 대안이 될 수 있지만, 그래서 위의 코드를 아래와 같이 수정하였다.

const allu = new Array(10).fill('dachshund');
const result = []

const asyncFunction = (i) => {
  return new Promise(resolve => {
    setTimeout(() => {
      result.push(i)
      resolve()
    }, 1000);
  })
}

(async() => {
    for(const i of allu) {
        await asyncFunction(i);
    }
    console.log(result)
})();

for of 사용

위와 같이 코드를 작성하니 비동기 처리가 성공적으로 이루어져서 result값이 제대로 나오지만, for...of 는 병렬적으로 처리하지 않기 때문에 배열이 길어질 경우에는 시간이 오래 걸리는 단점이 있다.

이러한 단점을 해결하기 위한 방법으로 Promise.all() 이 있다.

 

 

2. Promise.all()

const allu = new Array(10).fill('dachshund');
const result = []
const asyncFunction = (i) => {
  return new Promise(resolve => {
    setTimeout(() => {
      result.push(i)
      resolve()
    }, 1000);
  })
}

const promise = allu.map(async (_, i) => {
  await asyncFunction(i)
})

Promise.all(promise)
  .then(() => console.log(allu))

Promise.all() 사용

확실히 병렬적으로 처리하니까 for...of 보다 빠르게 result 값이 출력됐다.

 

✅ 결론

- 하나의 async function 내에서는 await 키워드를 만날 때마다 말그대로 기다렸다가(await) 비동기처리가 완료된 후에 직렬로 다음 task를 처리한다. 여러 비동기 task를 병렬적으로 시작시키려면 어떻게 해야할까? Promise.all 은 병렬로 동작하니까 요걸 쓰면 된다!

- 배열을 순회하며 비동기 요청을 보내는 경우,

  1. 비동기적으로 처리되어도 상관 없다 : forEach 함수를 써도 된다.(순서 제어 x)
  2. 각각의 요청에 대해 모두 await을 해야 한다 :
    • for 루프 안에서 await을 사용하면 각 비동기 작업이 순차적으로 완료될 때까지 기다릴 수 있다.
    • 이 방법은 각 요청이 완료되기를 기다려야 하므로 시간이 더 걸리지만, 순서를 제어할 수 있다
  3. 요청이 끝나는 순서는 상관 없다 : map 함수를 통해 promise를 반환하자.
    • map함수는 배열의 각 요소에 대해 주어진 함수를 실행하고, 그 결과로 새 배열을 만든다.
    • 비동기 함수를 map 내에서 호출할 때, map은 비동기 작업의 완료를 기다리지 않고 즉시 다음 요소로 넘어간다.
    • 이 경우 map은 비동기 함수의 Promise들을 새 배열에 담아 반환하지만, 이 Promise들은 다 완료되지 않은 상태이다.
    • map 함수 예제에서 console.log(result)는 map 함수가 반환한 후에 실행되기 때문에 result 배열은 아직 비어있거나 완료된 작업만 포함하고 있다.
    • Promise.all() 은 여러 Promise 를 병렬로 실행하고, 모든 Promise가 완료될 때까지 기다리는 메소드다.
    • promise.all() 예제에서의 Promise.all(promise)는 map에 의해 생성된 모든 프로미스가 완료될 때까지 기다린 다음, .then() 콜백을 실행한다.
    • 이 경우, .then() 콜백 내에서 console.log(allu)는 모든 promise가 완료된 후 실행되기 때문에, result 배열이 완성된 상태로 출력된다. 

요약하자면, 비동기함수를 콜백할 때, map은 비동기 작업의 완료를 기다리지 않고 각 요소에 대해 함수를 실행한 결과를 배열로 반환하는 반면, Promise.all()은 모든 프로미스가 완료될 때까지 기다리고, 그 결과를 처리할 수 있게 해준다.

반응형