모달 내에서 조회 API를 호출하며, 확인 버튼 동작에 invalidateQueries를 넣어두었다. 원래 의도는 새
모달을 열 때마다 새로운 GET API 호출을 보장하는 것이었지만, 실제로는 확인 버튼 직후 곧바로 API가
다시 fetch되는 의도하지 않은 동작이 발생했다. 이 과정을 통해 TanStack Query의 캐시 전략에 대해
자세히 이해하는 과정이 되었다.
결국 invalidateQueries, refetchQueries, removeQueries가 단순히 “다시 불러오기”가 아니라 각기
다른 캐시 동작을 가진다는 걸 알게 되었고, 그 차이를 비교하게 되었다.
invalidateQueries
단순히 “쿼리를 무효화해서 다시 호출한다” 정도로만 이해했는데, 핵심은 stale 표시라는 걸 알게
• 특정 query를 stale(오래됨) 상태로 표시한다.
• 쿼리가 active 상태(컴포넌트에서 사용 중, enabled=true)라면 → 즉시 refetch 발생
• 쿼리가 inactive 상태라면 → refetch는 일어나지 않고, 다음에 active될 때 새 요청 실행
쉽게 말하자면, “이건 오래됐기에 다음에 쓸 때는 새로 가져와라” 라는 태그를 붙이는 것과 같았다.
내가 겪은 사례
백오피스에서 useOverlay 훅을 사용했는데, 이 훅이 unmount되지 않고 계속 구독 상태로 남아
있었다. 이 상황에서 invalidateQueries를 실행하니 즉시 refetch가 일어났다.
• 정상적으로 unmount 되어 있었다면 → refetch는 발생하지 않고, 모달을 다시 열 때 새 호출이
일어났을 것이다.
• 하지만 overlay가 살아 있었기 때문에 active 상태로 간주되어 invalidate 직후 네트워크
요청이 발생했다.
invalidateQueries는 단순 무효화가 아닌, active 여부에 따라 즉시 refetch까지 일어날 수 있다.
추가로, 모달이 닫힌 상태에서도 데이터를 미리 준비해두고 싶다면 prefetchQuery가 더 적합하다는
것이었다. 구독 여부와 상관없이 백그라운드에서 캐시에 데이터를 담아둘 수 있고, 모달이 열리면 즉시
캐시된 값을 보여줄 수 있었다.
refetchQueries
refetchQueries는 훨씬 단순했다.
• stale/fresh 여부와 무관하게 특정 query를 즉시 강제로 네트워크 호출한다.
• 내부적으로는 query.fetch()를 호출하는 것과 유사했다.
쉽게 말하자면, “캐시 상태를 바꾸지 않고, 지금 당장 다시 불러와라” 였다.
invalidateQueries와 가장 큰 차이는 stale 마킹 여부였다.
• invalidateQueries:
"다음에 새로 불러와야 해"라는 태그를 남긴다.
• refetchQueries: 태그 없이 그냥 바로 다시 불러온다.
removeQueries
마지막으로 removeQueries는 성격이 달랐다. 단순히 다시 호출하는 게 아니라, 캐시 엔트리를 완전히
지우고 초기 상태로 되돌렸다.
• 특정 query 캐시를 아예 제거하고, freshness 상태, 구독자 정보 등 모든 메타데이터도
• 동일한 queryKey로 useQuery가 mount되면 → 무조건 새 네트워크 요청이 발생한다.
invalidate “갱신 필요 태그”, refetch “즉시 호출”이라면, remove는 “리셋”에 가까웠다.
쿼리 무효화 전략 변경: invalidateQueries →removeQueries
내가 겪었던 가장 큰 인사이트는, 모달에서 확인 버튼을 누른 뒤 닫히는 상황에서는 invalidateQueries
보다 removeQueries가 더 적합하다는 점이었다.
1. 왜 removeQueries인가?
• invalidateQueries: 쿼리를 stale로 표시하지만 캐시는 남아 있다. → 상황에 따라 즉시
refetch가 안 일어날 수도 있었다.
• removeQueries: 쿼리 캐시를 완전히 제거한다. → 모달이 다시 열리면 항상 새로운 네트워크
요청이 발생했다.
모달이 닫히고 다시 열릴 때 항상 최신 API 데이터를 보장해야 하는 UX라면, removeQueries가 더
명확한 선택이었다.
2. 기존 코드 (invalidateQueries 사용)
const queryClient = useQueryClient();
const postMutation = useMutation({
mutationFn: (data: NoticeFormData) =>
postFunction(scheduleId, { ...data, isEdited: isEditMode }),
onSuccess: () => {
toastSimpleMessage(`${title} 게시 완료했습니다.`, 'success', 3000);
queryClient.invalidateQueries([queryKey, scheduleId]);
onClose();
setIsEditMode(false);
},
onError: (error: Error) => {
toastSimpleMessage(error.message, 'error', 3000);
},
});
이 방식은 “invalidate → stale 표시 → refetch 트리거” 흐름이라, 구독 상태나 unmount 여부에
따라 달라질 수 있었다.
3. 개선 코드 (removeQueries로 변경)
const queryClient = useQueryClient();
const postMutation = useMutation({
mutationFn: (data: NoticeFormData) =>
postFunction(scheduleId, { ...data, isEdited: isEditMode }),
onSuccess: () => {
toastSimpleMessage(`${title} 게시 완료했다.`, 'success', 3000);
queryClient.removeQueries([queryKey, scheduleId]);
onClose();
setIsEditMode(false);
},
onError: (error: Error) => {
toastSimpleMessage(error.message, 'error', 3000);
},
});
이제 모달이 닫힌 후 다시 열리면 useQuery가 새로 mount되면서 무조건 API를 새로 호출했다.캐시에
의존하지 않고 항상 최신 데이터를 보장할 수 있게 되었다.
정리 & 깨달은 점
상황에 맞게 사용하기 위해 단순화하자면,
• 데이터 최신화 → invalidateQueries
• 즉시 새로고침 → refetchQueries
• 항상 새 호출을 강제해야 할 때 → removeQueries
이 경험을 통해, “데이터를 무효화할 것인가, 즉시 다시 불러올 것인가, 아예 리셋할 것인가” 를 명확히
구분하는 것이 안정적인 UX와 올바른 캐시 전략의 핵심이라는 걸 배웠다.
