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가 처리한다.