SSR 환경에서 react-query 사용하기 (initialData & Hydration)

hzn·2023년 4월 13일
0

PROJECT🏝

목록 보기
16/24
post-thumbnail
post-custom-banner

SSR 환경에서 react-query 사용하기

공식 문서에 따르면, React Query는 서버에서 데이터를 미리 받아와서 queryClient에 전달하는 2가지 방법을 지원한다.

1. initialData

  • 서버에서 데이터를 prefetch하고, 컴포넌트에 initialData로 전달하는 방법.
  • Next.js의 getStaticProps 또는 getServerSideProps를 사용하면, 가져온 데이터를 useQueryinitialData 옵션으로 넣어줄 수 있다.

장점

  • 적은 설정으로 간편하게 이용할 수 있다.

단점

  • 해당 컴포넌트의 자손 컴포넌트에서 데이터를 사용해야 할 경우 props drilling.
  • 여러 위치에서 동일한 쿼리를 사용해 useQuery를 호출하는 경우 모든 위치에 initialData를 전달해야 한다.
  • 쿼리가 서버에서 fetch된 시간을 알 수 있는 방법이 없으므로, dataUpdateAt와 쿼리가 refetch 되어야하는지 여부는 페이지가 로드된 시간을 기준으로 결정한다.

pages/nearby/[id].tsx

export async function getServerSideProps(context) {
  const { id } = context.params;
  const res = await getProductDetail(id); // 서버 측에서 상세 페이지 데이터 요청
  const detailData = res.data;
  return { props: { id, detailData }} // 받아온 데이터 props로 전달
}
...

export default function ProductDetail({ id, detailData }: productDetailType) {
  const { data } = useQuery(['productDetail'], () => getProductDetail(id), {
    initialData: detailData, // useQuery의 initialData 옵션으로 서버에서 받아온 데이터(`detailData`) 전달
  });  

2. Hydration

  • 서버에서 쿼리를 prefetch(캐싱)하고 캐시를 dehydrate한 후 클라이언트에서 rehydrate하는 방법.

  • React Query는 쿼리를 서버에서 prefetching하고 이 쿼리를 queryClient에 dehydraing하는 것을 지원한다. (여러 개의 쿼리도 가능)
    ➡️ 즉, 서버는 페이지 로드 즉시 사용가능한 마크업을 미리 렌더링 할 수 있다.
    ➡️ 또한 JS가 준비되는 즉시 react-query의 전체 기능을 사용할 수 있도록 쿼리가 업그레이드 혹은 hydrate된다.
    ➡️ 서버에서 render 된 시간을 기준으로 stale 상태가 되므로 클라이언트에서도 이에 따라 쿼리를 refetch한다.

  • 서버에서 pre-rendering (HTML을 initial Load) 할 때 쿼리도 캐싱
  • 이후 캐시를 dehydrate
  • hydration은 클라이언트(브라우저)에서

서버에서 쿼리 캐싱 & hydration 설정하기

1) _app.tsx

  1. 앱 내부(_app.tsx에서)와 instance ref(또는 React state)(해당 컴포넌트?)에 새 QueryClient 인스턴스를 만든다.
    ➡️ 이렇게 하면 컴포넌트 lifecycle 당 한 번만 QueryClient를 생성하는 동시에, 서로 다른 사용자와 요청 간에 데이터가 공유되지 않는다.
  2. 앱 컴포넌트를 <QueryClientProvider>로 감싸고 queryClient 인스턴스를 prop으로 전달한다
  3. 앱 컴포넌트를 <Hydrate>로 감싸고 pageProps.dehydratedState🦄를 prop으로 전달한다.

_app.jsx

import {
  Hydrate,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

export default function MyApp({ Component, pageProps }) {
  const [queryClient] = React.useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

➡️ 이제 getStaticProps(SSG) 또는 getServerSideProps(SSR)를 사용해 서버에서 페이지의 데이터를 prefetch 하기 위한 준비가 끝났다. (두 방식은 똑같은 방법으로 적용된다.)

2) 해당 페이지

  1. 각 페이지 요청마다 QueryClient 인스턴스를 만든다.
    (서버로 페이지 요청이 갈 때 마다 = getStaticProps 또는 getServerSideProps 함수 내에)
    ➡️ 이렇게 하면 서로 다른 사용자와 요청들 간에 데이터가 공유되지 않는다.

  2. queryClient.prefetchQuery 메서드를 사용해 데이터를 prefetch(캐싱)하고, 완료될 때까지 기다린다.

  3. dehydrate를 사용해 쿼리 캐시를 dehydrate하고, dehydratedState prop으로 페이지에 넘겨준다.
    (위의 _app.tsx에서 앱 컴포넌트를 감싼 <Hydrate>의 prop으로 전달된 pageProps.dehydratedState🦄가 이것이다.)

pages/posts.jsx

import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'

export async function getStaticProps() {
  const queryClient = new QueryClient() // 1. 새 QueryClient 인스턴스 만들기

  await queryClient.prefetchQuery(['posts'], getPosts) // 2. 데이터를 prefetch(캐싱)

  return {
    props: {
      dehydratedState: dehydrate(queryClient), // 3. dehydrate한 캐시를 props로 페이지에 넘겨준다.
    },
  }
}

function Posts() {
// dehydrate - hydrate 사용 ✅
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

// dehydrate - hydrate 사용 ❌
  const { data: otherData } = useQuery({
    queryKey: ['posts-2'],
    queryFn: getPosts,
  })

...
}
  • posts 쿼리
    : dehydrate - hydrate 사용 ✅ (getStaticProps에서 prefetch한 데이터와 같은 쿼리 키(posts) 사용)
    : 이 쿼리는 해당 페이지(Posts)의 아무 자손 페이지에서나 해당 방식으로 사용될 수 있으며, 받아온 데이터는 즉시 사용될 수 있다.
  • posts-2 쿼리
    : dehydrate - hydrate 사용 ❌ (다른 쿼리 키(posts-2) 사용)
    : 서버에서 쿼리를 미리 캐싱하지 않으며, 클라이언트에서 가져올 때까지 fetching 하지 않는다.
  • 위와 같이 일부 쿼리는 prefetch하고 (dehydrate - hydrate 방식) 다른 쿼리는 queryClient(앱 전체의 queryClient)에 가져와서 쓰는 것도 가능하다.
  • 즉,특정 쿼리에 대한 prefetchQuery를 추가하거나 제거하는 방법으로 어떤 데이터를 서버가 렌더링 할 것인지 아닌지를 제어할 수 있다.
    (서버에서 가져오고 싶으면 dehydrate - hydrate 방식을 사용하면 됨)

상세 페이지(SSR)에서 react-query 사용하기(Hydration 방식)

Ngether 상세 페이지 개요

_app.tsx

...
import {
  Hydrate,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query';
...
export default function App({ Component, pageProps }: AppPropsWithLayout) {
...
const queryClient = new QueryClient(); // 앱 전체 QueryClient 인스턴스
...
  return (
    <>
      <Head>
        <title>Ngether</title>
      </Head>
      <QueryClientProvider client={queryClient}> // queryClient 인스턴스
        <Hydrate state={pageProps.dehydratedState}> // dehydratedState를  Hydrate
          <ReactQueryDevtools initialIsOpen={false} />
          {renderWithLayout(
            <main className={notoSansKR.className}>
              <Component {...pageProps} /> // 컴포넌트
            </main>
          )}
        </Hydrate>
      </QueryClientProvider>
    </>
  );
}

pages/nearby/[id].tsx

import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query';

export async function getServerSideProps(context: any) {
   const { id } = context.params;

   const queryClient = new QueryClient(); //  새 QueryClient 인스턴스 만들기

    await queryClient.prefetchQuery(['productDetail'], () =>
    getProductDetail(id)
  ); // 데이터를 prefetch(캐싱)

  return { props: { id, dehydratedState: dehydrate(queryClient) } };
} //  dehydrate한 데이터 캐시를 props로 페이지에 넘겨준다.

...
export default function ProductDetail({ id }: productDetailType) {
  const { data } = useQuery(['productDetail'], () => getProductDetail(id)); // 같은 쿼리키(productDetail)로 데이터 받음

Hydration 시 serializing error 해결하기

  • 서버에서 클라이언트로 데이터(쿼리 캐시)를 props로 넘겨줄 때 AxiosResponse 타입(Promise 객체)이어서 serializing error가 발생하는 경우,

Server Error
Error: Error serializing .dehydratedState.queries[0].state.data.headers returned from getServerSideProps in "/nearby/[id]".
Reason: object ("[object AxiosHeaders]") cannot be serialized as JSON. Please only return JSON serializable data types.

  • props를 Promise 객체에서 data를 추출한 형태로 넘겨주도록 수정한다.

api/detail.ts (productDetail 데이터 요청 함수)

export async function getProductDetail(id: string) {
  const res = await axios.get(`${REQUEST_URL}/api/boards/${id}`, { // axios가 처리될 때까지 기다려야 하므로 async-await 처리
    headers: {
      Authorization: Cookies.get('access_token'),
      Refresh: Cookies.get('refresh_token'),
    },
  });

  return res.data; // res에서 data를 추출해서 리턴
}

pages/nearby/[id].tsx

export async function getServerSideProps(context: any) {
  const queryClient = new QueryClient();
  const { id } = context.params;
  await queryClient.prefetchQuery(['productDetail'], () =>
    getProductDetail(id)
  );

  return {
    props: {
      id,
      dehydratedState: dehydrate(queryClient),
    },
  };
}
...
export default function ProductDetail({ id }: productDetailType) {
  const { data } = useQuery(['productDetail'], () => getProductDetail(id));

레퍼런스

post-custom-banner

0개의 댓글