[React] React.js 실무 강좌 40. React Query의 옵티미스틱 업데이트 — 서버보다 한발 먼저 움직이는 UI
비동기 요청을 처리하다 보면 이런 순간이 자주 있습니다.
좋아요 버튼을 눌렀는데, 반응이 1~2초 늦게 오는 경우요.
사실 요청이 성공했는지 실패했는지는 중요하지 않습니다.
사용자는 그저 ‘눌렀을 때 바로 반응이 오는가’를 체감할 뿐입니다.
이걸 해결하는 핵심 개념이 바로 **옵티미스틱 업데이트(Optimistic Update)**입니다.
React Query를 쓰면 이걸 정말 간단하게 구현할 수 있습니다.
서버의 응답을 기다리지 않고,
UI가 “먼저 결과를 보여주는” 구조입니다.
예전엔 이렇게만 생각했습니다
초반엔 단순히 버튼을 누르면 로딩 스피너를 띄우고,
응답이 오면 상태를 바꾸는 식으로만 코드를 짰습니다.
const handleLike = async () => {
setIsLoading(true);
await axios.post('/api/like', { id });
setLiked(true);
setIsLoading(false);
};
이 코드, 논리적으로는 아무 문제 없습니다.
그런데 사용자가 느끼는 체감 속도는 굉장히 답답합니다.
서버 왕복이 끝나야만 좋아요 아이콘이 채워지니까요.
React Query로 구현하면 이렇게 바뀝니다
React Query의 useMutation은
onMutate, onError, onSettled 같은 훅을 제공합니다.
이걸 이용하면 서버보다 먼저 UI를 바꿨다가,
나중에 결과에 따라 복원하거나 확정할 수 있습니다.
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
function useLikePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id) => axios.post(`/api/posts/${id}/like`),
onMutate: async (id) => {
await queryClient.cancelQueries(['posts']);
const prevData = queryClient.getQueryData(['posts']);
queryClient.setQueryData(['posts'], (old) =>
old.map((post) =>
post.id === id ? { ...post, likes: post.likes + 1 } : post
)
);
return { prevData };
},
onError: (err, id, context) => {
queryClient.setQueryData(['posts'], context.prevData);
},
onSettled: () => {
queryClient.invalidateQueries(['posts']);
},
});
}
핵심은 onMutate 단계입니다.
서버로 요청을 보내기 전에,
React Query의 캐시 데이터를 먼저 수정합니다.
그럼 화면은 즉시 변경된 상태로 렌더링됩니다.
사용자는 “눌렀다 → 바로 반영됐다”는 즉각적인 피드백을 받죠.
그리고 서버 응답이 실패하면,
onError 단계에서 이전 데이터를 복원하면 됩니다.
이걸 잘못 써서 한참 헷갈렸던 시절
처음엔 단순히 setQueryData만 호출하면 될 줄 알았습니다.
그런데 여러 페이지에서 같은 데이터를 공유하고 있을 때
좋아요 수가 일부 화면에서는 갱신되지 않았습니다.
그 이유는 간단했습니다.
React Query는 queryKey 단위로만 캐시를 관리하기 때문이죠.
['post', id]와 ['posts']는 완전히 별개의 캐시입니다.
그래서 이후에는
- 목록 페이지에서는 ['posts']
- 상세 페이지에서는 ['post', id]
이 두 캐시를 모두 수정하거나,
invalidateQueries를 동시에 호출하도록 설계했습니다.
이걸 이해하고 나서야,
서버보다 먼저 반응하는 자연스러운 UI를 만들 수 있었습니다.
실무에서 확실히 체감했던 변화
최근에 작업했던 서비스 중 하나가 게시물 기반 SNS였는데,
좋아요, 북마크, 댓글 등록 같은 액션이 굉장히 자주 발생했습니다.
이때 서버 응답을 기다리지 않고 UI가 즉시 반응하도록
모든 Mutation에 옵티미스틱 업데이트를 붙였습니다.
그 결과 체감 속도가 완전히 달라졌습니다.
“서버가 빠른 게 아니라, 앱이 반응이 빠르다”는 인상을 줬습니다.
실제로 API 응답 속도는 그대로였지만,
사용자는 훨씬 부드럽게 느꼈습니다.
직접 써보면서 느낀 핵심 포인트
옵티미스틱 업데이트는 단순한 속도 최적화가 아닙니다.
사용자가 “서버 응답을 기다리는 중”이라는 사실을 인식하지 못하게 만드는 기술입니다.
다만 주의할 점도 있습니다.
- 서버 검증이 중요한 작업(결제, 승인 등)에는 쓰면 안 됩니다.
- 캐시 키 구조를 잘못 잡으면 일부 화면이 엉뚱하게 업데이트됩니다.
그래서 실무에서는 옵티미스틱 업데이트를 전역적으로 적용하기보다는,
‘피드백이 빨라야 하는 인터랙션’ 위주로 선별적으로 적용했습니다.
좋아요, 팔로우, 즐겨찾기 같은 UI에 딱 어울립니다.
React Query의 옵티미스틱 업데이트는
UI가 사용자보다 뒤처지지 않게 만들어줍니다.
서버는 여전히 느릴 수 있지만,
사용자는 그 사실을 전혀 눈치채지 못합니다.
결국 이 기능을 이해하고 나면
“빠르게 보이는 UI”가 아니라,
**“사용자가 느끼기에 빠른 서비스”**를 만드는 법을 배우게 됩니다.
다음 글에서는 이 흐름을 확장해서
React Query와 WebSocket을 함께 써서, 서버 변경 사항을 실시간으로 반영하는 구조를 다뤄보겠습니다.
이건 옵티미스틱 업데이트의 연장선에 있는, 진짜 ‘실시간 UX’ 이야기입니다.