
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
export default function App({ Component, pageProps }) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// 보통 SSR에서는 staleTime을 0 이상으로 해줌으로써
// 클라이언트 사이드에서 바로 다시 데이터를 refetch 하는 것을 피한다.
staleTime: 60 * 1000,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
우선 App 컴포넌트에서 초기 설정을 해주어야 한다.
React 프로젝트와는 다르게, App 컴포넌트 안에 새로운 QueryClient를 useState()를 사용하여 state로 선언해주어야 한다.
App 컴포넌트 바깥에 선언하게 된다면, SSR 시 쿼리 캐시가 다른 사용자들과 request 간에 공유될 수 있기 때문에, 반드시 App 컴포넌트 내부에 선언을 해주어야 하며,
Next.js에서는 페이지를 이동하면 App 컴포넌트로부터 새롭게 렌더링되기 때문에, 쿼리 클라이언트가 매번 새롭게 생성되는 것을 막기 위해 state로 저장해주어야 한다.
React Query에서는 두 가지 방법으로 prefetching을 지원한다.
initialData를 사용한 prefetchingexport async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
이 방법은, Next.js에서 정적 생성, 서버 사이드 렌더링을 하면서 prefetch한 데이터를 useQuery()의 initialData로 설정해주는 방법이다.
사용 방법이 매우 간단하며, prefetching 단계에서는 React Query를 전혀 사용하지 않아도 된다는 장점이 있다.
다만, 몇 가지 단점도 존재하긴 한다.
getStaticProps(), getServerSideProps()는 pages 폴더 안에서만 동작하기 때문에, useQuery()를 사용하려는 컴포넌트까지 prefetch한 데이터를 props drilling으로 내려주어야 한다.
같은 쿼리의 useQuery()를 여러 곳에서 사용한다면, 모든 useQuery()에 똑같은 initialData를 설정해주어야 한다.
쿼리가 서버로부터 언제 fetch 되었는 지 정확하게 알 수 없기 때문에, dataUpdatedAt의 시간이나 쿼리 refetching이 필요한 지에 대한 여부는 페이지가 로드된 시점으로부터 계산된다.
특정 쿼리 키로 캐싱된 데이터가 이미 있다면, initialData는 해당 데이터를 절대 덮어쓰지 않는다. 때문에, 이미 캐싱된 데이터가 더 오래 된 것이더라도, getServerSideProps() 함수로 받아 온 데이터는 initialData로 설정이 되기 때문에, 새로운 데이터로 업데이트 할 수 없다.
import { dehydrate, HydrationBoundary, QueryClient, useQuery } from '@tanstack/react-query'
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
function Posts() {
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
// 이 쿼리는 서버에서 prefetch하지 않는 데이터.
// prefetch하는 데이터와 아닌 데이터를 자유롭게 섞어서 활용할 수 있다.
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
export default PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
Hydration은 이미 렌더링된 정적인 HTML을 React 코드와 연결해서 동적인 상태로 바꿔주는 것을, Dehydrate는 동적인 것을 다시 정적인 상태로 만드는 작업을 말하는데,
위 코드와 같이, prefetch 한 결과값이 담긴 queryClient를 dehydrate해서 클라이언트로 보내줄 수도 있다.
이렇게 하면, 초기 설정 코드가 늘어나긴 하지만, initialData를 이용하면서 발생하는 여러 단점을 모두 해결할 수 있다.