
옵티미스틱 업데이트
- 이전 상태를 기억할 수 있어야 합니다.
- 서버의 응답을 예상할 수 있어야 합니다.
- 경우에 따라 상태를 적절하게 업데이트할 수 있어야 합니다.
(onMutate, onError, onSettled)
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { fetcher, QueryKeys } from '@/queryClient';
import { ProductType } from '@/shared/types/data.type';
const useLike = () => {
const queryClient = useQueryClient();
// 1️⃣ 좋아요 기능을 담당하는 함수(likeProduct) 생성
const { mutate: likeProduct } = useMutation(
// 🚨 실제로 좋아요를 담당하는 API를 없어서 동작하지 않았습니다.
(productId: number) =>
fetcher({ method: 'POST', path: `/products/${productId}/like` }),
{
// ✅ cancelQueries를 통해 QueryKeys.PRODUCTS 키를 가진 모든 쿼리를 취소하고
// 이를 통해 잠재적인 데이터 충돌을 방지
// ✅ getQueryData를 통해 QueryKeys.PRODUCTS 키를 가진 쿼리의 현재 캐시된 데이터를 가져와 previousProducts 변수에 저장하고
// 이를 통해 나중에 오류가 발생했을 때 이전 상태로 되돌리기 위해 사용
// ✅ setQueryData를 통해 사용자가 "좋아요" 버튼을 클릭했을 때 UI를 즉시 업데이트
onMutate: async (productId: number) => {
await queryClient.cancelQueries({ queryKey: QueryKeys.PRODUCTS });
const previousProducts = queryClient.getQueryData(QueryKeys.PRODUCTS);
queryClient.setQueryData(QueryKeys.PRODUCTS, (old: ProductType[]) => {
return old.map((product: ProductType) =>
product.id === productId ? { ...product, liked: true } : product
);
});
return { previousProducts };
},
// ✅ setQueryData와 위에 있는 previousProducts를 통해 실패 시 이전 데이터로 돌아가도록 변경
onError: (err: Error, context: { previousProducts: ProductType[] }) => {
console.log(err);
queryClient.setQueryData(QueryKeys.PRODUCTS, context.previousProducts);
},
// ✅ invalidateQueries를 통해 QueryKeys.PRODUCTS 키를 가진 쿼리를 무효화하여
// 다음 번에 해당 쿼리가 요청될 때 최신 데이터를 가져오도록 함.
// 이는 낙관적 업데이트가 완료된 후, 서버의 실제 상태와 일치하도록 데이터를 동기화하는 데 사용
onSettled: () => {
queryClient.invalidateQueries({ queryKey: QueryKeys.PRODUCTS });
}
}
);
// 2️⃣ 좋아요 해제 기능을 담당하는 함수(unlikeProduct) 생성
const { mutate: unlikeProduct } = useMutation(
// 🚨 실제로 안좋아요를 담당하는 API를 없어서 동작하지 않았습니다.
(productId: number) =>
fetcher({ method: 'POST', path: `/products/${productId}/unlike` }),
{
// ✅ 위의 내용 참고
onMutate: async (productId: number) => {
await queryClient.cancelQueries({ queryKey: QueryKeys.PRODUCTS });
const previousProducts = queryClient.getQueryData(QueryKeys.PRODUCTS);
queryClient.setQueryData(QueryKeys.PRODUCTS, (old: ProductType[]) => {
return old.map((product: ProductType) =>
product.id === productId ? { ...product, liked: false } : product
);
});
return { previousProducts };
},
// ✅ 위의 내용 참고
onError: (err: Error, context: { previousProducts: ProductType[] }) => {
console.log(err);
queryClient.setQueryData(QueryKeys.PRODUCTS, context.previousProducts);
},
// ✅ 위의 내용 참고
onSettled: () => {
queryClient.invalidateQueries({ queryKey: QueryKeys.PRODUCTS });
}
}
);
return { likeProduct, unlikeProduct };
};
export default useLike;
import { ProductType } from '@/shared/types/data.type';
import { Link } from 'react-router-dom';
import useLike from '@/hooks/useLike';
import { Button } from '../ui/button';
interface ProductItemProps {
product: ProductType;
}
const ProductItem = (props: ProductItemProps) => {
const {
product: { image, price, title, id, liked }
} = props;
const { likeProduct, unlikeProduct } = useLike();
const handleLike = () => {
if (liked) {
unlikeProduct(id);
} else {
likeProduct(id);
}
};
return (
<li className="flex h-[395px] w-[200px] flex-col items-center gap-4 p-4">
<Link to={`/products/${id}`}>
<img
src={image}
alt={title}
className="min-h-[300px] w-full object-contain"
/>
<h3 className="line-clamp-1 font-medium">{title}</h3>
<p className="font-extrabold">{`${price} $`}</p>
</Link>
<Button onClick={handleLike}>
{liked ? '빈 하트 UI' : '채워진 하트 UI'}
</Button>
</li>
);
};
export default ProductItem;
실제 구현 화면이 아니고 이해를 돕기 위한 사진입니다.

cancelQueries는 특정 쿼리의 진행 중인 요청을 취소하는 메서드입니다. 주로 낙관적 업데이트를 수행할 때, 현재 진행 중인 쿼리를 취소하고, 잠재적인 데이터 충돌을 방지하기 위해 사용됩니다.getQueryData는 특정 쿼리 키에 해당하는 캐시된 데이터를 가져오는 메서드입니다. 주로 현재 캐시된 데이터를 읽어와서 낙관적 업데이트를 수행하기 전에 이전 상태를 저장하는 데 사용됩니다.setQueryData는 특정 쿼리 키에 해당하는 캐시된 데이터를 업데이트하는 메서드입니다. 주로 낙관적 업데이트를 수행할 때, 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 데 사용됩니다.invalidateQueries는 특정 쿼리 키에 해당하는 쿼리를 무효화하는 메서드입니다. 무효화된 쿼리는 다음 번에 다시 요청될 때 새로 데이터를 가져오게 됩니다. 주로 서버에서 데이터가 변경된 후, 최신 데이터를 가져오기 위해 사용됩니다.