수량을 누르면 총 결제금액이 오르는 것까지 구현했다. 근데 수량 버튼을 눌렀을 때 좀 느리게 브라우저에 반영되어서 optimsitic update로 구현해보려고한다. 

 

[프로젝트] Next.js 장바구니 구현하기 - (2)

장바구니에 담은 상품 수량 추가/감소 기능 구현하던 중 추가하면 아래 결제 금액도 변경이 되어야하는데 변경이 되지 않고 새로고침해야 변경이 되었다.. useState로 화면에 바로 렌더링되도록

ejunyang.tistory.com

 

 

 

Optimistic update(낙관적 업데이트)

말 그대로 낙관적이다. 서버에 요청을 보내고 됐겠지~~~하고 화면에 적용해버리는 것. 서버 요청이 정상임을 가정하고 더 나은 UX를 제공할 수 있다. 예시로 좋아요 버튼을 구현할 때 많이 사용한다. 시간이 걸린다는 단점을 해결하기 위해 고려된 방법인 만큼 네트워크가 느린 환경에서 유용하다는 장점이 있으나, 서버와의 데이터가 다를 수 있다는 단점이 있으므로 이를 해결하기 위해 서버와 클라이언트 간에 상태가 같은지 최종 확인 과정을 거쳐야만 한다.

 

 

Optimistic Updates | TanStack Query React Docs

React Query provides two ways to optimistically update your UI before a mutation has completed. You can either use the onMutate option to update your cache directly, or leverage the returned variables to update your UI from the useMutation result. Via the

tanstack.com

 


 

CountButton.tsx

원래 코드는 아래와 같다. useMutation으로 데이터 업데이트를 해주었고 업데이트에 성공할 경우 invalidateQueries를 사용해 무효화해서 화면해 새로운 데이터를 가져와서 보여주었다. 서버에 요청을 보내고 받아와서 화면에 반영하는데까지 오래걸려서 낙관적 업데이트 기술을 사용해 바로바로 수량이 올라가고 결제 금액이 반영될 수 있도록 코드를 수정해보았다.

const addCountMutation = useMutation({
    mutationFn: async () => {
      const newCount = counts + 1;
      await updateCountInDatabase(newCount);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['cart']
      });
    }
  });

 

 

 

 

낙관적 업데이트 코드는 아래와 같다. 기존에 썼던 onSuccess는 더이상 사용하지 않고, onMutate / onError / onSettled 이 조합이 필요하다. 실행순서는 다음과 같다. onMutate ➡️ mutationFn ➡️ onError ➡️ onSettled 순이다.

 

1. onMutate

이때 cart 쿼리 키로 데이터를 가지고 오는 중이라면, 기존 쿼리를 일단 취소해야한다. 왜냐하면 사이드 이펙트를 일으킬 수 있기 때문. 

getQueryData로 가져온 데이터를 백업 해두고 setQueryData로 변경될 것으로 예측한 데이터를 넣어주면 된다. 

onMutate: async () => {
      await queryClient.cancelQueries({
        queryKey: ['cart']
      }); // 기존 쿼리 취소

      const previousCart = queryClient.getQueryData(['cart']); // 기존 데이터 저장

      // optimistic update
      queryClient.setQueryData(['cart'], (oldData: any) => {
        return oldData.map((item: any) =>
          item.product_id === product_id ? { ...item, count: counts + 1 } : item
        );
      });

      return { previousCart }; // 롤백을 위한 이전 데이터 반환
    },

 

2. mutationFn

실제로 서버에 요청하는 함수를 실행한다. 

  mutationFn: async () => {
      const newCount = counts + 1;
      await updateCountInDatabase(newCount);
      return newCount;
    },

 

 

3. onError

위 코드에서 리턴된 previousCart는 아래 context로 들어가게 된다. 만약 서버에 요청을 보내고 오류가 날 경우 전에 백업해둔 데이터로 원복을 해준다.

onError: (err, variables, context) => {
      queryClient.setQueryData(['cart'], context?.previousCart);
    },

 

 

4. onSettled

onMutate에서 이미 ui가 완성되었지만 한번 더 ui 갱신이 필요하거나, 혹시 모를 상황을 대비해 마지막으로 invalidateQueries로 쿼리를 무효화해준다.

 onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['cart']
      }); // 쿼리 무효화
    }

 

 

완성 코드

코드를 변경하기 전 심화 강의자료와 스탠다드반 리액트 쿼리 강의 자료를 참고해서 구현했다. 꼼꼼한 자료 덕분인지 큰 오류 없이 잘 구현할 수 있었던 것 같다.

const addCountMutation = useMutation({
    mutationFn: async () => {
      const newCount = counts + 1;
      await updateCountInDatabase(newCount);
      return newCount;
    },
    onMutate: async () => {
      await queryClient.cancelQueries({
        queryKey: ['cart']
      }); // 기존 쿼리 취소

      const previousCart = queryClient.getQueryData(['cart']); // 기존 데이터 저장

      // optimistic update
      queryClient.setQueryData(['cart'], (oldData: any) => {
        return oldData.map((item: any) =>
          item.product_id === product_id ? { ...item, count: counts + 1 } : item
        );
      });

      return { previousCart }; // 롤백을 위한 이전 데이터 반환
    },
    onError: (err, variables, context) => {
      queryClient.setQueryData(['cart'], context?.previousCart);
    },
    onSettled: () => {
      queryClient.invalidateQueries({
        queryKey: ['cart']
      }); // 쿼리 무효화
    }
  });

 

 

프로젝트 적용

테스트하느라 37번이나 눌러봤지만.. 잘 업데이트되는 것을 확인할 수 있다.

 

+ Recent posts