Promise.all 이해하기 Thumbnail

면접 복기

10 min read
JavaScript

Promise.all 이해하기

최근에 면접을 보게 됐는데, 거기서 API 최적화 방안에 대한 질문이 나왔다. 알고는 있었지만 대비를 하지 못했던 질문이어서 엉뚱한 답변만 했던 것 같다.

ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ

그래서 이번에 API 최적화 방안에 대한 질문의 대답을 준비하면서 Promise.all의 동작 방식에 대해서 공부해보면 좋겠다고 생각했다.

Promise

Promise는 JS 프론트엔드를 하다보면 무조건 사용하게 되는 개념이다. 비동기 처리에 대한 상태를 가지고 있는 객체로, 비동기 작업의 대기, 완료, 실패 상태에 따른 처리를 할 수 있도록 만든 Wrapper 객체이다.

Promise.all

프로미스에 .all 이 붙었다. 용어만 봤을 때 프로미스를 전체 실행한다는건가? 싶었다. 처음에 생각했던 것과 비슷하게 Promise.all은 여러 개의 비동기 작업을 동시에 병렬적으로 실행하고, 모든 작업이 완료될 때까지 기다렸다가 최종 결과를 한 번에 배열로 돌려주는 메서드이다.

  • 모든 작업이 성공하면 각 Promise의 결과가 배열 형태로 한 번에 반환
  • 한 개라도 실패하면, 첫 번째로 실패한 이유만 반환하고 나머지는 무시됨
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results); // [1, 2, 3]
  })
  .catch((error) => {
    console.error(error);
  });

Promise.race와 Promise.all의 차이점

Promise.all은 모두 성공해야 반환하지만, race는 가장 먼저 완료되거나 실패한 프로미스의 값을 하나만 반환한다.

  • Promise.all은 전체를 기다림
  • Promise.race는 가장 빠른 하나만을 기다림

race는 우리가 아는 레이스와 같은 개념으로 가장 빠르게 실행되는 결과만을 원할 때 사용한다. 언제 쓰이는지 쉽게 이해하기 위한 예제는 아래와 같다.

Promise의 다른 메서드들

Promise에는 all과 race 말고도 다른 메서드들이 있다.

  • Promise.all
  • Promise.race
  • Promise.allSettled
  • Promise.any
  • Promise.resolve
  • Promise.reject

allSettled는 모든 결과를 확인하는 것까지는 Promise.all과 똑같다. 차이점은 실패하는 것까지 다 기다린다는 점이다. Settled의 뜻이 안정된, 정착된의 뜻을 가지고 있는 것을 생각하면 완전히 종료될 때까지 기다리는 메서드라는 것을 알 수 있다.

any는 Promise.race와 비교할 수 있다. Promise.any는 가장 먼저 처리되는 결과를 반환하는 것까지는 똑같지만, 가장 먼저 **“성공”**하는 것만 기다린다. 성공 전에 실패가 나오더라도 무시하고, 다음 성공을 기다린다. 만약 모두 실패하는 경우에는 AggregateError를 반환한다.

Promise.all로 어떤 최적화를 할 수 있을까?

Promise.all로는 어떤 일을 할 수 있을까. 일단 코드를 줄일 수 있는 것이 장점이라고 생각된다. 같은 레벨의 API 요청이라면 일일히 요청할 필요없이 한꺼번에 요청해서 코드도 줄이고 속도도 줄이는 것이 좋을 것 같다.

만약 순차적으로 요청해야하는 API라면? A라는 데이터를 불러오고, A를 payload로 하는 B API 요청을 보내기 위해서는 A API가 완수되기 까지를 기다려야한다. 이 경우에는 Promise.all은 사용하지 않아도 된다.

// 순차 처리(비최적화)
const users = await fetchUsers();   // 300ms
const products = await fetchProducts(); // 200ms
const orders = await fetchOrders();    // 250ms
// 총 대기: 약 750ms

async / await은 순서가 보장되지 않는 비동기 작업들의 순서를 보장하기 위해서 사용하는 키워드들이다. 즉, 요청 함수 앞에 await을 붙이는 순간에는 그 아래 함수들은 요청이 끝나고 결과가 나오기 전까지는 실행되지 않는다.

그렇다면 같은 레벨이 아닌 함수를 위와 같이 3번 연속으로 동기적으로 실행하게되면 대기 시간이 길어지게 된다.

// 병렬 처리(최적화)
const [users, products, orders] = await Promise.all([
  fetchUsers(),           // 300ms
  fetchProducts(),        // 200ms
  fetchOrders()           // 250ms
]);
// 총 대기: 약 300ms (가장 오래 걸리는 작업 기준)

위와 같이 최적화를 할 수 있다. 이 경우에는 병렬적으로 처리하기 때문에 가장 오래걸린 작업의 시간 만큼만 소요된다.

궁금한 점, 여러 요청 중 늦어지는 요청이 있을 경우에는 어떻게 되나

학습을 하다보니 궁금한 점이 생겼다. 만약 마이페이지에서 유저 정보와, 유저가 작성한 글 리스트, 유저가 작성한 댓글 리스트들을 렌더링한다고 할 때, 세 요청을 Promise.all로 묶어서 병렬로 요청한다.

여기서 두 요청은 10ms로 매우 빨리 받아지지만, 나머지 하나의 요청이 1000ms가 걸릴 경우, 두 요청에 대한 값으로 렌더링을 먼저 진행하고 늦어지는 값은 나중에 렌더링하도록 할 수 있는건가?

흠

만약 Promise.all만 사용하면 가장 늦어지는 결과 값이 나올 때까지 나머지 값을 사용할 수 없다고 한다. 개별적으로 사용하고 싶을 경우에는 then이나 await을 사용해서 처리해야한다고 한다.

await을 사용하는 코드는 결국에 async를 붙여야하기 때문에 복잡해질 수 있어서 가장 보기 편한 코드는 아래처럼 then을 써서 예약해두는게 좋은 방식인 것 같다. 아래처럼 하면 가장 await을 함수 앞에 붙이지 않았기 때문에 3 요청 모두 병렬적으로(거의) 실행되지만, 값이 받아와지는대로 상태를 변경하고, UI를 렌더링 할 수 있다.

const MyPage = () => {
  const [userInfo, setUserInfo] = useState(null);
  const [userPosts, setUserPosts] = useState(null);
  const [userComments, setUserComments] = useState(null);

  useEffect(() => {
    // 각각 독립적으로 처리
    fetchUserInfo().then(setUserInfo);
    fetchUserPosts().then(setUserPosts);
    fetchUserComments().then(setUserComments);
  }, []);

  return (
    <div>
      {userInfo ? <UserInfo data={userInfo} /> : <Skeleton />}
      {userPosts ? <PostList data={userPosts} /> : <Skeleton />}
      {userComments ? <CommentList data={userComments} /> : <Skeleton />}
    </div>
  );
};

React Query와 SWR에서는..

위와 같은 병렬처리와 우선으로 처리되는 값에 대한 렌더링을 편하게 사용하기 위한 방법을 React-query와 SWR 같은 서버 상태 관리 라이브러리에서는 편리하게 지원한다.

React-query

리액트 쿼리에서는 아래와 같이 useQueries 훅을 지원한다. useQuery를 여러번 써도 블락킹 되지 않아서 병렬적이라고 봐도 된다.

const MyPage = () => {
  const queries = useQueries({
    queries: [
      { queryKey: ['user'], queryFn: fetchUserInfo },
      { queryKey: ['posts'], queryFn: fetchUserPosts },
      { queryKey: ['comments'], queryFn: fetchUserComments }
    ]
  });

  const [userQuery, postsQuery, commentsQuery] = queries;

  return (
    <div>
      {userQuery.isLoading ? <Skeleton /> : <UserInfo data={userQuery.data} />}
      {postsQuery.isLoading ? <Skeleton /> : <PostList data={postsQuery.data} />}
      {commentsQuery.isLoading ? <Skeleton /> : <CommentList data={commentsQuery.data} />}
    </div>
  );
};

지금하고 있는 프로젝트에서 한번 적용해봐야겠다.

Table of Contents

0
추천 글