React-query와 SSR
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
로 설정하는 방법이다.
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
동일하게 getStaticProps
나 getServerSideProps
에서 데이터를 prefetch를 하고, queryClient
를 dehydrate
하여 페이지에 dehydratedState
로 내려주면 된다.
사용하기에 앞서 다음과 같이 app 컴포넌트를 <QueryClientProvider>
, <Hydrate>
로 감싸서 설정해주어야 한다.
// _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>
);
}
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),
},
};
}
- prefetch 해서
queryClient
를dehydrate
한다. (html 렌더링 전 데이터 패치) - 서버사이드 렌더링 시 prefetch 할 때와 같은 key를 가지는 query를 만나면 캐싱된 데이터를 반환하고 이에 따라 렌더링을 진행한다.
- 서버사이드 렌더링을 진행한 HTML 파일을 클라이언트로 보내준다.
- 클라이언트에서
hydrate
진행 시queryClient
에서dehydrate
했던 데이터를 바탕으로hydrate
를 진행한다.
dehydrate?
queryClient
를 dehydrate
해서 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로 직접 넘겨주어 읽어올 순 없나?
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한 요청값이 담겨있지 않을까 했는데 dehydratedState
는 Next.js 페이지의 props로 직접 전달되지 않는다고 한다.
대신 서버에서 클라이언트로 데이터를 전달해야 하는 경우에는 페이지 구성 요소에서 getInitialProps 를 이용하여 데이터를 가져와서 컴포넌트에게 props로 전달할 수 있다.
출처