ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ผ๋ฉด ํ ๋ฒ์ฏค์ ์ด๋ฐ ๊ณ ๋ฏผ์ ํด๋ณด์ จ์ ๊ฒ๋๋ค: "๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ์ง?" React Query(Tanstack Query)๋ ์ด๋ฐ ๊ณ ๋ฏผ์ ๋ํ ๊ฐ๋ ฅํ ํด๋ต์ ์ ์ํ์ต๋๋ค. ํ์ง๋ง Next.js์ SSR ํ๊ฒฝ์์๋ ์ฝ๊ฐ์ ๋ง๋ฒ ์ฃผ๋ฌธ์ด ๋ ํ์ํ์ฃ !

์ด ๊ธ์์๋ Next.js์ React Query๋ฅผ ํจ๊ป ์ฌ์ฉํ ๋์ SSR ์ต์ ํ ์ ๋ต์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค. 3๊ฐ์ง ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ๋น๊ตํ์ฌ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ ๊ฐ์ฅ ์ ํฉํ ๋ฐฉ๋ฒ์ ์ฐพ๋ ๋ฐ ๋์์ด ๋๊ธธ ๋ฐ๋๋๋ค.
๋จผ์ , ์ SSR์ด ํ์ํ์ง ์ดํด๋ด ์๋ค:
์ด๊ธฐ ๋ก๋ฉ ์๋ ๊ฐ์ : ์๋ฒ์์ ๋ฏธ๋ฆฌ ๋ ๋๋ง๋ HTML์ ๋ฐ์ ์ฌ์ฉ์์๊ฒ ์ฆ์ ์ปจํ ์ธ ๋ฅผ ๋ณด์ฌ์ค ์ ์์ต๋๋ค.
SEO ์ต์ ํ: ๊ฒ์ ์์ง ๋ด์ด JavaScript๋ฅผ ์คํํ์ง ์๊ณ ๋ ์ปจํ ์ธ ๋ฅผ ์ฝ์ ์ ์์ด ๊ฒ์ ๋ ธ์ถ์ ์ ๋ฆฌํฉ๋๋ค.
์ฑ๋ฅ ํฅ์: ํด๋ผ์ด์ธํธ์ ๋ฆฌ์์ค ์ฌ์ฉ์ ์ค์ฌ ๋ ๋์ ์ฑ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์ : ์ด๊ธฐ ๋ก๋ฉ ์ ๋น ํ๋ฉด์ด๋ ๋ก๋ฉ ์คํผ๋ ๋์ ์ปจํ ์ธ ๋ฅผ ๋ฐ๋ก ๋ณผ ์ ์์ด ์ฌ์ฉ์ ๊ฒฝํ์ด ํฅ์๋ฉ๋๋ค.
Tanstack Query๋ฅผ SSR๊ณผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ด๋ฌํ ์ฅ์ ์ ๋ชจ๋ ํ์ฉํ ์ ์์ต๋๋ค. ๊ทธ๋ผ ์ด๋ป๊ฒ ์ค์ ํด์ผ ํ ๊น์?
Next.js์์๋ ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ๋ฃจํธ ๋ ๋ฒจ์์ ๋ฐ๋ก ์ฌ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์, ๋ณ๋์ 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๋ฅผ ๋ง๋ญ๋๋ค.
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>
);
}
์ด์ Next.js์ ๋ฃจํธ ๋ ์ด์์์์ ์ด Provider๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
React Query์ Next.js๋ฅผ ํจ๊ป ์ฌ์ฉํ ๋ 3๊ฐ์ง ์ฃผ์ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค. ๊ฐ๊ฐ์ ์ฅ๋จ์ ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.

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๋ ๋ ๋ณต์กํ ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ์ ๊ฐ์ง ํ์ด์ง์ ์ ํฉํ ๋ฐฉ๋ฒ์
๋๋ค. ํนํ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํด์ผ ํ ๋ ์ ์ฉํฉ๋๋ค.
๋ค์์ ์ค์ ํ๋ก์ ํธ์์์ ์ฌ์ฉ ์์์ ๋๋ค:
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;
PrefetchedQueryHydrationBoundary ์ปดํฌ๋ํธ์ ๊ตฌํ:
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 ํ
์ ์ฌ์ฉํ๋ฉด React์ 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');
},
});
};
์ฅ์ :
๋จ์ :
๊ฐ ๋ฐฉ๋ฒ์ ์ํฉ์ ๋ฐ๋ผ ์ ํฉํ ์ฌ์ฉ ์ฌ๋ก๊ฐ ์์ต๋๋ค:
initialData: ๊ฐ๋จํ ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ์ ๊ฐ์ง ํ์ด์ง๋ ๋น ๋ฅด๊ฒ ๊ตฌํํด์ผ ํ๋ ๊ฒฝ์ฐ์ ์ ํฉํฉ๋๋ค.
PrefetchedQueryHydrationBoundary: ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ ์ฌ๋ฌ ์ฟผ๋ฆฌ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ ํ์ด์ง์ ์ ํฉํฉ๋๋ค.
useSuspenseQuery: ์ต์ React ํจํด์ ํ์ฉํ๊ณ , ๋ ์ ์ธ์ ์ธ ๋ฐ์ดํฐ ๋ก๋ฉ์ ๊ตฌํํ๊ณ ์ถ์ ๋ ์ ํฉํฉ๋๋ค.

Next.js์ Tanstack Query๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ค์ํ๋ฉฐ, ๊ฐ ๋ฐฉ๋ฒ์ ๊ณ ์ ํ ์ฅ๋จ์ ์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ํ๋ก์ ํธ์ ์๊ตฌ์ฌํญ๊ณผ ํ์ ์ ํธ๋์ ๋ฐ๋ผ ์ ์ ํ ๋ฐฉ๋ฒ์ ์ ํํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณต๋๋ fetch ํจ์๋ง์ผ๋ก๋ ์บ์ฑ, ๋ฌดํ ์คํฌ๋กค, ๋๊ด์ ์ ๋ฐ์ดํธ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ด๋ ต์ต๋๋ค. Tanstack Query๋ ์ด๋ฌํ ๋ณต์กํ ์ํ ๊ด๋ฆฌ๋ฅผ ๊ฐ๋จํ๊ฒ ํด์ฃผ๋ฏ๋ก, SSR ํ๊ฒฝ์์๋ ์ ์ ํ ๋ฐฉ๋ฒ์ผ๋ก ํ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
์ธ ๊ฐ์ง ๋ฐฉ๋ฒ ์ค ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ ๊ฐ์ฅ ์ ํฉํ ๋ฐฉ๋ฒ์ ์ ํํ์ฌ Next.js์ React Query์ ๊ฐ๋ ฅํ ์กฐํฉ์ ๊ฒฝํํด๋ณด์ธ์!