Javascript

반복문 forEach()와 map() (feat. Promise를 이용한 병렬처리구조)

마손리 2022. 11. 28. 22:18

지난 포스트(https://mason-lee.tistory.com/2)의 for와 forEach()에 이어서 forEach()와 map()의 차이 그리고 비동기를 이용하여 병렬처리구조를 알아보겠습니다.

 

 

forEach()와 map()의 차이점

  forEach() map()
callback 유무 O O
return 허용 X O
속도 비교적 높음 비교적 낮음

둘의 차이점중 제일 크게 눈에 띄는것은 map()의 경우 return값을 반환한다는 것이다.

map()사용시 새로운 array로 원하는 값들을 반환할 수 있고 그로인한 메모리할당, 속도저하(forEach와 비교시)로 이어진다.

 

그래서 맵은 언제써???

사실 map()과 forEach()보단 map()과 filter()의 비교가 더 필요해 보인다.

forEach()의 경우 return값이 필요없는 경우 사용하면 될것이고 map()의 경우 array안의 특정값들을 골라 변경후 새로운array로 반환, filter()의 경우 특정값들만으로 이루어진 새로운 array가 필요할경우 사용해주면 좋다.

 

됫고... 빠른 예제 들어가자!

이제 이번 포스트의 메인 포커스로 Promise를 이용하여 여러 데이터들을 병렬구조로 처리하여 직렬보다 빠르게 데이터 처리를 해보자.

 

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const delay = () => {
  const randomDelay = Math.floor(Math.random() * 4) * 100;
  return new Promise((resolve) => setTimeout(resolve, randomDelay));
};

위와 같이 데이터들이 들어있는 array와 각 인덱스당 랜덤 딜레이를 줄 함수 하나를 만들고,

 

const asyncFn = async (arr) => {
  const newArr = arr.map((item) => {
    return delay().then(() => item);
  });
  
  const result = await Promise.all(newArr);
  console.log(result);
};

asyncFn(arr);

코드를 살펴보면 arr.map()으로 각각의 데이터들을 받아와 newArr라는 변수에 저장,

(처음에는 map()함수안에도 async await을 선언하여 각각의 데이터를 비동기로 처리하려 했는데, 생각해보니 병렬처리를 이용하는데 당연히 이전값들의 처리가 완료되는것을 기다릴 필요가 없어 제거해주었다.)

 

자, 이제부터 중요하다.

Promise.all()의 경우 무조건 array형태의 데이터 구조를 사용하여야한다. 그렇기에 map()을 사용하여 자동으로 array 형태로 리턴, 그 리턴된 array를 바로 Promise.all()안에 넣어준것이다. 

 

그렇다면 왜? Promise를 사용하고 또 await을 사용하여 비동기처리를 2번이나 해줘야 되는겨???

그 이유는 위에 사용한 Promise.all의 처리시간을 기다려 주어야 되기 때문입니다. 그렇지않으면 newArr의 값들이 처리되지 않은채로 즉 result가 아직 없는 상태일때 console.log()가 실행됩니다. 위의 코드에서 async함수 없이 사용도 가능합니다.

const newArr = arr.map((item) => {
  return delay().then(() => item);
});

Promise.all(newArr).then(console.log);

위의 예제에서 await을 사용하지 않을경우 async함수는 필요없게 되므로 .then을 이용하여 같은 결과를 출력해주는 모습입니다.

 

 

신기한거??

결과값

이렇게 결과값을 보면 신기한점이 생긴다. 분명 각 인덱스마다 랜덤한 딜레이를 부여했는데도 map()은 처음의 정렬을 기억하고 그 순서대로 다시 정렬을 한다는 점이다. 

 

실제 예제

아래 코드는 회원가입 이전 중복되는 username이나 email이 있는지 확인하는 코드이다.

const existUser = await db.findUsername(username)
const existEmail = await db.findEmail(email)
if (existUser) {
  return res.send({ ok: false, error: "같은 username이 존재합니다." })
}
if (existEmail) {
  return res.send({ ok: false, error: "같은 email이 존재합니다." })
}

위와 같이 (async는 생략됨) await으로 두번 처리하게되면 username을 DB내에서 검색뒤 가저오고, 그 과정이 끝난후에야 email을 DB에서 검색한 후 조건문으로 들어서게된다. 위의 코드를 병렬방식으로 바꿔주면,

 

const exists = await Promise.all([db.findUsername(username), db.findEmail(email)])
if (exists[0] || exists[1]) {
  return res.send({
  	ok:false, 
  	error:`같은 ${exists[0] ? "username" : exists[1] ? "email" : null}이 존재합니다.`
  });
}

Promise.all()을 이용하여 두가지의 데이터를 한번에 처리한뒤 변수 exists에 옴겨준 후 조건문으로 가게된다.

 

아 난 모르겠고 그냥 더 쉬운거 줘

옛다 쉬운거

 

우리가 코딩을 하다보면 API나 DB에서 데이터를 가저와야되는데 필수 데이터들을 가저오기 전까지는 다음 코드들을 실행하면 안되는 상황이 발생한다.

그 경우 직렬처리방식을 사용하게 된다면 하나의 데이터를 가저온 이후 다음 데이터를 가저오게 되므로 시간적인 손해가 발생하게 되므로 비동기를 이용하여 병렬처리방식을 선택해주면 하나의 데이터를 받아오는 중에라도 다른데이터를 받아 올수 있게 되며 Promise.all([])을 이용하여 전체 데이터를 묶어서 기다려준뒤 다음 코드가 실행되도록 만들 수 있다.

(동기, 비동기 얘기하니까 싱글스레드 마렵네... 시간이 된다면 좀더 공부한 후 포스팅 하겠슴미다...)

 

물론 단점도 존재한다. Promise.all의 경우 array단위로 진행시키기 때문에 하나의 데이터만 처리되었다고 바로 사용할수 있는건 아니다. 각자의 상황에 맞게 적절히 사용하는 것이 중요해 보인다.(물론 나도 잘 못하지만...)

 

다음 포스트에서는 비동기 처리방식인 callback, promise, async await 함수 들에대해 알아보겠습니다.

(다음 포스트: https://mason-lee.tistory.com/4)