Server Component를 쓰면 React Query는 필요없을까
2025년 3월 20일
App Router에서 Server Component는 async/await로 데이터를 직접 가져올 수 있다. 그러면 React Query가 필요 없는 걸까?
Server Component에서 직접 fetch로 충분한 경우
페이지 진입 시 한 번 가져오고, 이후 갱신이 필요 없는 데이터라면 Server Component에서 직접 fetch하는 게 맞다.
async function PostPage({ params }) {
const post = await fetch(`/api/posts/${params.id}`).then((r) => r.json());
return <PostDetail post={post} />;
}
클라이언트 번들에 React Query가 포함되지 않고, 로딩 상태 관리도 필요 없다.
React Query가 필요한 경우
데이터가 클라이언트 상호작용에 따라 바뀌어야 하거나, 백그라운드에서 주기적으로 갱신되어야 할 때는 React Query가 필요하다.
- 사용자 액션으로 데이터를 다시 가져와야 할 때 (
refetch) - 탭 포커스 시 자동으로 최신 데이터를 가져오고 싶을 때
- 여러 컴포넌트가 같은 데이터를 공유하고 캐시를 유지해야 할 때
mutation이후 관련 쿼리를 무효화(invalidate)해야 할 때
Server Component의 fetch는 이런 클라이언트 사이드 캐싱과 갱신 전략을 다루지 못한다.
Prefetch로 캐시 채우기
Server Component에서 데이터를 미리 가져와 클라이언트에 전달하면 초기 로딩 없이 React Query 캐시를 채울 수 있다.
// app/posts/page.tsx — Server Component
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from "@tanstack/react-query";
import { PostList } from "./PostList";
async function PostsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ["posts"],
queryFn: () => fetch("/api/posts").then((r) => r.json()),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostList />
</HydrationBoundary>
);
}
// PostList.tsx — Client Component
"use client";
import { useQuery } from "@tanstack/react-query";
function PostList() {
const { data: posts } = useQuery({
queryKey: ["posts"],
queryFn: () => fetch("/api/posts").then((r) => r.json()),
});
return (
<ul>
{posts?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
서버에서 prefetch한 데이터가 HydrationBoundary를 통해 클라이언트 캐시로 전달된다. PostList는 마운트 시점에 이미 캐시에 데이터가 있기 때문에 로딩 상태 없이 바로 렌더링된다. 이후 포커스나 인터벌에 의한 refetch는 React Query가 처리한다.