Skill
2026년 01월 04일next.js app router 에서의 tanstack-query (5)
📚 Lesson 5: useMutation & 낙관적 업데이트(Optimistic Update) 요약
1. useMutation이란?
* 용도: 서버의 데이터를 생성, 수정, 삭제(CUD)할 때 사용하는 훅입니다.
* 차이점: useQuery는 컴포넌트 마운트 시 자동 실행되지만, useMutation은 우리가 원할 때
mutate() 함수를 호출하여 수동으로 실행합니다.
2. 낙관적 업데이트 구현 공식 (3단계)
1. `onMutate` (실행 직전):
* 진행 중인 쿼리를 취소합니다(cancelQueries).
* 실패 시 복구할 이전 상태(Snapshot)를 저장합니다.
* setQueryData로 캐시를 즉시 수정합니다.
2. `onError` (실패 시):
* 저장해둔 스냅샷으로 캐시를 원상복구(Rollback)합니다.
3. `onSettled` (성공/실패 공통):
* invalidateQueries를 통해 서버의 최신 데이터와 최종 동기화합니다.
구현
// .../wishlist-button.tsx "use client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toggleWishlistAction } from "@/shared/api/products/actions"; import { Heart } from "lucide-react"; import { cn } from "@/shared/lib/utils"; import { toast } from "sonner"; interface WishlistButtonProps { productId: number; isWished: boolean; queryKey: string[]; // 👈 핵심: 수정할 타겟 쿼리 키를 주입받음 className?: string; } interface Product { id: number; name: string; price: number; isWished: boolean; } export function WishlistButton({ productId, isWished, queryKey, className }: WishlistButtonProps) { const queryClient = useQueryClient(); const { mutate } = useMutation({ mutationFn: () => toggleWishlistAction(productId), // 1️⃣ [낙관적 업데이트] onMutate: async () => { await queryClient.cancelQueries({ queryKey }); const previousProducts = queryClient.getQueryData<Product[]>(queryKey); queryClient.setQueryData<Product[]>(queryKey, (old) => { if (!old) return []; return old.map((p) => p.id === productId ? { ...p, isWished: !isWished } : p ); }); return { previousProducts }; }, // 2️⃣ [에러 시 롤백] onError: (err, newTodo, context) => { if (context?.previousProducts) { queryClient.setQueryData(queryKey, context.previousProducts); } toast.error("찜하기 실패! 다시 시도해주세요."); }, // 3️⃣ [완료 후 최신화] onSettled: () => { queryClient.invalidateQueries({ queryKey }); }, }); return ( <button onClick={(e) => { e.stopPropagation(); mutate(); toast.success(isWished ? "찜 취소 💔" : "찜 완료 ❤️"); }} className={cn( "p-2 rounded-full transition-all hover:bg-red-50 active:scale-95", className )} > <Heart className={cn( "size-6 transition-colors duration-300", isWished ? "fill-red-500 text-red-500" : "text-gray-300 hover:text-red-300" )} /> </button> ); }
💡👨🏫 선생님의 최종 한마디: "데이터를 가져오는 것만큼이나 중요한 것이 '사용자의 액션에 어떻게 반응하느냐'입니다. 낙관적 업데이트를 할 줄 안다는 것은 이제 단순한 코더를 넘어 사용자의 심리를 이해하는 개발자가 되었다는 증거입니다!"