React Query와 React Router Loaders 사용 방법 (Pre-fetch & Cache Data)
Source: Dev.to
문제
페이지로 이동하면 보통 데이터가 가져오는 동안 지연이 발생합니다. 사용자는 로딩 스피너를 보게 되고, 요청이 끝난 후에 내용이 나타납니다. 별로 좋지 않죠.
페이지가 로드될 때 데이터가 이미 존재한다면 어떨까요?
React Query와 React 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가 캐싱, 백그라운드 재조회 및 오래된 데이터를 자동으로 처리합니다.