[자바스크립트] 배열의 비동기 작업

minidoo·2021년 3월 3일
0

자바스크립트 / NodeJS

목록 보기
21/27
post-thumbnail

1. forEach는 순차처리가 안된다?!

function arrayTest() {
  const promiseFunc = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('hi');
      }, 1000);
    })
  }

  let arr = [1, 2, 3, 4, 5];
  arr.forEach(async () => {
    const result = await promiseFunc();
    console.log(result);
  })
}

arrayTest();

우리는 위 코드에서 1초 간격으로 hi가 5번 출력되기를 기대했지만, 실행 결과는 1초 후 hi동시에 5번 출력된다. Promise, async, await 비동기 문법은 모두 사용했지만 원하는대로 실행되지 않는다.

forEach 를 사용했을 때, 비동기 처리가 되지 않는 이유는 무엇일까?

2. forEach의 동작원리

Array.prototype.forEach = function(callback) {
  for(let index = 0; index < this.length; index++) {
    callback(this[index], index, this);
  }
}

코드에서 볼 수 있듯이 forEach() 는 배열을 돌면서 callback을 실행하지만, 다음 항목으로 이동하기 전 현재 callback이 완료되기를 기다리지 않는다.

즉, forEach는 callback 호출에만 관여할 뿐 callback 함수가 비동기 작업을 하는지 안하는지에는 관심이 없다.
따라서 순차적으로 출력되는 결과를 얻을 수 없었던 것이다 😂

3. 비동기 작업 순차처리 ( for, for...of )

async function asyncForEach(array, callback) {
  for(let index = 0; index < array.length; index++) {
    await callback(this[index], index, this);
  }
}

forEach 자체를 async 함수로 만들고, 각 callback을 await 하면 순차처리가 가능하다.
이 원리를 바탕으로 forEach 대신 forfor...of를 사용하면 원하는 결과를 얻을 수 있다 :)

1) for 문 사용하기

async function arrayTest() {
  const promiseFunc = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('hi');
      }, 1000);
    })
  }

  let arr = [1, 2, 3, 4, 5];
  for(let i = 0; i < arr.length; i++) {		// for 문
    const result = await promiseFunc();
    console.log(result);
  }
}

arrayTest();

2) for...of 문 사용하기

async function arrayTest() {
  const promiseFunc = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('hi');
      }, 1000);
    })
  }

  let arr = [1, 2, 3, 4, 5];
  for(let i of arr) {		// for...of 문
    const result = await promiseFunc();
    console.log(result);
  }
}

arrayTest();

4. 병렬처리는 어떻게?!

배열 요소의 비동기 작업은 순차처리병렬처리로 실행된다.

순차처리는 배열 요소들에 대해 차례대로 작업을 수행하는 것이다. 병렬처리는 배열 요소들이 한꺼번에 비동기 작업을 수행하며, 실행 순서가 중요하지 않을 때 사용한다.

예를 들어, 단일 메뉴만 파는 호두과자 전문점이 있다고 가정하자. 호두과자는 오전에 다 만들어지고 판매는 오후부터 시작한다. 오후가 되자 사람들은 호두과자를 사기 위해 문 밖까지 길게 줄을 서 있다. 이때 호두과자를 받는 사람의 순서는 중요하지만(순차 처리), 미리 만들어지는 호두과자의 순서는 중요하지 않다(병렬 처리).

비동기 작업의 병렬처리를 위해서는 Promise.all을 사용한다.

const allWalnut = ['walnut1', 'walnut2', 'walnut3'];

function getWalnut(walnut) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(walnut);
      resolve();
    }, 1000);
  });
}

async function parallel(allWalnut) {
  const promises = allWalnut.map(walnut => getWalnut(walnut));	-- (1)
  await Promise.all(promises);	-- (2)
  console.log("Sold out :)");	-- (3)
}

parallel(allWalnut);

(1) map 함수는 배열의 각 요소를 돌면서 getWalnut()을 실행한 결과를 promises에 담는다.
(2) Promise.all은 pending 상태의 promises들이 모두 resolve가 될 때까지 기다린다.
(3) Promise.all도 resolve가 되면 "Sold out :)"을 출력한다.

순차처리를 했다면 3초 이상 걸리는 작업이 병렬 처리를 통해 약 1초만에 완료됐다.
이처럼 순서가 중요하지 않은 과정은 병렬 처리를 하는 것이 효율적이다.

5. Promise.all과 forEach의 차이점

"1. forEach는 순차처리가 안된다?!"에서 forEach문을 사용하면 5번의 hi가 동시에 출력된다고 언급했다. 그렇다면 '병렬 처리와 다른점이 뭐지?' 라는 의문점이 들 것이다.

호두과자 예시를 Promise.all에서 forEach 문으로 바꿔보았다.

const allWalnut = ['walnut1', 'walnut2', 'walnut3'];

function getWalnut(walnut) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(walnut);
      resolve();
    }, 1000);
  });
}

async function parallel(allWalnut) {
  allWalnut.forEach(async (walnut) => await getWalnut(walnut));
  await console.log("Sold out :)");
}

parallel(allWalnut);

forEach문을 사용하면 호두 과자가 팔리기도 전에 "Sold out :)" 이 출력된다.

순차처리처럼 forEach는 배열을 돌면서 callback을 호출하기만 할 뿐, 비동기 처리에는 관심이 없다. 호출한 callback들이 아직 pending 상태이지만, forEach는 할 일을 완료한 것이다.

배열의 비동기에 forEach를 사용하면 forEach는 callback 호출만을 하기 때문에 순차처리, 병렬처리 모두 호출 이후의 작업에 관여하지 않는다. 따라서 순차처리에서는 for과 for...of를 사용하여 비동기를 처리하고, 병렬처리에서는 map과 Promise.all을 사용하면 pending이 resolved 되는 타이밍을 감지할 수 있다.

참고 사이트

배열에 비동기 작업을 실시할 때 알아두면 좋은 이야기들
JavaScript: async/await with forEach( )

0개의 댓글