서버 데이터 변경(mutation) → 기존 캐시 무효화 → 최신 데이터 재요청(query 재실행)
서버 상태와 React Query의 역할
React Query가 관리하는 건 UI 상태가 아니라 서버 상태입니다.
서버 상태의 특징
- 서버가 진짜 진실(Single Source of Truth)
- 클라이언트가 마음대로 바꾸면 안 됨
- 언제든 다른 사용자/요인으로 바뀔 수 있음
그래서 React Query는 이렇게 동작합니다.
- 내가 아는 데이터는 캐시일 뿐이고, 틀릴 수 있다
updateAppointmentStatus: 서버 변경
export async function updateAppointmentStatus(input: {
id: string;
status: AppointmentStatus;
}) {
try {
const appointment = await prisma.appointment.update({
where: { id: input.id },
data: { status: input.status },
});
return appointment;
} catch (error) {
console.error("Error updating appointment:", error);
throw new Error("Failed to update appointment");
}
}
- 여기서 실제로 일어나는 일은 단 하나입니다.
- DB의 appointment 상태가 변경됨
중요한 점은 이 시점에서 프론트 캐시는 아무 변화가 없습니다
useMutation은 "실행 트리거"다
export function useUpdateAppointmentStatus() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateAppointmentStatus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["getAppointments"] });
},
onError: (error) => console.error("Failed to update appointment:", error),
});
}
useMutation의 역할
- 서버에 변경 요청을 보내는 버튼
- 자동으로 캐시를 바꾸지 않음
- mutation은 캐시를 갱신하지 않는다
- mutation은 "서버가 바뀌었다"는 사실만 만든다
왜 invalidateQueries를 써야 할까?
invalidateQueries가 하는 일
queryClient.invalidateQueries({ queryKey: ["getAppointments"] });
- 이 코드는 이렇게 번역할 수 있습니다.
- "getAppointments 캐시는 이제 믿을 수 없으니 무효 처리해라"
invalidate = 삭제
invalidate = "다음에 필요하면 다시 가져와"
invalidate 이후 실제로 일어나는 흐름
- appointment 상태 변경 (mutation 성공)
- getAppointments 캐시 → stale 상태
- 화면 어딘가에서 useQuery(['getAppointments']) 사용 중
- React Query 판단 "이 쿼리는 stale이네? 다시 fetch 해야겠다"
- 서버에서 최신 데이터 재요청
- UI 자동 갱신
invalidateQueries를 안 쓰면 생기는 문제
상황
- 서버에서는 상태가 바뀜
- 캐시는 이전 데이터 유지
결과
- 화면은 안 바뀐 것처럼 보임
- 새로고침해야만 반영됨
왜 그냥 setQueryData 안 쓰나요?
queryClient.setQueryData(['getAppointments'], ...)
setQueryData 단점
- 서버 응답 구조를 정확히 알아야 함
- 로직이 복잡해짐
- 실수하면 서버 상태와 불일치
invalidateQueries 장점
- 서버를 다시 신뢰
- 로직 단순
- 유지보수 안정적
그래서 기본 전략은 invalidate입니다.
invalidateQueries 할 때 쿼리 인풋은 안 넣어도 되나요?
결론부터 말하면 "항상 안 넣어도 되는 건 아니고, 쿼리 키를 어떻게 설계했느냐에 따라 다릅니다."
쿼리 키에 인풋이 없는 경우 (고정 키)
useQuery({
queryKey: ['getAppointments'],
queryFn: fetchAppointments,
});
- 이런 구조라면 invalidate도 동일하게 인풋 없이 써도 됩니다.
queryClient.invalidateQueries({ queryKey: ['getAppointments'] });
- 이 경우 캐시 대상이 하나뿐이라 모호함이 없습니다.
쿼리 키에 인풋이 포함된 경우 (필터, 페이지, 조건)
useQuery({
queryKey: ['getAppointments', { status, page }],
queryFn: fetchAppointments,
});
- 이 경우 invalidate 전략이 두 가지로 나뉩니다.
특정 조건만 무효화
queryClient.invalidateQueries({
queryKey: ['getAppointments', { status, page }],
});
- 정확하지만
- 조건이 많아질수록 관리가 어려움
prefix 기준으로 전체 무효화 (실무에서 가장 많이 사용)
queryClient.invalidateQueries({ queryKey: ['getAppointments'] });
React Query는 queryKey를 prefix 매칭으로 처리하기 때문에,
['getAppointments']는 아래 쿼리들을 전부 stale 처리합니다.
- ['getAppointments', { status: 'CONFIRMED' }]
- ['getAppointments', { status: 'COMPLETED', page: 2 }]
mutation 결과로 캐시를 직접 업데이트하면 더 효율적이지 않나요?
쿼리 비용만 보면 mutation 결과를 활용해서 캐시를 직접 수정하는 편이 더 효율적일 수 있습니다.
mutation 결과로 캐시를 직접 갱신하는 방식
return useMutation({
mutationFn: updateAppointmentStatus,
onSuccess: (updated) => {
queryClient.setQueryData(['getAppointments'], (old: any[]) => {
if (!old) return old;
return old.map((apt) =>
apt.id === updated.id
? { ...apt, status: updated.status }
: apt
);
});
},
});
장점
- 네트워크 재요청 없음 → 성능/비용 절약
- UI 즉시 반영 → 체감 UX 좋음
단점
- 필터/페이지/정렬 캐시가 여러 개면 전부 수동 업데이트 필요
- 서버에서 같이 바뀌는 값(updatedAt, 통계, 연관 데이터 등)을 놓치기 쉬움
- 캐시와 서버 상태 불일치 위험
'개발 공부 > React' 카테고리의 다른 글
| Next.js API Route (0) | 2026.01.11 |
|---|---|
| Clerk PricingTable (0) | 2025.12.25 |
| 탭 간 상태 동기화 - React (0) | 2025.12.21 |
| Clerk 로그인/회원가입 (0) | 2025.11.29 |
| UploadThing을 통한 이미지 업로드 구현하기 (0) | 2025.11.27 |