๐Ÿง™โ€โ™‚๏ธ Next.js์™€ React Query์˜ SSR ๋งˆ๋ฒ•: ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด์˜ ์™„๋ฒฝํ•œ ์กฐํ™”

Woodyยท2024๋…„ 8์›” 27์ผ
0

react

๋ชฉ๋ก ๋ณด๊ธฐ
6/6

๋“ค์–ด๊ฐ€๋ฉฐ

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ํ•œ ๋ฒˆ์ฏค์€ ์ด๋Ÿฐ ๊ณ ๋ฏผ์„ ํ•ด๋ณด์…จ์„ ๊ฒ๋‹ˆ๋‹ค: "๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋–ป๊ฒŒ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜์ง€?" React Query(Tanstack Query)๋Š” ์ด๋Ÿฐ ๊ณ ๋ฏผ์— ๋Œ€ํ•œ ๊ฐ•๋ ฅํ•œ ํ•ด๋‹ต์„ ์ œ์‹œํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ Next.js์˜ SSR ํ™˜๊ฒฝ์—์„œ๋Š” ์•ฝ๊ฐ„์˜ ๋งˆ๋ฒ• ์ฃผ๋ฌธ์ด ๋” ํ•„์š”ํ•˜์ฃ !

๋งˆ๋ฒ•์‚ฌ ์ฝ”๋”ฉ

์ด ๊ธ€์—์„œ๋Š” Next.js์™€ React Query๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ์˜ SSR ์ตœ์ ํ™” ์ „๋žต์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 3๊ฐ€์ง€ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ๋น„๊ตํ•˜์—ฌ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๋ฐฉ๋ฒ•์„ ์ฐพ๋Š” ๋ฐ ๋„์›€์ด ๋˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

SSR์ด ํ•„์š”ํ•œ ์ด์œ : ๋น ๋ฅธ ํŽ˜์ด์ง€, ํ–‰๋ณตํ•œ ์‚ฌ์šฉ์ž

๋จผ์ €, ์™œ SSR์ด ํ•„์š”ํ•œ์ง€ ์‚ดํŽด๋ด…์‹œ๋‹ค:

  1. ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„ ๊ฐœ์„ : ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ๋ Œ๋”๋ง๋œ HTML์„ ๋ฐ›์•„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ฆ‰์‹œ ์ปจํ…์ธ ๋ฅผ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  2. SEO ์ตœ์ ํ™”: ๊ฒ€์ƒ‰ ์—”์ง„ ๋ด‡์ด JavaScript๋ฅผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ ๋„ ์ปจํ…์ธ ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ์–ด ๊ฒ€์ƒ‰ ๋…ธ์ถœ์— ์œ ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  3. ์„ฑ๋Šฅ ํ–ฅ์ƒ: ํด๋ผ์ด์–ธํŠธ์˜ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ์„ ์ค„์—ฌ ๋” ๋‚˜์€ ์„ฑ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  4. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ : ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ ๋นˆ ํ™”๋ฉด์ด๋‚˜ ๋กœ๋”ฉ ์Šคํ”ผ๋„ˆ ๋Œ€์‹  ์ปจํ…์ธ ๋ฅผ ๋ฐ”๋กœ ๋ณผ ์ˆ˜ ์žˆ์–ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

Tanstack Query๋ฅผ SSR๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ์žฅ์ ์„ ๋ชจ๋‘ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•ด์•ผ ํ• ๊นŒ์š”?

SSR์„ ์œ„ํ•œ React Query Provider ์„ค์ •

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๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Tanstack Query์˜ SSR ์ตœ์ ํ™” 3๊ฐ€์ง€ ๋ฐฉ๋ฒ•

React Query์™€ Next.js๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ 3๊ฐ€์ง€ ์ฃผ์š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•

1. initialData ์‚ฌ์šฉํ•˜๊ธฐ: ๊ฐ„๋‹จํ•˜์ง€๋งŒ ๊ฐ•๋ ฅํ•œ ๋ฐฉ๋ฒ•

initialData ์˜ต์…˜์€ ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์œผ๋กœ, ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

๋™์ž‘ ์›๋ฆฌ:

  1. ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
  2. ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ props๋กœ ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  3. ํด๋ผ์ด์–ธํŠธ์—์„œ 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;

2. PrefetchedQueryHydrationBoundary ์‚ฌ์šฉํ•˜๊ธฐ: ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํ•œ๋ฒˆ์—

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>
  );
}

์žฅ์ :

  • ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์™€ ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ๋ณต์žกํ•œ ํŽ˜์ด์ง€์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ :

  • ๋ณ„๋„์˜ prefetch ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ ํŽ˜์ด์ง€๋ฅผ ๊ฐ์‹ธ๋Š” SSR ํŽ˜์ด์ง€๋ฅผ ๋ณ„๋„๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

3. @tanstack/react-query-next-experimental ์‚ฌ์šฉํ•˜๊ธฐ: ๋ฏธ๋ž˜๋ฅผ ํ–ฅํ•œ ๋„์ „

@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');
    },
  });
};

์žฅ์ :

  • Suspense์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ์„ ์–ธ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์™€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Next.js 13 ์ด์ƒ์˜ App Router์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ :

  • ์•„์ง ์‹คํ—˜์ ์ธ ํŒจํ‚ค์ง€์ด๋ฏ€๋กœ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
  • Suspense๋ฅผ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์ถ”๊ฐ€์ ์ธ ์„ค์ •์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์–ด๋–ค ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ์š”?

๊ฐ ๋ฐฉ๋ฒ•์€ ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ ํ•ฉํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. initialData: ๊ฐ„๋‹จํ•œ ๋ฐ์ดํ„ฐ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ฐ€์ง„ ํŽ˜์ด์ง€๋‚˜ ๋น ๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

  2. PrefetchedQueryHydrationBoundary: ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋‚˜ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ํŽ˜์ด์ง€์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

  3. useSuspenseQuery: ์ตœ์‹  React ํŒจํ„ด์„ ํ™œ์šฉํ•˜๊ณ , ๋” ์„ ์–ธ์ ์ธ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ๋ก : ์ƒํ™ฉ์— ๋งž๋Š” ์„ ํƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค

๊ฒฐ๋ก 

Next.js์™€ Tanstack Query๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์–‘ํ•˜๋ฉฐ, ๊ฐ ๋ฐฉ๋ฒ•์€ ๊ณ ์œ ํ•œ ์žฅ๋‹จ์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŒ€์˜ ์„ ํ˜ธ๋„์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜๋Š” fetch ํ•จ์ˆ˜๋งŒ์œผ๋กœ๋Š” ์บ์‹ฑ, ๋ฌดํ•œ ์Šคํฌ๋กค, ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. Tanstack Query๋Š” ์ด๋Ÿฌํ•œ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด์ฃผ๋ฏ€๋กœ, SSR ํ™˜๊ฒฝ์—์„œ๋„ ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์œผ๋กœ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

์„ธ ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ์ค‘ ์—ฌ๋Ÿฌ๋ถ„์˜ ํ”„๋กœ์ ํŠธ์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๋ฐฉ๋ฒ•์„ ์„ ํƒํ•˜์—ฌ Next.js์™€ React Query์˜ ๊ฐ•๋ ฅํ•œ ์กฐํ•ฉ์„ ๊ฒฝํ—˜ํ•ด๋ณด์„ธ์š”!

๋ ˆํผ๋Ÿฐ์Šค

profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋กœ ์‚ด์•„๊ฐ€๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€