Next.js와 React Query의 SSR 최적화

Woody·2024년 8월 27일
0

react

목록 보기
6/6

React-query(tanstack-query) 는 서버 상태 관리를 위한 인기있는 라이브러리 입니다.
클라이언트 기반으로만 작동되던 React 에서는 바로 사용하면 됐지만, 최근 Next.js 가 급 부상하며 SSR 방식에는 조금 문제가 되었습니다.

이에 tanstack-query 를 SSR 에서 사용하기 위한 몇가지 방법이 추천되고 있습니다.

SSR 이 필요한 이유

  1. 초기 로딩 속도 개선: 서버에서 미리 렌더링된 HTML을 받아 빠르게 화면을 표시할 수 있습니다.

  2. SEO 최적화: 검색 엔진 봇이 JavaScript를 실행하지 않고도 컨텐츠를 읽을 수 있습니다.

  3. 성능 향상: 클라이언트의 리소스 사용을 줄일 수 있습니다.

  4. 사용자 경험 개선: 초기 로딩 시 빈 화면이나 로딩 스피너 대신 컨텐츠를 바로 볼 수 있습니다.

Tanstack Query를 SSR과 함께 사용할 때는 서버에서 데이터를 미리 가져와 클라이언트로 전달하는 과정이 필요합니다.

SSR 을 위한 Provider 설정

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

Tanstack-query 에서 제시하는 SSR 최적화 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;

PrefetchedQueryHydrationBoundary 사용하기

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

장점

  • 서버에서 데이터를 미리 가져와 초기 렌더링 시 데이터를 즉시 사용할 수 있습니다.

  • 여러 쿼리를 한 번에 처리할 수 있어 복잡한 페이지에 적합합니다.

단점

  • 따로 prefetch 함수를 사용해주어야 함
  • 별도의 SSR 페이지를 통해 clientPage 를 불러주어야 함

@tanstack/react-query-next-experimental 사용하기

@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와 함께 사용하여 로딩 상태를 선언적으로 관리할 수 있습니다.

  • 서버 컴포넌트와 클라이언트 컴포넌트 간의 데이터 흐름을 자연스럽게 처리할 수 있습니다.

결론

PrefetchedQueryHydrationBoundary

  • 복잡한 데이터 요구사항을 가진 페이지에 적합합니다.

initialData

  • 간단한 데이터 프리페칭에 유용합니다.

@tanstack/react-query-next-experimental의 useSuspenseQuery

  • 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

profile
프론트엔드 개발자로 살아가기

0개의 댓글