옵티미스틱 업데이트
- 이전 상태를 기억할 수 있어야 합니다.
- 서버의 응답을 예상할 수 있어야 합니다.
- 경우에 따라 상태를 적절하게 업데이트할 수 있어야 합니다.
(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
는 특정 쿼리 키에 해당하는 쿼리를 무효화하는 메서드입니다. 무효화된 쿼리는 다음 번에 다시 요청될 때 새로 데이터를 가져오게 됩니다. 주로 서버에서 데이터가 변경된 후, 최신 데이터를 가져오기 위해 사용됩니다.