React-query(tanstack-query) 는 서버 상태 관리를 위한 인기있는 라이브러리 입니다.
클라이언트 기반으로만 작동되던 React 에서는 바로 사용하면 됐지만, 최근 Next.js 가 급 부상하며 SSR 방식에는 조금 문제가 되었습니다.
이에 tanstack-query 를 SSR 에서 사용하기 위한 몇가지 방법이 추천되고 있습니다.
초기 로딩 속도 개선: 서버에서 미리 렌더링된 HTML을 받아 빠르게 화면을 표시할 수 있습니다.
SEO 최적화: 검색 엔진 봇이 JavaScript를 실행하지 않고도 컨텐츠를 읽을 수 있습니다.
성능 향상: 클라이언트의 리소스 사용을 줄일 수 있습니다.
사용자 경험 개선: 초기 로딩 시 빈 화면이나 로딩 스피너 대신 컨텐츠를 바로 볼 수 있습니다.
Tanstack Query를 SSR과 함께 사용할 때는 서버에서 데이터를 미리 가져와 클라이언트로 전달하는 과정이 필요합니다.
Next 에서는 client 를 참조하는 코드를 root 에서 사용해서는 안되기 때문에 해당 provider 를 만들어 내려줘야 한다.
'use client';
import {
QueryClient,
QueryClientProvider,
isServer,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// SSR의 경우, 기본 staleTime을 0 이상으로 설정하여 클라이언트에서 즉시 다시 가져오는 것을 방지합니다.
staleTime: 60 * 1000,
},
},
});
}
let browserQueryClient: QueryClient | undefined;
function getQueryClient() {
if (isServer) {
// 서버에서는 항상 새로운 queryClient를 만듭니다.
return makeQueryClient();
}
// 브라우저에서는 queryClient가 없으면 새로운 queryClient를 만듭니다.
// React가 초기 렌더링 중에 일시 정지하면 새로운 클라이언트를 다시 만들지 않도록 매우 중요합니다.
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} client={queryClient} />
{children}
</ReactQueryStreamedHydration>
</QueryClientProvider>
);
}
initialData 옵션을 사용하면 쿼리의 초기 데이터를 직접 제공할 수 있습니다.
서버에서 데이터를 가져옵니다.
가져온 데이터를 props로 컴포넌트에 전달합니다.
클라이언트에서 useQuery 훅의 initialData 옵션에 이 데이터를 전달합니다.
'use client';
import { useQuery } from '@tanstack/react-query';
const fetchTodos = async () => {
const res = await fetch('https://api.example.com/todos');
return res.json();
};
export async function getServerSideProps() {
const todos = await fetchTodos();
return {
props: {
todos,
},
};
}
function Home({ todos }) {
const { data } = useQuery(['todos'], fetchTodos, {
initialData: todos,
});
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
export default Home;
PrefetchedQueryHydrationBoundary는 서버에서 미리 가져온 데이터를 클라이언트에 hydrate하는 방법을 제공합니다.
다음은 실제 프로젝트에서의 사용 예시입니다.
import fetcher from 'api/fetcher';
import { queryKey } from 'api/queryKey';
import PrefetchedQueryHydrationBoundary from 'api/PrefetchedQueryHydrationBoundary';
import UserManagementPage from './clientPage';
const UserManagementPageWithSSR = () => {
return (
<PrefetchedQueryHydrationBoundary
queryList={[
{ queryKey: [queryKey.fetchUsers], queryFn: () => fetcher('users') },
]}
>
<UserManagementPage />
</PrefetchedQueryHydrationBoundary>
);
};
export default UserManagementPageWithSSR;
import {
dehydrate,
HydrationBoundary,
QueryClient,
QueryFunction,
} from '@tanstack/react-query';
type PrefetchedQueryHydrationBoundaryProps<T> = {
children: React.ReactNode;
queryList: { queryKey: string[]; queryFn: QueryFunction<T> }[];
};
export default async function PrefetchedQueryHydrationBoundary<T>({
children,
queryList,
}: PrefetchedQueryHydrationBoundaryProps<T>) {
const queryClient = new QueryClient();
// 모든 쿼리를 병렬로 prefetch함
await Promise.all(
queryList.map(({ queryKey, queryFn }) =>
queryClient.prefetchQuery({
queryKey,
queryFn,
})
)
);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
);
}
장점
서버에서 데이터를 미리 가져와 초기 렌더링 시 데이터를 즉시 사용할 수 있습니다.
여러 쿼리를 한 번에 처리할 수 있어 복잡한 페이지에 적합합니다.
단점
@tanstack/react-query-next-experimental 패키지는 Next.js와 Tanstack Query의 통합을 더욱 간편하게 만듭니다.
특히 useSuspenseQuery 훅을 사용하면 Suspense와 함께 비동기 데이터 로딩을 처리할 수 있습니다.
import { useSuspenseQuery } from '@tanstack/react-query';
import { queryKey } from 'api/queryKey';
import fetcher from '../../fetcher';
import { Robot } from '../types';
export const useFetchRobots = () => {
return useSuspenseQuery<Robot[], Error>({
queryKey: [queryKey.fetchRobots],
queryFn: () => {
return fetcher<Robot[], undefined>('robots');
},
});
};
장점:
Suspense와 함께 사용하여 로딩 상태를 선언적으로 관리할 수 있습니다.
서버 컴포넌트와 클라이언트 컴포넌트 간의 데이터 흐름을 자연스럽게 처리할 수 있습니다.
기본적으로 제시되는 fetch 함수에서는 지원하지 않는 다양한 기능들(캐싱 또는 무한 스크롤) 을 위해서는 tanstack-query 를 제외하고 쓰기에는 아쉽습니다.
3가지의 방식을 통해 SSR 에서도 tanstack-query를 멋지게 써봅시다.
https://tanstack.com/query/v4/docs/framework/react/guides/ssr
https://tanstack.com/query/latest/docs/framework/react/guides/ssr