[프로젝트] Next.js 장바구니 구현하기 - (4)
가현님 덕에 최고심에 빠져버렸다.. 채김져.. 미루고 미루던 삭제 구현하기. 그래도 수량 버튼을 useMutation으로 처리하면서 힌트를 얻어서 삭제도 useMutation을 통해 데이터를 업데이트하려고 한다.
🧺 장바구니 1단계
🧺 장바구니 2단계
🧺 장바구니 3단계
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
기존에 있었던 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 }; // 롤백을 위한 이전 데이터 반환
},
프로젝트 적용