본 글은 React Query 공식 문서 중 SSR 페이지를 한글 번역한 것이며, Using Next.js 챕터까지를 다루고 있습니다.
React Query는 서버에서 데이터를 prefetch해와 queryClient에 넘겨주는 것에 있어 두 가지 방식을 지원한다.
- 직접 데이터를 prefetch해와서
initialData
로 넘겨주는 것- 간단한 케이스를 위한 빠른 설정이 가능하다.
- 몇 가지 주의 사항이 있다.
- 서버에서 query를 prefetch하고, 그 cache를 dehydrate해서 클라이언트에 그것을 rehydrate하는 것
- 약간 더 많은 설정이 필요하다.
💡 dehydrate와 hydrate
SSR에서 hydration의 개념을 먼저 살펴보자. 서버 사이드에서 먼저 정적인 페이지(HTML)를 렌더링하고, JS 코드가 모두 로드되면 이 HTML에 이벤트 핸들러 등을 붙여 동적인 페이지를 만드는 과정을 hydration이라 말한다. hydration을 직역하면 '수분 공급'이라는 뜻인데, 즉 건조한 HTML에 수분(인터랙션, 이벤트 핸들러 등)을 공급하여 동적인 페이지를 만들어나가는 과정을 말한다.
React Query에서도 hydration과 관련된 기능을 제공하고 있는데, 공식 문서 중 hydration 페이지를 살펴보면dehydrate
와hydrate
메서드를 볼 수 있다.dehydrate
는 나중에 hydrate로 공급할 수 있는cache
에 대한 고정된 표현을 생성하며,hydrate
는 이전에 dehydrate된 state를cache
에 추가한다고 소개되어있다.
Using Next.js
이러한 메커니즘들의 정확한 구현은 플랫폼마다 다양할 수 있지만, React Query에서는 pre-rendering에 대한 두가지 형식을 지원하는 Next.js로 시작하는 것을 권장한다.
- Static Generation (SSG)
- Server-side Rendering (SSR)
React Query는 사용 중인 플랫폼에 관계 없이 이 두 가지 형식의 pre-rendering을 모두 지원한다.
Using initialData
Next.js의 getStaticProps
나 getSererSideProps
함수를 통해 fetch한 데이터를 useQuery
의 initialData
옵션을 통해서도 넘겨줄 수 있다. React Query의 관점에서 이들은 같은 방식이라 볼 수 있으며, getStaticProps
는 아래와 같이 볼 수 있다.
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery(['posts'], getPosts, { initialData: props.posts })
// ...
}
initialData
를 활용하는 방식은 설정할 것이 적고 몇몇 케이스에서는 가장 빠른 솔루션일 수 있겠지만, 전체 접근 방식과 비교했을 때 몇 가지 트레이드오프가 존재한다.
- 만일
useQuery
를 컴포넌트 트리 깊숙이 존재하는 컴포넌트에서 호출한다면,initialData
를 그 지점까지 넘겨줘야 한다. - 만일
useQuery
를 같은 query로 여러 위치에서 호출한다면, 호출한 지점 모두에initialData
를 넘겨줘야한다. - 해당 query가 서버로부터 fetch된 정확한 시점을 알 수 있는 방법이 없어서, 페이지가 로드된 시점을 기반으로
dataUpdatedAt
이나 refetch가 필요한 시점에 대한 것을 결정한다.
Using Hydration
React Query는 Next.js 서버에서 여러 개의 query를 prefetch하고 그 query들을 queryClient에 dehydrate하는 것을 지원한다. 즉, 서버는 페이지 로드 시 즉시 사용할 수 있는 마크업을 미리 렌더링할 수 있으며, JS를 사용할 수 있게 되면 React Query는 라이브러리 자체의 기능으로 이러한 query들을 업그레이드하거나 hydrate할 수 있다. 이 기능 중에는 query들이 서버에서 렌더링된 이후로 클라이언트에서 stale한 상태가 되었을 때 refetch해오는 것도 포함된다.
서버에서의 query 캐싱 지원 및 hydration 설정을 위해서는:
- app, instance ref (또는 React 상태) 내에 새로운
QueryClient
instance를 생성하자. 이렇게 하면 컴포넌트 라이프사이클 당 QueryClient를 오직 한 번만 생성하여 데이터가 서로 다른 사용자와 요청 간에 공유되지 않는다. - app 컴포넌트를
<QueryClientProvider>
로 감싸고 client instance에 넘겨주자. - app 컴포넌트를
<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해올 준비가 되었다. React Query의 관점에서 이들은 같은 방식이라 볼 수 있으며, getStaticProps
는 아래와 같다.
- 각 페이지의 request 별로 새로운
QueryClient
instance를 생성하자. 이렇게 하면 서로 다른 사용자와 요청 간에 데이터가 공유되지 않는다. - client의
prefetchQuery
메서드를 사용해 데이터를 prefetch해오고 완료되기까지 기다리자. - query cache를 dehydrate하기 위해
dehydrate
메서드를 사용하고,dehydratedState
prop을 통해 이를 페이지에 넘겨주자. 이는_app.js
에서 불러온 cache와 동일한 prop이다.
// 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 Posts() {
// 이 useQuery는 "Posts" 페이지에 대한 더 깊은 자식 요소에서 사용될 수도 있으며, data는 어느 쪽에서 사용되든 즉시 사용할 수 있다.
const { data } = useQuery(['posts'], getPosts)
// 이 query는 서버에서 prefetch된 것이 아니며 클라이언트에서 시작할 때까지 fetch하지 않는다.
// 두 가지 패턴(서버에서 prefetch, 클라이언트에서 fetch)은 혼합될 수 있다.
const { data: otherData } = useQuery(['posts-2'], getPosts)
// ...
}
위에서 설명한 대로, 일부 query들은 prefetch하고 다른 query들은 queryClient에서 fetch하도록 하는 것이 가능하다. 이 말인 즉슨, 특정 query에 대해 prefetchQuery
를 추가하거나 제거하지 않고 어떤 컨텐츠를 서버가 렌더링할지 제어할 수 있다는 것이다.
Caveat for Next.js rewrites
만일 Next.js의 rewrites 기능과 함께 Automatic Static Optimization이나 getStaticProps
를 쓸 경우 함정이 있는데, React Query에 의한 두번째 hydration을 발생시킬 수 있다. Next.js가 클라이언트에서 rewrites를 파싱하고 hydration 후 매개변수를 수집하여 router.query
에 제공할 수 있도록 해야 하기 때문이다.
이 결과는 모든 hydration data(예를 들어 컴포넌트의 props로 사용되는 데이터이거나 useEffect
의 useMemo
의 의존성 배열에서 사용되는 데이터)에 대해 참조 동등성(referential equality)이 누락된 것이다.
참고 자료