Prefetching & QueryClient: 캐시를 직접 다루기

React Query의 모든 캐시는 QueryClient가 관리합니다. QueryClient를 직접 다루면 컴포넌트 외부에서도 캐시를 읽고 쓰거나, 데이터를 미리 가져올 수 있습니다.

QueryClient 가져오기

import { useQueryClient } from '@tanstack/react-query';
 
function MyComponent() {
  const queryClient = useQueryClient();
  // 이제 queryClient로 캐시를 직접 조작할 수 있음
}

Prefetching: 데이터를 미리 가져오기

사용자가 어떤 동작을 하기 전에 미리 데이터를 가져와 캐시에 저장해두는 것입니다. 실제로 그 데이터가 필요해지는 순간 이미 준비되어 있어 즉각 반응합니다.

마우스 호버 시 미리 가져오기

function TodoList() {
  const queryClient = useQueryClient();
  const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
 
  function prefetchTodoDetail(id) {
    queryClient.prefetchQuery({
      queryKey: ['todo', id],
      queryFn: () => fetchTodo(id),
      staleTime: 1000 * 60, // 1분간 fresh
    });
  }
 
  return (
    <ul>
      {todos?.map(todo => (
        <li
          key={todo.id}
          onMouseEnter={() => prefetchTodoDetail(todo.id)} // 마우스 올리면 미리 fetch
        >
          <Link to={`/todos/${todo.id}`}>{todo.title}</Link>
        </li>
      ))}
    </ul>
  );
}

링크에 마우스를 올리는 순간 상세 데이터를 미리 가져옵니다. 링크를 클릭하면 이미 캐시에 있으므로 즉시 보여집니다.

라우트 전환 전 미리 가져오기

function UserListPage() {
  const queryClient = useQueryClient();
  const navigate = useNavigate();
 
  async function handleUserClick(userId) {
    // 페이지 이동 전에 미리 가져오기
    await queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => fetchUser(userId),
    });
    navigate(`/users/${userId}`);
  }
 
  return (
    <ul>
      {users.map(user => (
        <li key={user.id} onClick={() => handleUserClick(user.id)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

서버 사이드에서 미리 가져오기 (Next.js)

// Next.js 페이지 컴포넌트
export async function getStaticProps() {
  const queryClient = new QueryClient();
 
  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  });
 
  return {
    props: {
      dehydratedState: dehydrate(queryClient), // 캐시를 직렬화해서 전달
    },
  };
}
 
function PostsPage() {
  // 서버에서 prefetch된 데이터가 이미 캐시에 있음
  const { data } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  // 클라이언트에서 로딩 없이 즉시 표시
  return <PostList posts={data} />;
}

QueryClient 주요 메서드

getQueryData / setQueryData

// 캐시에서 데이터 읽기
const todos = queryClient.getQueryData(['todos']);
 
// 캐시에 데이터 직접 쓰기
queryClient.setQueryData(['todos'], newTodos);
 
// 함수형 업데이트
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

getQueryState: 쿼리 메타정보 확인

const state = queryClient.getQueryState(['todos']);
// state.dataUpdatedAt: 마지막으로 데이터가 갱신된 시간
// state.status: 'pending' | 'error' | 'success'
// state.fetchStatus: 'fetching' | 'idle'
 
// 마지막 업데이트 이후 얼마나 됐는지 확인
const lastUpdated = Date.now() - state.dataUpdatedAt;
if (lastUpdated > 1000 * 60 * 5) {
  // 5분 이상 지났으면 재fetch
  queryClient.invalidateQueries({ queryKey: ['todos'] });
}

fetchQuery: 컴포넌트 외부에서 fetch

// 이벤트 핸들러, 유틸 함수 등 컴포넌트 밖에서 데이터 가져오기
async function loadUserData(userId) {
  const user = await queryClient.fetchQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
  return user;
}

prefetchQuery와 달리 결과를 반환합니다. 이미 캐시에 있으면 캐시를 반환하고, 없으면 fetch합니다.

QueryClient 전역 설정

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,           // 기본 1분
      gcTime: 1000 * 60 * 10,         // 기본 10분
      retry: 1,                        // 기본 재시도 1회
      refetchOnWindowFocus: true,
    },
    mutations: {
      retry: 0, // 뮤테이션은 기본적으로 재시도 안 함
    },
  },
  queryCache: new QueryCache({
    // 모든 쿼리 에러를 한 곳에서 처리
    onError: (error, query) => {
      if (error.status === 401) {
        navigate('/login');
      }
    },
  }),
  mutationCache: new MutationCache({
    // 모든 뮤테이션 성공을 한 곳에서 처리
    onSuccess: () => {
      toast.success('저장됐습니다.');
    },
    onError: (error) => {
      toast.error(`오류: ${error.message}`);
    },
  }),
});

QueryCacheMutationCache의 전역 콜백은 개별 쿼리/뮤테이션 콜백과 함께 동작합니다. 공통 에러 처리(토스트, 로그, 리다이렉트)를 한 곳에서 관리할 수 있습니다.

React Query DevTools

개발 중에 캐시 상태를 시각적으로 확인할 수 있습니다.

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

DevTools에서 확인할 수 있는 것:

  • 현재 캐시에 있는 모든 쿼리와 상태
  • 각 쿼리의 데이터, stale 여부, 마지막 업데이트 시간
  • 특정 쿼리를 수동으로 무효화/재fetch
  • 쿼리 데이터를 직접 수정

정리

기능메서드사용 시점
미리 가져오기prefetchQuery마우스 호버, 라우트 전환 전
캐시 읽기getQueryData캐시 값 확인이 필요할 때
캐시 쓰기setQueryData낙관적 업데이트, 서버 응답으로 즉시 반영
강제 fetchfetchQuery컴포넌트 외부에서 데이터 필요 시
전역 설정QueryClient 생성 시앱 전체 기본 동작 설정

prefetchQuery로 사용자 행동을 예측하고 미리 데이터를 준비하면, 페이지 전환이나 데이터 표시가 즉각적으로 이루어져 UX를 크게 개선할 수 있습니다.