SSR 환경에서의 React Query
포스트
취소

SSR 환경에서의 React Query

React Query

React Query는 서버와 클라이언트 사이에서 데이터를 관리하기 위한 좋은 라이브러리입니다.
자세한 설명은 React Query 문서를 확인 해주세요.

SSR 환경에서 사용하기

React Query는 useQuery라는 hook을 이용하여 사용하는 점에서 알 수 있듯이 클라이언트 렌더링 때 데이터를 호출하고 관리하는 라이브러리입니다.
그러다보니 웹 서버에 제일 처음 접속하여 수신 받는 HTML 문서에는 API 호출 이후에 렌더링 되는 데이터들이 전혀 존재하지 않습니다.
이는 초기 로딩 속도 및 검색 엔진 최적화에서 그다지 바람직하지 않아요.

이 문제를 해결하기 위해 React Query 공식 페이지에서 알려주는 솔루션에 대해서 소개합니다.

Solution 1

Next.js 환경에서 SSR을 구현하기 위한 보편적인 방법은 getStaticProps 함수 혹은 getServerSideProps를 사용하는 것입니다.

1
2
3
4
5
6
7
8
9
export const getServerSideProps = () => {
  const data = fetch(api).then((r) => r.json()).then((r) => r);

  return {
    props: {
      data,
    },
  };
};

위 코드를 사용하면 api 라는 Endpoint에 데이터를 호출하여 SSR에서 데이터를 가져온 뒤, props 객체에 데이터를 담아 페이지 렌더링을 진행할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const getData = () => fetch(api).then((r) => r.json()).then((r) => r);

export const IndexPage = (props) => {
  const data = useQuery({
    queryKey: ['data'],
    queryFn: getData,
    initialData: props.data, // 여기 중요!
  });

  return (
    <div>
      { data.isSuccess && data }
    </div>
  );
};

export const getServerSideProps = () => {
  const data = getData();

  return {
    props: {
      data,
    },
  };
};

getServerSideProps에서 데이터를 호출한 뒤에 useQuery의 프로퍼티 중 하나인 initialData에 데이터를 전달하면 Next.js는 서버에서 불러온 데이터를 기반으로 HTML을 생성하여 기존의 문제점을 해결할 수 있습니다.

Solution 2

가급적 이 방법을 사용하시길 권장합니다.
두 번째 방법은 dehydratehydrate를 사용하는 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export interface AppProps {
  dehydratedState: DehydratedState;
};

export const App = ({ Component, ...rest }: AppPropsWithLayout<AppProps>) => {
  const Layout = Component.Layout || ((page) => page);

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

export default App;

우선 /pages/_app.tsx 파일에 Hydrate Provider를 추가합니다.
pageProps 객체에는 열린 페이지 컴포넌트에 할당 되는 props가 객체로 전달 되는데, 이 안에 dehydratedState 라는 값이 포함 될 예정입니다.
dehydratedStateHydrate Provider에 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { QueryClient, dehydrate } from '@tanstack/react-query';

const getData = () => fetch(api).then((r) => r.json()).then((r) => r);

export const getServerSideProps = () => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ['data'],
    queryFn: getData,
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
};

이후 데이터가 실제로 호출 되는 하위 페이지에서는 getServerSideProps 함수 내에서 QueryClient를 선언한 뒤 prefetchQuery 메소드를 사용하여 API를 호출합니다.
prefetchQuery 메소드는 useQuery 메소드와 유사하지만 실제로 데이터를 반환하지는 않고 해당 API에서 넘어온 데이터를 캐싱하는 역할만 합니다.

dehydratedState는 페이지 컴포넌트의 props에 할당 되어 _app.tsx 파일에서 pageProps 객체로 참조가 가능합니다.
Dehydrate된 QueryClientHydrate라는 컴포넌트가 CSR을 시작할 때 동일한 Query를 호출하는 부분을 찾아 initialData에 할당하는 역할을 수행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { QueryClient, dehydrate } from '@tanstack/react-query';

const getData = () => fetch(api).then((r) => r.json()).then((r) => r);

export const IndexPage = () => {
  const data = useQuery({
    queryKey: ['data'],
    queryFn: getData,
  });

  return (
    <div>
      { data.isSuccess && data }
    </div>
  );
};

export const getServerSideProps = () => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ['data'],
    queryFn: getData,
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
};

그러면 위와 같이 initialData 없이 SSR 처리가 가능합니다.

마치며

SSR 환경에서의 개발을 진행하시다 보면 hydrate라는 용어를 굉장히 많이 접하실거예요.
이 상황에서 말하는 hydrate는 대부분 ‘서버에서 만들어진 데이터를 클라이언트에서 활용할 수 있도록 동기화 시켜주는 작업’이라고 생각하시면 될 것 같습니다.
이에 대해 작성했던 문서가 있으니 한 번 쯤 읽어주시면 좋을 것 같습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

도형의 각도를 회전하는 방법

기술 블로그 살리기, TF부터 글 작성까지의 일대기