Promise 잘 사용하기 (2) - 반복문 돌리기

joonseokhu·2020년 4월 12일
11

Promise 잘 사용하기

목록 보기
2/2

한번에 비동기 작업을 실행하고 결과 취합하기 (Promise.all)

const getInfoOfUsers = ids => {
  const result = Promise.all( // [A]
      ids.map((id) => { // [B]
          return axios.get(`https://example.com/user/${id}`) // [C]
            .then(res => res.data)
      })
  );

  return result; // [D]
}

useEffect(() => {
  getInfoOfUsers([12, 34, 567, 345, 123, 888])
    .then(users => {
      setState(users)
    })
})

Promise.all 을 통한 비동기 반복문 처리는 모든 비동기 작업을 동시에 수행한다.
10개의 비동기 작업을 Promise.all을 통해 처리할 경우, 자바스크립트는 10개의 비동기 작업을 동시에 시작한 후, 10개의 작업이 모두 종결(settled)되는 순간 결과를 취합해 배열로 만든다.

A. Promise.all 메서드는 배열 안에 들어있는 프로미스들을 전부 해소시킨 후 해소된 배열을 비동기 결과로 만든다. 단 배열 속 프로미스들 중 하나라도 에러로 처리될경우 경우 결과는 에러로 처리된다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

이때 유의할 점은, 예를들어 길이가 10인 배열을 가지고 Promise.all로 비동기 작업을 걸고, 이중 단 하나의 요소만 에러를 내어서 전체 Promise.all의 결과가 에러로 처리될 때, 그렇다고 해서 나머지 9개 요소의 비동기 작업이 취소되지는 않는다는 것이다.

B. 이러한 Promise.all 을 사용하기 위해 배열을 map 메서드로 돌리면서 새로운 배열의 각 요소에 promise가 담기도록 만들면 된다.

C. map 메서드의 콜백 리턴값은 새로운 배열의 요소가 된다. 이 코드에서 콜백 리턴값은 axios의 리턴값, 즉 프로미스 값이다.

D. Promise.all의 결과 역시 마찬가지로 프로미스이다. 프로미스 그대로 리턴하면 마찬가지로 외부에서 getInfoOfUsers 를 비동기 함수처럼 사용할수 있다.

비동기 반복문을 순차적으로 실행시키기 (Promise reducer)

순차적인 실행순서를 보장하는 비동기 반복문을 만들어내려면 배열의 reduce 메서드를 활용해야 한다.

reduce를 활용한 비동기 반복문은, Promise 체인을 반복시키기 위한 코드 패턴이다. 비동기 작업을 하나하나 기다리면서 반복문이 작동하는게 아니라, 어떤 순서로 비동기 작업이 작동해야 하는지 명시하기 위한 코드가 반복되는 것이다.

Promise.resolve()
  .then(() => {
    return 비동기작업(첫번째요소)
  }).then(() => {
    return 비동기작업(두번째요소)
  }).then(() => {
    return 비동기작업(세번째요소)
  }).then(() => {
    return 비동기작업(네번째요소)
  }) // ...

결과적으로 이러한 형태의 코드를 만들어 내는 것이 목표라고 생각하면 된다.

useEffect(() => {
  const users = [1, 45, 67, 12, 976, 473, 2]

  users.reduce((prevPromise, user) => { // [A]
    
    const currentPromise = prevPromise.then(() => { // [B]
      return axios.delete(`https://example.com/user/${user}`) // [C]
    });

    return currentPromise;

  }, Promise.resolve()) // [A]

})

A. reduce의 콜백 첫번째 인자는 이전 작업에서 리턴된 값이며, 두번째 인자는 반복작업을 돌릴 대상배열의 현재 값이다. reduce의 두번째 인자는 초기값, 즉 첫번째 작업 수행시 콜백의 첫번째 인자에 들어갈 값이다.

초기값에 들어가는 Promise.resolve()는 그저 then 메서드를 사용할 수 있게 도와줄 뿐 실제 결과물에 아무런 영향을 주지 않는다. reduce 콜백 내에서 이전 결과에 이어서 끊임없이 then 메서드를 사용하기 위한 준비작업 정도로 받아들이면 된다.

B. 이전 작업에서 받아오는 prevPromise는 비동기 결과값인 Promise이다. 여기에 then 메서드를 써서 해당 비동기 작업 직후 무슨 동작을 할지 정의가 가능해진다.

C. 현재 배열요소를 사용해서 비동기 작업을 호출한 후 그 결과인 Promise를 리턴한다. Promise의 then/catch메서드에서 리턴된 값은 그것이 비동기 결과이든 동기결과이든 상관없이 비동기 코드로 wrapping 되어서 그 다음 then 메서드에서 접근 가능해진다.

D. 또한, 그렇게 작성한 프로미스 체인 역시 다시 reduce의 콜백에서 통째로 리턴한다.
즉, 앞에서 전달받은 프로미스 체인에 체인을 하나 더 걸어주고 뒤로 전달해주는 셈이 된다.

코드를 좀 더 압축하면 다음과 같이 된다.

useEffect(() => {
  const users = [1, 45, 67, 12, 976, 473, 2]

  users.reduce((prevP, user) => (
    prevP.then(() => (
      axios.delete(`https://example.com/user/${user}`)
    ))
  ), Promise.resolve())

})

Promise reducer가 체인 뿐 아니라 결과값도 reduce 하도록 리팩토링

이렇게 만든 코드는 비동기 순차실행은 보장하지만 비동기 결과값을 담아오진 않는다. 초기값에 빈 배열을 물려주고 그 안에 하나씩 추가하도록 만들어보자.

useEffect(() => {
  const users = [1, 45, 67, 12, 976, 473, 2]

  const results = users.reduce((prevPrms, user) => (
    prevPrms.then(async prevRes => { // [A]
      const currRes = await axios.delete(`https://example.com/user/${user}`) [B]
      return [...prevRes, currRes] [C]
    })
  ), Promise.resolve([])) // [D]

  results.then(data => {
    setState(data)
  })
})

A. 기존엔 then 메서드에서 인자로 아무것도 받아오고 있지 않았지만, 파라미터를 지정해 이전 비동기 작업의 결과를 받아올 수 있게 만든다. 이전 비동기 작업 결과는 배열일거라고 전제하고 작업한다. 또한, 편의를 위해 then 메서드의 콜백도 async 처리해준다.

B. 현재 비동기 작업의 결과를 바로 리턴하지 말고 await로 처리한 뒤 변수에 담아둔다.

C. spread 로 이전 비동기 결과와 현재 비동기 결과가 병합된 배열을 만들어 리턴한다. 우리가 then 메서드에서 이전 작업의 결과를 배열이라고 전제했었다. 현재 작업의 결과물은 결국 다음 작업에서 이전 결과물이 되므로 현재 작업 역시 이전 작업의 결과물과 같은 구조인 배열로 결과를 내야한다.따라서, 작업이 진행될 수록 배열에 요소가 점점 쌓이는 식이 되어야 한다.

D. 작업들간에 전달해주는 값이 배열이고, 작업이 진행됨에 따라 배열 안에 요소가 점점 쌓이게 되므로, 초기값은 빈 배열이어야 한다. Promise.resolve 함수의 인자에 어떤 값을 넣으면 그 값은 Promise.resolve 의 비동기 결과로 처리된다.

Promise reducer 패턴 함수화

reduce를 활용한 순차적 비동기 반복문을 만드는 패턴은 유용하지만, 매번 저렇게 길고 복잡한 코드를 쓰는건 쉽지 않다. 따라서 재사용하기 쉽게 저 패턴을 함수화해보자.

const reducePromises = (array, callback) => ( // [A]
  array.reduce((prevPrms, currElem, index) => ( // [B]
    prevPrms.then(async prevRes => {
      const currRes = await callback(currElem, index); // [C]
      return [...prevRes, currRes];
    })
  ), Promise.resolve([]))
)

A. 첫번째 인자로 배열을, 두번째 인자로 각 반복작업에서 수행될 비동기코드를 받도록 한다.

B. 첫번째 인자로 받는 배열을 사용해서 reduce를 하도록 한다. 기왕 하는것 index까지 받아서 전달하도록 하자.

C. 두번째 인자로 받는 함수가 비동기 결과를 리턴할거라는 전제하에, 받아온 함수에 현재배열요소와 현재 인덱스를 전달하면서 호출하고, 비동기 결과를 받아서 사용한다.

useCallback(() => {
  const users = [1, 45, 67, 12, 976, 473, 2];

  reducePromises(users, user => {
    return axios.delete(`https://example.com/user/${user}`)
  })
  .then(results => {
    setState(results)
  })

})

이제 함수를 별도로 저장해두고, 순차적인 비동기 반복이 필요할 땐 이렇게 활용하면 된다.

profile
풀스택 집요정

3개의 댓글

comment-user-thumbnail
2020년 6월 6일

하나의 개념에서 점차 빌드업 해나가고 정리하고 함수화하는 과정에 대한 유익한 정보네요!
하나 궁금한게 있어서 여쭤보고싶은데
Promise reducer 패턴 함수화 부분에서
reduce 메소드의 index 매개변수 옵션을 사용하셨는데 실질적인 코드에 적용되진 않았지만 사용하신 이유가 있을까요?
혹시 에러를 잡아낼 때 에러를 던지는 요소를 찾기 위해서 사용하기 위해서 활용하는건가요?

1개의 답글
comment-user-thumbnail
2022년 2월 23일

선생님 너무 감사합니다.
그동안 비동기에 대한 공부가 필요하다고 생각하고 미루고 있었는데
정말 정리 잘해주셔서 저같은 코린이도 잘 배우고 갑니다.

답글 달기