[JS] 반복문에서 async/await 활용하기

Jiheon Kim·2023년 1월 25일
1

Javascript

목록 보기
1/6
post-thumbnail

반복문에서 async/await로 비동기 처리를 한다면 어떻게 될까?

// index.js
const url = "https://jsonplaceholder.typicode.com/todos/";
const temp = [1, 2, 3];

temp.forEach(async (el) => {
  console.log(" ======>");
  const res = await fetch(url + el).then((res) => res.json());
  console.log(" <======");
  console.log("res ::", res.id);
});
  1. temp 배열을 돌면서 콜백함수를 실행하는데 async를 사용하여 promise로 감싸주었다
  2. temp의 각 원소 값을 baseUrl에 더해서 fetch를 한다
  3. fetch는 await을 사용하여 정상적으로 비동기 처리가 끝나면 로그를 찍게한다
  4. 배열의 length만큼 반복한다

내가 예상한 결과

// console
======>
<======
res :: 1
======>
<======
res :: 2
======>
<======
res :: 3

실행 결과는?

// console
 ======>
 ======>
 ======>
 <======
res :: 3
 <======
res :: 1
 <======
res :: 2

temp함수가 forEach를 돌면서 async가 붙은 콜백함수를 실행해주고, 그 안에서 fetch를 await해서 fetch가 된 결과 값을 기다린 다음 로그를 찍어줄 거라 예상했는데 forEach는 await을 무시하고 자기 할 일만 했다.

무슨 문제가 있을까 ?

forEach는 await을 해주지않았다 따라서 3번의 요청이 거의 동시에 일어났고 forEach는 끝났지만 콜백함수는 아직 종료되지 않았기 때문에 비동기 처리가 끝나는 대로 response을 받아서 로그가 찍혔다. 만약 이 코드를 작성한 사람이 이렇게 3번의 요청을 병렬적으로 동작하기를 원했다면 상관없지만 처음 예상한 대로 3번의 요청을 await하여 순차적으로 처리하길 원했다면 예상치 못한 결과가 나온 것이다.

forEach는 왜 이런 결과가 나온 걸까?

for (let x = 0; x < temp.length; x++) {
  async () => await fetch();
}

forEach는 지금 이런 식으로 동작하고 있을 것이다 (배열의 크기만큼 돌면서 비동기 처리를 하고 있으니까) 여기서 자세히 보면 재미있는 것을 발견할 수 있는데 for문이 도는 바깥쪽 context와 안쪽 콜백함수의 context 이렇게 2개의 context가 존재한다는 것이다 그렇기 때문에 안쪽 콜백함수에서 비동기처리를 하고 await을 해도 바깥쪽 for문을 막을 수 없다

(async function () {
  for (let x = 0; x < temp.length; x++) {
    // 만약 forEach에서 돌때마다 await을 하게된다면 순차적으로 실행되지 않을까? 
    await (async () => await fetch()); // 예시를 들었을뿐 잘못된 문법 입니다. 
  }
})();

가만 생각해보면 for문을 돌면서 순차적으로 실행을 해주려면 콜백함수도 async/await으로 되어 있어야 할텐데 그렇지않기 때문에 forEach는 비동기처리를 기다려 주지 않는다고 생각한다.

💡 참고로 forEach뿐만 아니라 forEach처럼 동작하는 map, filter 이런 애들도 똑같다

해결방법은?

처음 예상한 순차적으로 처리하기 위해서는 for문 혹은 for ... of 을 사용해야 한다

const baseUrl = "https://jsonplaceholder.typicode.com/todos/";
const temp = [1, 2, 3];

(async function () {
  for (let el of temp) {
    console.log(" ======>");
    const res = await fetch(baseUrl + el).then((res) => res.json());
    console.log(" <======");
    console.log("res ::", res.id);
  }
})();

안정적인 병렬처리를 위해 Promise API를 사용해보자

1) 순차처리 vs 병렬처리 ?

지금은 순차적으로 처리하기 위해서 forEachfor ... of 로 수정하였지만, 만약 3번의 요청이 서로에게 영향을 주지 않는 경우 당연 병렬처리를 하는 것이 유리하다 (1번의 요청이 1초가 걸린다면 순차처리는 3초가 걸리고 병렬처리는 1초가 걸린다)

2) Promise.all() 이란 ?

배열로 받은 모든 프로미스가 fulfill(이행) 된 이후, 모든 프로미스의 반환 값을 배열에 넣어 반환한다. 그런데 만약 배열에 있는 프로미스 중 하나라도 reject가 호출된다면, 성공한 프로미스 응답은 무시된 채로 그냥 바로 catch로 빠져버리게 된다.

const responseArray = await Promise.all([promise1, promise2, promise3]);

3) Promise.race(), Promise.allSettled()

하나의 promise라도 실패하면 에러로 처리되는 promise.all은 호출이 많이 질수록 비효율적으로 작업을 처리한다. 반면, Promise.race는 최초 resolved된 값만 resolve하고 Promise.allSettled는 여러 프로미스를 병렬적으로 처리하되, 하나의 프로미스가 실패해도 무조건 이행한다.
Promise.all과 같은 형태로 실행하지만, 배열로 받은 모든 프로미스의 fulfilled, reject 여부와 상관없이, 전부 완료만 되었다면(not pending) 해당 프로미스들의 결과를 배열로 반환한다.

[
  { status: "fulfilled", value: 1 }, // 성공 
  { status: "rejected", value: 2 }, // 실패
];

4) 코드를 최적화 해보자

배운 Promise API를 사용해서 코드를 수정해보자

const baseUrl = "https://jsonplaceholder.typicode.com/todos/";
const temp = [1, 2, 3];

(async function () {
  const response = temp
    .map((el) => fetch(baseUrl + el)
    .then((res) => res.json()));
  const responseArray = await Promise.all(response);
  console.log(responseArray);
})();
// console
[
  { userId: 1, id: 1, title: "delectus aut autem", completed: false },
  { userId: 1, id: 2, title: "quis ut nam facilis et officia qui", completed: false },
  { userId: 1, id: 3, title: "fugiat veniam minus", completed: false },
]

결론

forEach와 같은 배열 API는 단순히 콜백함수만 실행시켜준다. 그렇기 때문에 콜백함수에서 비동기 처리를 하던 무엇을 하던 자기 할 일을 다한 forEach는 종료된다. 하지만 콜백함수들은 pending 상태로 아직 남아있게 되어 결과적으로 forEach가 종료된 이후 비동기 작업의 상태를 추적할 수 없게 된다
하지만 promise.all()은 다르다 콜백 함수에서 반환하는 값들을 차곡차곡 배열에 넣어놓고 모든 비동기 처리가 끝나는 타이밍을 감지할 수 있기 때문에 이후의 흐름을 제어하기 쉽다

profile
누군가는 해야하잖아

0개의 댓글