[react-query]옵티미스틱업데이트 (feat.좋아요 기능)

해달·2022년 8월 28일
7

서두

sns나 이커머스에서 많이 사용되는 좋아요 기능!
이 기능을 구현하면서 optimistic update 라는걸 처음 알게 되었다.
서버 상태관리로 react-query를 사용하고 있기에 리액트쿼리 라이브러리를 이용해 어떻게 구현하는지와 옵티미스틱 업데이트가 왜 필요한지에 대해 알아보았다.


optimistic update (낙관적 업데이트)

사용자 인터페이스에서 발생하는 작업을 즉시 반영하여 사용자에게 빠른 피드백을 제공하는 기술이다.
사용자 동작을 먼저 반영하고, 통신이 완료되지 않은 상태에서도 해당 변경사항을 즉시 화면에 업데이트해준다.

🤔 왜 필요할까 ?

좋아요 버튼을 클릭하면 일반적으로 서버와의 통신을 통해 데이터를 업데이트하고 응답을 기다린다.

좋아요 기능같은 경우에는 유저가 좋아요를 눌렀다고 인지할 수 있는 빠른 인터랙션이 필요하기때문에,
실제로 서버와 데이터를 주고 받기전에 서버와의 통신을 기다리지 않고 클라이언트 측에서 데이터를 업데이트를 해주어

유저에게 자신의 행위에 대한 빠른 피드백을 제공하고 응답을 기다리는 동안의 대기 시간을 줄여 줄 수 있다.

유저가 좋아요를 눌렀을 때 인지되는 UI가 느리게 반영된다면 유저는 자기가 한 행위가 제대로 이루어졌는지 확인이 어려울 것이다.

🚨 주의해야 할 사항

유저에게 좋은 경험을 주기위해 클라이언트에서 데이터를 미리 업데이트해주는 것이기 때문에,
서버와의 통신에서 실패하게 될 경우 이전 데이터로 롤백하는 에러핸들링을 추가로 처리해놓아야 한다.
위 핸들링은 onError를 통해 구현해놓을 수 있다.


구현

적용하기 전.GIF

좋아요를 눌렀음에도 서버에서 응답을 기다려 데이터를 업데이트하기때문에 반응속도가 느린것을 확인할 수 있다.

위 사항을 개선해보자!

1. 좋아요 기능을 하는 함수 만들기

const addMyItem = async ({ itemId }: ReqParmasType) => { 
// 서버와 통신하여 데이터를 업데이트하는 기능
};

2. 후처리 핸들링으로 옵티미스틱 업데이트 처리해주기

  1. 업데이트 함수를 뮤테이션에 전달해주고
    onMutate에서 뮤테이션이 실행되기 전에 실행되는 함수로,
    미리 동작 될 행동을 적어준다
    (데이터를 클라이언트에서 미리 처리해주는작업!)

  2. queryClient.cancelQueries([queryKeys.myItems])
    queryCliecnt에서 cancleQueries
    쿼리 요청을 무효화 시킬 쿼리키를 전달해주어 혹여 다른곳에서 전달받은 쿼리키의 데이터가 업데이트 되는걸 캔슬처리해준다.

    • 사용자가 페이지를 떠나거나 새로운 데이터를 요청하는 등의 상황이 발생하면 해당 쿼리는 취소되어 불필요한 데이터 요청을 중단시킨다.
  3. queryClient.getQueryData([queryKeys.myItems])
    사용하여 이전 myItems 데이터를 가져온다.
    이전 데이터를 가져오는 이유는, 낙관적 업데이트를 수행하기 전에 현재 상태를 기억해야 하기 때문이다.

  4. if(이전 데이터가 존재할 경우) queryClient.setQueryData
    위 메서드를 이용해 쿼리의 데이터를 업데이트한다.
    변경해줄 데이터는 가져온 이전데이터에 클릭한 Item의like 값을 true로 변경해놓은 데이터다.

  5. 변경 된 데이터를 return 시켜 훅을 사용한 컴포넌트에서는 가공된 데이터를 전달받아 유저에게 미리 보여준다.

return useMutation(addMyItem, {
    onMutate: async ({ ItemId }) => {
      await queryClient.cancelQueries([queryKeys.myItems])
      const previousMyItems = queryClient.getQueryData([queryKeys.myItems]);

      if (previousMyItems) {
        queryClient.setQueryData(
          [queryKeys.MyItems],
          previousMyItems.map((item) =>
            item.id === itemId ? { ...item, like: true } : item
          )
        );
      }

      return {
        previousMyItems,
      };
    },

만약 서버에서 데이터를 전달받지 못해 에러가 발생했다면
context에서 이전 데이터를 가져와 다시 업데이트를 해주어야 한다.
context에 들어있는 previousMyItemsonMutate에서 반환 된 값이다.

    onError: (err, brandId, context) => {
      queryClient.setQueryData([queryKeys.myItems], context?.previousMyItems);

요청이 실패 또는 성공했을 경우 업데이트 할 쿼리키를 invalid 작업을 해주어 리패치 해주도록 한다.

    onSettled: () => {
      queryClient.invalidateQueries([queryKeys.myItems]);
    },
  });
};

전체코드

return useMutation(addMyItem, {
    onMutate: async ({ ItemId }) => {
      await queryClient.cancelQueries([queryKeys.myItems])


      const previousMyItems = queryClient.getQueryData([queryKeys.myItems]);

      if (previousMyItems) {
        queryClient.setQueryData(
          [queryKeys.MyItems],
          previousMyItems.map((item) =>
            item.id === itemId ? { ...item, like: true } : item
          )
        );
      }

      return {
        previousMyItems,
      };
    },

    onError: (err, brandId, context) => {
      queryClient.setQueryData([queryKeys.myItems], context?.previousMyItems);

    onSettled: () => {
      queryClient.invalidateQueries([queryKeys.myItems]);
    },
  });
};

적용 후

눌렀을 때 좋아요가 적용 된 상태로 아이템이 표시되는 걸 볼 수 있다.

흔히 사용되는 좋아요 기능을 좀더 심리스하게 만들어 볼 수 있었다.


reference

0개의 댓글