가현님 덕에 최고심에 빠져버렸다.. 채김져.. 미루고 미루던 삭제 구현하기. 그래도 수량 버튼을 useMutation으로 처리하면서 힌트를 얻어서 삭제도 useMutation을 통해 데이터를 업데이트하려고 한다. 

 

🧺 장바구니 1단계

 

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

이번주 월요일부터 개발작업에 들어갔다. 와이어프레임이 거의 나와서(혜원님의 열일) 개발을 바로 시작할 수 있었다. 내가 맡은 기능은 아래와 같다. 우선 월요일에 특산물 전체보드와 상세페

ejunyang.tistory.com

🧺 장바구니 2단계

 

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

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

ejunyang.tistory.com

🧺 장바구니 3단계

 

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

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

ejunyang.tistory.com

 


 

Data-table-column-header.tsx

원래는 아래와 같은 로직으로 진행했다. 하지만 새로고침해야 삭제가 되었다.

Data-table을 임포트하고있는 CartList 컴포넌트에서 이미 카트 데이터를 useQuery로 관리하고 있기 때문에 업데이트는 모두 useMutation을 사용해주기로했다.

const deleteProduct = async (productId: string) => {
  const { data, error } = await supabase
    .from('cart')
    .delete()
    .eq('product_id', productId);
  if (error) {
    console.error('상품을 삭제하는데 실패했습니다.', error);
  } else {
    return data;
  }
};

 

 

 

이전에 카운트 버튼을 참고해서 구현했다. useDeleteProduct 훅을 생성하고 아래 쿼리문을 썼다. 원래는 함수로 만들었는데 빌드 테스트할때 에러가 났다. useQueryClient 훅이 deleteProduct 함수 안에서 호출되는 것은 React 훅 규칙에 위배된다는 에러였고, 훅은 반드시 React 함수 컴포넌트 또는 사용자 정의 훅 내에서만 호출해야한다는 오류였다.

Error: React Hook "useQueryClient" is called in function "deleteProduct" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use". react-hooks/rules-of-hooks

 

 

그래서 따로 useDeleteProduct 훅 파일을 만들어 빼놓고 임포트 시켰다.

const useDeleteProduct = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (productId: string) => {
      const { error } = await supabase
        .from('cart')
        .delete()
        .eq('product_id', productId);

      if (error) {
        throw new Error('상품을 삭제하지 못했습니다.' + error.message);
      }
    },
    onSuccess: () => {
       queryClient.invalidateQueries({
         queryKey: ['cart']
       });
     }
 })

 

 

데이터는 잘 삭제되지만 이것도 수량을 올릴때 결제 금액이 천천히 올랐던 것처럼 느리게 반영되는 이슈 발생.. 이것도 낙관적 업데이트를 해줘야하나보다.. useMutation으로 서버의 데이터를 수정할 땐 꼭 optimistic update를 해줘야할 것 같다.

 {
    id: 'delete',
    header: '',
    cell: ({ row }) => {
      const mutation = useDeleteProduct();

      return (
        <button onClick={() => mutation.mutate(row.getValue('product_id'))}>
          <CgClose className="text-[#959595]" />
        </button>
      );
    }
  }

 

여기서 또 나타난 빌드에러..☠️  columns가 배열로 되어있어서 const mutation = useDeleteProduct(); 을 컬럼 배열 안에 넣는 것은 훅 규칙에 위배된다는 말이었다. 훅은 반드시 리액트 컴포넌트 안에 호출해야한다고 한다.

Error: React Hook "useDeleteProduct" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function. react-hooks/rules-of-hooks

 

 

그래서 삭제버튼 컴포넌트를 만들어서 컬럼 배열에 추가해주었다.

const DeleteButton = ({ productId }: { productId: string }) => {
  const mutation = useDeleteProduct();

  const handleDelete = () => {
    if (confirm('해당 제품을 삭제하시겠습니까?')) {
      mutation.mutate(productId);
    }
  };

  return (
    <button onClick={handleDelete}>
      <CgClose className="text-[#959595]" />
    </button>
  );
};
{
    id: 'delete',
    header: '',
    cell: ({ row }) => {
      return <DeleteButton productId={row.getValue('product_id')} />;
    }
  }

 

 

Optimisitic Update

 

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

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

ejunyang.tistory.com

 

기존에 있었던 onSuccess는 지우고 onMutate, onError, onSettled 조합을 추가했다. 이미 만들어 놓은 카운터 컴포넌트에서 그대로 가져와서 사용했고, 삭제하는 로직만 변경했다.

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

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

      // optimistic update
      queryClient.setQueryData(['cart'], (oldData: CartItem[] = []) => {
        return oldData.filter((item) => item.product_id !== productId);
      });

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

 

 

 

 

프로젝트 적용

 

 

수량을 누르면 총 결제금액이 오르는 것까지 구현했다. 근데 수량 버튼을 눌렀을 때 좀 느리게 브라우저에 반영되어서 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