Optimistic Update를 적용하면
서버 응답 전에 UI가 먼저 업데이트되어 반응이 즉각적입니다.
하지만 현실적으로는,
등으로 인해 서버 요청이 실패할 수도 있습니다.
이 경우,
이미 UI는 바뀐 상태이므로 원래 상태로 롤백(rollback) 해야 합니다.
React Query의 useMutation은
onError 콜백을 통해 서버 요청 실패 시점을 잡을 수 있습니다.
이를 활용하면 Optimistic Update 후 캐시를 다시 원래 값으로 복구할 수 있습니다.
const { mutate } = useMutation({
mutationFn: toggleLinkPin,
// ✅ 성공 시: 서버 응답으로 최종 확정
onSuccess: (response, { linkId, boxId }) => {
queryClient.setQueryData(["linkDetail", linkId, boxId], (old) =>
old
? { ...old, data: { ...old.data, isPin: response.data.isPin } }
: old
);
},
// ⚠️ 실패 시: 원래 상태로 롤백
onError: (_err, { linkId, boxId, currentPinState }) => {
// 1️⃣ LinkDetail 캐시 롤백
queryClient.setQueryData(["linkDetail", linkId, boxId], (old) =>
old
? { ...old, data: { ...old.data, isPin: currentPinState } }
: old
);
// 2️⃣ List 캐시도 롤백
queryClient.setQueriesData<InfiniteData<ListResponse>>(
{ queryKey: ["list", boxId], exact: false },
(old) =>
old
? {
...old,
pages: old.pages.map((page) => ({
...page,
items: page.items.map((item) =>
item.type === "link" && item.id === linkId
? { ...item, isPin: currentPinState } // 🔙 원상 복구
: item
),
})),
}
: old
);
// 3️⃣ 사용자에게 알림
showError("핀 변경에 실패했습니다");
},
});
currentPinState 전달하기Optimistic Update 시점에는 이미 isPin 상태가 토글된 상태이기 때문에,
실패 시 복구할 원래 값(currentPinState)을 mutate()에 함께 넘겨야 합니다.
mutate({ linkId, boxId, currentPinState });
onError: (_err, variables) => {
const { currentPinState } = variables; // ✅ 최신 값 보장
queryClient.setQueryData(...); // 롤백 처리
}
이렇게 하면,
서버 요청이 실패하더라도 변경 전 상태로 즉시 복구할 수 있습니다.
| 항목 | 설명 |
|---|---|
| Optimistic Update | 서버 응답 전에 UI를 먼저 변경 |
| 문제점 | 요청 실패 시, UI는 이미 변경된 상태 |
| 해결법 | onError에서 currentPinState로 원래 상태 복원 |
| 롤백 대상 | LinkDetail, List(무한스크롤 캐시 모두) |
| UX 보완 | showError() 등으로 사용자에게 알림 |
Optimistic Update는 UX를 빠르게 만들어주지만,
실패 대비 롤백 전략이 없으면 데이터 불일치가 발생할 수 있습니다.
따라서 항상 다음 구조를 유지하세요:
mutate(variables, {
onMutate: optimisticUpdate,
onError: rollbackUpdate,
onSuccess: confirmServerState,
});
낙관적 업데이트는 "즉시 반응하는 UX"와 "데이터 일관성"의 균형이 핵심입니다.