[React Query] (5) SSR
🧐 서론
지난 포스팅까지 진행하며, React Query를 사용해서 데이터를 다루는 주요 문법들을 정리하였다.
프로젝트를 마이그레이션하며 적용하던 중, Next.js 세팅인 점을 상기했으며 당연히 React Query도 SSR을 위한 솔루션을 제공하는 것을 알았다.
Next.js에서의 SSR을 React Query와 함께 사용하기 위해 어떤 방법이 있는지 한 번 정리해보도록 하겠다.
🌺 SSR 사용하기
공식문서는 React Query가 2가지 형태의 SSR을 지원한다고 소개한다.
- initialData : SSR 메서드로 불러온 응답을 React Query 기본값으로 넣어주는 방법
- Hydration : SSR 내에서 prefetch를 통해 쿼리를 불러온 뒤, queryClient에서 dehydrate한 상태값으로 페이지에 전달(권장)
initialData 방법이 세팅수요도 적고 간단하나 자손 컴포넌트까지 data를 props drilling 해야한다는 번거로움이 존재하고,
Hydration 방법은 queryClient로 간단히 접근 가능하며 기존 queryKey 값을 활용할 수 있다는 장점이 있어 2번을 권장하는 것이다.
각각의 방법을 어떻게 사용할 수 있는지 알아보자!
1. initialData
Next.js의 getStaticProps, getServerSideProps 함수를 통해 fetch한 결과값을 useQuery()의 initialData 옵션으로 넘겨준다.
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery(['posts'], getPosts, { initialData: props.posts })
// ...
}
보시다시피, Next.js SSR 문법을 통해 빠른 설정이 가능하나, 위에서 언급한 props drilling 등 몇 가지 trade-off가 존재한다.
- initialData를 세팅하는 자손 컴포넌트에서 활용하려는 경우, 해당 지점까지 props drilling을 넘겨주어야함
- 같은 query가 여러 곳에서 호출된다면 모두 initialData를 세팅해야한다
- query가 서버에서 fetch된 정확한 시점을 모르기에, 페이지가 로드된 시점을 기준으로 dataUpdatedAt을 설정해서 refetch 됨
2. Hydration
getStaticProps, getServerSideProps 함수에서 prefetch를 통해 쿼리 데이터를 미리 요청한다.
그리고, 결과값이 담긴 queryClient를 dehydrate한 뒤, page.props에 dehydratedState로 내려주는 방법이다.
// _app.jsx
import { Hydrate, QueryClient, QueryClientProvider } from '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>
)
}
먼저, <Hydrate>로 앱 컴포넌트를 한 번 감싸준다. 여기에, Hydration 시 참조를 위해 pageProps.dehydratedState를 넘겨준다.
* queryClient는 lifeCycle 주기당 인스턴스가 1번만 생성되도록 App 외부, state, 혹은 ref 등으로 저장한다.
// /components/Component.jsx
// pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query';
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(['posts'], getPosts)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Component() {
const { data } = useQuery(['posts'], getPosts)
}
다음으로, 컴포넌트에서 쿼리를 prefetch 하는 예제이다. SSR, SSG 모두 동일한 패턴을 사용한다.
- queryClient 인스턴스를 생성한 뒤, 여기에 prefetchQuery() 메서드로 쿼리를 prefetch 한다. (문법은 useQuery와 유사)
- query를 캐싱하기 위해, dehydrate() 처리를 한 queryClient를 dehydratedState로 추가한다.
만약, SSR 이후 useQuery()로 refetch가 일어나는 것을 비활성화고자 한다면, refetchOnMount를 false로 설정하거나 staleTime을 Infinity로 주는 등 방법이 있다.
* Prefetch 원리?
React Query의 SSR(prefetching)은 query cache와 dehydrate의 원리로 작동한다고 생각하면 된다.
먼저, prefetchQuery로 불러온 쿼리들은 dehydrate된 queryClient에 아래처럼 담겨있다.
{
mutations: [],
queries: [
{ state: [Object], queryKey: [Array], queryHash: '["user"]' },
{ state: [Object], queryKey: [Array], queryHash: '["event",1]' }
]
}
<Hydrate>는 dehydrateState로 받은 props를 캐시에 세팅하고, _core.hydrate() 함수를 호출하여 수화를 담당한다.
// Hydrate.js
function useHydrate(state, options) {
var queryClient = (0, _QueryClientProvider.useQueryClient)();
var optionsRef = _react.default.useRef(options);
optionsRef.current = options; // Running hydrate again with the same queries is safe,
// it wont overwrite or initialize existing queries,
// relying on useMemo here is only a performance optimization.
// hydrate can and should be run *during* render here for SSR to work properly
_react.default.useMemo(function () {
if (state) {
(0, _core.hydrate)(queryClient, state, optionsRef.current);
}
}, [queryClient, state]);
}
var Hydrate = function Hydrate(_ref) {
var children = _ref.children,
options = _ref.options,
state = _ref.state;
useHydrate(state, options);
return children;
};
// hydration.js
var queries = dehydratedState.queries || [];
queries.forEach(function (dehydratedQuery) {
var _options$defaultOptio2;
var query = queryCache.get(dehydratedQuery.queryHash); // Do not hydrate if an existing query exists with newer data
if (query) {
if (query.state.dataUpdatedAt < dehydratedQuery.state.dataUpdatedAt) {
query.setState(dehydratedQuery.state);
}
return;
} // Restore query
queryCache.build(client, (0, _extends2.default)({}, options == null ? void 0 : (_options$defaultOptio2 = options.defaultOptions) == null ? void 0 : _options$defaultOptio2.queries, {
queryKey: dehydratedQuery.queryKey,
queryHash: dehydratedQuery.queryHash
}), dehydratedQuery.state);
});
즉, getStaticProps 혹은 getServerSideProps에서 prefetch 후 queryClient를 dehydrate 시킨 값을 props로 넘겨주어야 한다.
여기서 queryCache에 세팅을 하므로, SSR 도중 useQuery를 만나면 해당 키로 캐싱된 값을 통해 렌더링을 진행하는 것이다.
또한, 클라이언트에서 hydrate되는 경우에도 queryClient에 dehydrate 했던 데이터를 바탕으로 진행되는 것이다.
SSR을 하는 방법이 그렇게 어렵지는 않았다.
특히, prefetchQuery 기능을 통해 복수의 SSR을 하나의 쿼리로 병합할 수 있으며, useQuery와도 연계되는 것이 Hydrate를 채택하는 큰 장점인 듯 하다.
React Query에 관한 시리즈는 이 정도로 마무리가 될 것 같다.
회사를 옮기면서 정신도 없지만, 앞으로 공부해야 할 그리고 공부하고 싶은 테마들이 매우 많다.
Next.js 최신버전인 11, 12버전에 관한 글을 한 번 정리한 뒤, 다음 테마를 또다시 찾아나서야겠다!! 🧐🧐
📎 출처
- [공식] React Query 공식문서 : https://tanstack.com/query/v4/docs/guides/ssr
- [공식/번역] seogeurim 님의 블로그 : https://seogeurim.tistory.com/19
- [React Query와 SSR] eomttt 님의 블로그 : https://velog.io/@eomttt/React-Query-%EC%99%80-SSR
- [Hydrate 적용법] devkkiri 님의 블로그 : https://devkkiri.com/post/9611766e-dc1f-4355-a94d-6ac1b4fba13a