👉 로딩 없이 바로 렌더되는 구조
// queryClient.ts
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient();
// api/fetchPosts.ts
export async function fetchPosts() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts");
if (!res.ok) throw new Error("API 에러");
return res.json();
}
// routes/posts.tsx
import { queryClient } from "../queryClient";
import { fetchPosts } from "../api/fetchPosts";
import { useQuery } from "@tanstack/react-query";
// ✅ loader: 페이지 들어가기 전에 실행
export async function loader() {
await queryClient.ensureQueryData({
queryKey: ["posts"],
queryFn: fetchPosts,
});
return null;
}
// ✅ 컴포넌트
export default function PostsPage() {
const { data, isLoading } = useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
});
if (isLoading) return <div>로딩중...</div>;
return (
<div>
<h1>게시글 리스트</h1>
{data.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
// routes.ts
import { route } from "@react-router/dev/routes";
export default [
route("/", "routes/posts.tsx"),
];
// root.tsx
import {
QueryClientProvider,
HydrationBoundary,
} from "@tanstack/react-query";
import { queryClient } from "./queryClient";
export default function Root({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<HydrationBoundary>
{children}
</HydrationBoundary>
</QueryClientProvider>
);
}
1. "/" 라우트 진입
2. loader 실행
3. queryClient에 posts 데이터 저장됨
4. 컴포넌트 렌더
5. useQuery → 캐시 데이터 즉시 사용
👉 그래서
isLoading 거의 안 뜨고 바로 데이터 보인다
// loader
export async function loader({ request }: any) {
const url = new URL(request.url);
const page = url.searchParams.get("page") ?? "1";
await queryClient.ensureQueryData({
queryKey: ["posts", page],
queryFn: () => fetchPosts(page),
});
return null;
}
// component
const { data } = useQuery({
queryKey: ["posts", page],
queryFn: () => fetchPosts(page),
});
👉 둘을 합치면
빠른 초기 렌더 + 강력한 캐싱
👉 loader = “미리 가져오기”
👉 useQuery = “재사용 + 관리”