Query Invalidation & Refetching: 캐시를 최신 상태로 유지하기

서버 데이터가 변경됐을 때 React Query 캐시를 어떻게 최신 상태로 유지할까요? 핵심 도구는 Query Invalidation입니다.

invalidateQueries: 캐시를 구식으로 표시

invalidateQueries는 해당 쿼리 캐시를 stale로 표시하고, 현재 구독 중인 컴포넌트가 있다면 즉시 백그라운드 재fetch를 트리거합니다.

import { useQueryClient } from '@tanstack/react-query';
 
function useCreateTodo() {
  const queryClient = useQueryClient();
 
  return useMutation({
    mutationFn: createTodo,
    onSuccess: () => {
      // 새 할 일이 생겼으므로 목록 캐시를 무효화
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
}

무효화 후:

  • 해당 쿼리를 구독하는 컴포넌트가 있으면 → 즉시 백그라운드 재fetch
  • 구독하는 컴포넌트가 없으면 → stale로만 표시, 다음에 마운트될 때 재fetch

부분 매칭으로 관련 쿼리 한 번에 무효화

invalidateQueries는 key를 접두사(prefix)로 매칭합니다.

// ['todos']로 시작하는 모든 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['todos'] });
 
// 무효화되는 쿼리들:
// ['todos']
// ['todos', 'list']
// ['todos', 'list', { filter: 'active' }]
// ['todos', 'detail', 1]
// ['todos', 'detail', 2]
// 특정 상세만 무효화
queryClient.invalidateQueries({ queryKey: ['todos', 'detail', todoId] });
 
// 목록만 무효화 (상세는 그대로)
queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });

exact: 정확히 일치하는 쿼리만 무효화

// exact: true면 정확히 ['todos']인 쿼리만 무효화
// ['todos', 'list'] 같은 하위 쿼리는 무효화 안 됨
queryClient.invalidateQueries({
  queryKey: ['todos'],
  exact: true,
});

refetchQueries vs invalidateQueries

// invalidateQueries: stale로 표시, 마운트된 쿼리만 재fetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
 
// refetchQueries: 즉시 강제로 재fetch (마운트 여부 상관없음)
queryClient.refetchQueries({ queryKey: ['todos'] });

대부분의 경우 invalidateQueries가 적합합니다. refetchQueries는 “지금 당장 무조건 새로 가져와야 할 때” 사용합니다.

setQueryData: 서버 응답으로 캐시 직접 업데이트

API 응답에 이미 업데이트된 데이터가 포함되어 있다면, 추가 API 호출 없이 캐시를 직접 업데이트할 수 있습니다.

const mutation = useMutation({
  mutationFn: updateTodo,
  onSuccess: (updatedTodo) => {
    // 서버 응답(updatedTodo)으로 캐시 직접 업데이트
    queryClient.setQueryData(
      ['todos', 'detail', updatedTodo.id],
      updatedTodo
    );
    // 목록은 무효화해서 재fetch
    queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
  },
});

이 방식은 상세 쿼리에 대한 추가 API 호출을 줄입니다.

함수형 업데이트: 기존 캐시 기반으로 업데이트

queryClient.setQueryData(['todos', 'list'], (oldData) => {
  if (!oldData) return oldData;
  return oldData.map(todo =>
    todo.id === updatedTodo.id ? updatedTodo : todo
  );
});

removeQueries: 캐시에서 완전히 제거

// 로그아웃 시 사용자 관련 캐시 전부 제거
function handleLogout() {
  queryClient.removeQueries({ queryKey: ['user'] });
  queryClient.removeQueries({ queryKey: ['todos'] });
  logout();
}

cancelQueries: 진행 중인 요청 취소

낙관적 업데이트 시 기존 요청이 완료되어 상태를 덮어쓰는 것을 방지합니다.

const mutation = useMutation({
  mutationFn: updateTodo,
  onMutate: async (newTodo) => {
    // 진행 중인 refetch가 낙관적 업데이트를 덮어쓰지 않도록 취소
    await queryClient.cancelQueries({ queryKey: ['todos'] });
  },
});

실전 패턴: 뮤테이션 후 처리 전략

function useTodoMutations() {
  const queryClient = useQueryClient();
 
  const updateMutation = useMutation({
    mutationFn: updateTodo,
    onSuccess: (updatedTodo) => {
      // 전략 1: 관련 쿼리 전부 무효화 (가장 단순, 추가 API 호출 발생)
      queryClient.invalidateQueries({ queryKey: ['todos'] });
 
      // 전략 2: 상세는 직접 업데이트, 목록만 무효화 (API 호출 최소화)
      queryClient.setQueryData(['todos', 'detail', updatedTodo.id], updatedTodo);
      queryClient.invalidateQueries({ queryKey: ['todos', 'list'] });
 
      // 전략 3: 낙관적 업데이트 (07 아티클에서 자세히)
    },
  });
 
  return { updateMutation };
}

정리

메서드역할
invalidateQueriesstale 표시 + 마운트된 쿼리 재fetch
refetchQueries강제 즉시 재fetch
setQueryData캐시 직접 업데이트
removeQueries캐시 완전 제거
cancelQueries진행 중인 요청 취소

뮤테이션 후 invalidateQueries로 관련 캐시를 무효화하는 것이 가장 기본이자 안전한 패턴입니다. 성능이 중요하다면 setQueryData로 추가 API 호출을 줄이세요.