React Query와 React Router Loaders 사용 방법 (Pre-fetch & Cache Data)

발행: (2026년 2월 26일 오후 09:11 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

문제

페이지로 이동하면 보통 데이터가 가져오는 동안 지연이 발생합니다. 사용자는 로딩 스피너를 보게 되고, 요청이 끝난 후에 내용이 나타납니다. 별로 좋지 않죠.

페이지가 로드될 때 데이터가 이미 존재한다면 어떨까요?
React QueryReact Router loaders를 결합하면 그것이 가능해집니다.

아이디어를 간단히 설명하면

  • Loader는 컴포넌트가 마운트되기 이전에 실행됩니다 (React Router가 네비게이션 시 호출).

  • Loader 안에서 React Query에 “이미 캐시된 데이터가 있나요?” 라고 묻습니다.

    • → 즉시 사용합니다. 네트워크 요청이 없습니다.
    • 아니오 → 지금 데이터를 가져오고, 기다린 뒤 캐시합니다.

컴포넌트가 최종적으로 마운트될 때, 동일한 쿼리와 함께 useQuery를 호출합니다. 데이터가 이미 캐시되어 있기 때문에 즉시 렌더링됩니다 — 로딩 상태가 없습니다.

핵심 메서드는 queryClient.ensureQueryData(queryOptions)입니다. 이것을 “이 데이터가 존재하도록 보장한다 — 캐시에서 가져오거나 없으면 fetch한다.” 라고 생각하면 됩니다.

단계별 예시: 간단한 포켓몬 페이지

1. 쿼리 설정

// pages/Pokemon.jsx
import { useQuery } from '@tanstack/react-query';
import { useLoaderData } from 'react-router-dom';
import axios from 'axios';

// A function that returns the query config (key + fetch function).
// We reuse this in BOTH the loader and the component.
const pokemonQuery = (name) => ({
  queryKey: ['pokemon', name],
  queryFn: async () => {
    const response = await axios.get(
      `https://pokeapi.co/api/v2/pokemon/${name}`
    );
    return response.data;
  },
});

2. 로더 만들기

// The loader receives queryClient from the router setup (see step 4).
// It runs BEFORE the component mounts.
export const loader = (queryClient) => {
  return async ({ params }) => {
    const { name } = params;

    // ensureQueryData checks the cache first:
    //   - cached? → returns it instantly
    //   - not cached? → fetches, caches, and returns it
    await queryClient.ensureQueryData(pokemonQuery(name));

    // We only return the param — the actual data lives in React Query's cache
    return { name };
  };
};

3. 컴포넌트 구축

const Pokemon = () => {
  // Get the param that the loader returned
  const { name } = useLoaderData();

  // useQuery uses the SAME query config as the loader.
  // Since ensureQueryData already cached it, this renders instantly.
  const { data: pokemon } = useQuery(pokemonQuery(name));

  return (
    <>
      <h2>{pokemon.name}</h2>
      <p>Height: {pokemon.height}</p>
      <p>Weight: {pokemon.weight}</p>
    </>
  );
};

export default Pokemon;

4. 라우터에 연결하기

// App.jsx
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

import Pokemon, { loader as pokemonLoader } from './pages/Pokemon';

const queryClient = new QueryClient();

const router = createBrowserRouter([
  {
    path: '/pokemon/:name',
    element: <Pokemon />,
    // Pass queryClient into the loader
    loader: pokemonLoader(queryClient),
  },
]);

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  );
}

export default App;

How It All Flows

User clicks link to /pokemon/pikachu


Router calls loader BEFORE mounting the component


loader calls: await queryClient.ensureQueryData(pokemonQuery("pikachu"))

        ├── Cache HIT?  → 캐시 히트? → 캐시된 데이터를 즉시 반환 (fetch 없음)
        └── Cache MISS? → 캐시 미스? → API에서 가져와 결과를 캐시한 뒤 반환


Component mounts → useQuery(pokemonQuery("pikachu"))


Data is already in cache → Renders IMMEDIATELY (no loading spinner)


데이터가 이미 캐시에 있음 → 즉시 렌더링 (로딩 스피너 없음)

useQuery만 사용하지 않을까?

접근 방식발생하는 일
useQuery만 사용컴포넌트가 마운트 → 데이터를 가져오기 시작 → 로딩 표시 → 데이터 표시
로더에서 ensureQueryData + useQuery마운트 전에 데이터가 가져와짐 → 컴포넌트가 즉시 데이터와 함께 렌더링

로드러 접근 방식은 특히 페이지 이동 시 더 부드럽고 빠른 UX를 제공합니다.

핵심 요약

  • ensureQueryData = “캐시가 있으면 캐시를 사용하고, 없으면 가져와서 캐시합니다.”
  • 공유 쿼리 설정 함수(예: pokemonQuery)를 만들고 로더와 컴포넌트 모두에서 사용합니다.
  • 로더가 캐시를 미리 채워서 컴포넌트의 useQuery가 데이터를 즉시 찾을 수 있습니다.
  • 로더에서는 파라미터만 반환하고, 데이터 자체는 반환하지 않습니다. 데이터는 React Query 캐시에 저장됩니다.
  • 이제 페이지가 탐색 시 즉시 로드되며, React Query가 캐싱, 백그라운드 재조회 및 오래된 데이터를 자동으로 처리합니다.
0 조회
Back to Blog

관련 글

더 보기 »

3단계 반응형 E-commerce 헤더

!Triple-Tier Responsive E-commerce Header의 커버 이미지 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2...