Next.js는 서버 사이드에서 데이터 Prefetching이 가능하다. Prefetch 하게 되는 데이터는 HTML 페이지가 클라이언트에게 전송되기 전에 준비되어 HTML에 포함되어 렌더링된다.

React-Query는 Next.js의 서버 사이드에서 데이터를 Prefetch하여 queryClient로 넘겨주는 기능을 제공한다.

따라서 한 번 사이트가 로딩된 후에는 로딩 시간이 크게 단축된다는 점, SEO에 좋다는 장점이 있다.

사실 진행하고 있는 토이 프로젝트의 데이터는 데이터가 크지 않기 때문에 로딩 시간에 큰 차이가 없겠지만, Next.js를 이용한 SSR을 React-Query를 이용해서 데이터를 패치해보고 싶어서 알아보게 되었다.


공식 문서에서 보면 React-Query를 이용하여 SSR에서 데이터를 prefetch 하기 위해 두 가지 방법을 지원한다. 이 때 다음 두 가지가 지원되는 Next.js 에서 사용하기를 권장하고 있다.

  • Static Generation (SSG)
  • Server-side Rendering (SSR)

1. InitialData

Next.js의 getStaticProps 또는 getServerSideProp에서 원하는 API를 요청해서 데이터를 패치한 다음, 응답을 페이지에 props 로 넘겨주어 useQuery의 initialData 로 설정하는 방법이다.

Copy
function Posts(props) {
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
    initialData: props.posts, // initialData 로 설정
  });
}

export async function getStaticProps() {
  const posts = await getPosts();
  return { props: { posts } };
}

장/단점

  • 간단하다
  • 더 깊은 컴포넌트에서 useQuery 를 사용할 경우 하위 컴포넌트로 props를 계속해서 건네주어야 한다. (props-drilling)
  • 같은 응답을 하는 query가 여러개인 경우 initialData를 다 넣어주어야 한다.
  • 서버에서 쿼리를 가져온 시간을 알 수 없어서 데이터가 업데이트 된 시간이나 쿼리를 다시 가져와야 하는지에 대한 여부를 페이지가 로드 된 시간을 기반으로 결정해야 한다.

그래서 React-query에서도 두 번째 방법을 추천한다.


2. Hydration

동일하게 getStaticPropsgetServerSideProps 에서 데이터를 prefetch를 하고, queryClientdehydrate 하여 페이지에 dehydratedState로 내려주면 된다.

사용하기에 앞서 다음과 같이 app 컴포넌트를 <QueryClientProvider>, <Hydrate> 로 감싸서 설정해주어야 한다.

Copy
// _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>
  );
}
Copy
import { dehydrate, QueryClient, useQuery } from 'react-query';

function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the "Posts"-page, data will be available immediately either way
  const { data } = useQuery('posts', getPosts);

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: otherData } = useQuery('posts-2', getPosts);

  // ...
}

export async function getStaticProps() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery('posts', getPosts);

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };
}
  1. prefetch 해서 queryClientdehydrate한다. (html 렌더링 전 데이터 패치)
  2. 서버사이드 렌더링 시 prefetch 할 때와 같은 key를 가지는 query를 만나면 캐싱된 데이터를 반환하고 이에 따라 렌더링을 진행한다.
  3. 서버사이드 렌더링을 진행한 HTML 파일을 클라이언트로 보내준다.
  4. 클라이언트에서 hydrate 진행 시 queryClient 에서 dehydrate 했던 데이터를 바탕으로 hydrate 를 진행한다.

dehydrate?

queryClientdehydrate 해서 props 에 내려주고 있다. 이 때 dehydrate 는 쿼리 클라이언트의 상태를 serialize(직렬화)하는 역할이라고 보면 된다.

dehydrate 함으로써 클라이언트-서버간 전송해야하는 데이터의 양이 줄어들고, 애플리케이션의 성능을 최적화할 수 있다는 장점이 있다.

정보를 보내기 위해서는 전송 가능한 형태로 만드는 것(serialize)이 필요하다. 이 때 dehydrate(탈수) 물기를 빼주어야 전달하기 쉽고 전달해줄 수 있다 라고 생각하면 쉽다.


hydrate?

hydrate 란 NextJS 개념은 아니고 React 개념으로, DOM 요소에 자바스크립트 속성을 매칭시키기 위한 것을 말한다.

즉, NextJS에서는 서버 사이드에서 pre rendering 한 html 파일들을 서버사이드 렌더링 형식으로 보내주고, 클라이언트 사이드에서 React 코드를 통해 hydrate 를 진행한다.

또한 hydrate 를 진행해도 단순히 DOM에 JS 속성을 매칭시키는 일이기 때문에 paint가 다시 일어나진 않는다.

서버사이드에서 내려주는 HTML은 자바스크립트 이벤트 리스너들이 붙어있지 않은데, hydrate 단계에서 이런 부분들을 다시 붙여주게 된다.

서버단에서 dehydrated 되어서 온 데이터를 물기가 빠져있으므로 다시 물을 부어준다. hydrate 를 진행해주어 스크립트 코드들을 매칭시켜준다.


dehydratedState를 page에 props로 직접 넘겨주어 읽어올 순 없나?

Copy
function Posts({dehydratedState}) { // 이런식으로 바로 넘겨줄 순 없나?
	console.log(dehydratedState); // undefined
	...
}

export async function getSeverSideProps() {
 const queryClient = new QueryClient()
 await queryClient.prefetchQuery('posts', getPosts);

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

이렇게 넘겨주어서 읽어오게되면 dehydratedState는 undefined 가 뜨게 된다.

prefetch한 요청값이 담겨있지 않을까 했는데 dehydratedStateNext.js 페이지의 props로 직접 전달되지 않는다고 한다.

대신 서버에서 클라이언트로 데이터를 전달해야 하는 경우에는 페이지 구성 요소에서 getInitialProps 를 이용하여 데이터를 가져와서 컴포넌트에게 props로 전달할 수 있다.


출처

Copy