공식 문서에 따르면, React Query는 서버에서 데이터를 미리 받아와서 queryClient
에 전달하는 2가지 방법을 지원한다.
initialData
로 전달하는 방법.getStaticProps
또는 getServerSideProps
를 사용하면, 가져온 데이터를 useQuery
의 initialData
옵션으로 넣어줄 수 있다. dataUpdateAt
와 쿼리가 refetch 되어야하는지 여부는 페이지가 로드된 시간을 기준으로 결정한다.pages/nearby/[id].tsx
export async function getServerSideProps(context) { const { id } = context.params; const res = await getProductDetail(id); // 서버 측에서 상세 페이지 데이터 요청 const detailData = res.data; return { props: { id, detailData }} // 받아온 데이터 props로 전달 } ... export default function ProductDetail({ id, detailData }: productDetailType) { const { data } = useQuery(['productDetail'], () => getProductDetail(id), { initialData: detailData, // useQuery의 initialData 옵션으로 서버에서 받아온 데이터(`detailData`) 전달 });
서버에서 쿼리를 prefetch(캐싱)하고 캐시를 dehydrate한 후 클라이언트에서 rehydrate하는 방법.
React Query는 쿼리를 서버에서 prefetching하고 이 쿼리를 queryClient
에 dehydraing하는 것을 지원한다. (여러 개의 쿼리도 가능)
➡️ 즉, 서버는 페이지 로드 즉시 사용가능한 마크업을 미리 렌더링 할 수 있다.
➡️ 또한 JS가 준비되는 즉시 react-query의 전체 기능을 사용할 수 있도록 쿼리가 업그레이드 혹은 hydrate된다.
➡️ 서버에서 render 된 시간을 기준으로 stale
상태가 되므로 클라이언트에서도 이에 따라 쿼리를 refetch한다.
- 서버에서 pre-rendering (HTML을 initial Load) 할 때 쿼리도 캐싱
- 이후 캐시를 dehydrate
- hydration은 클라이언트(브라우저)에서
_app.tsx
에서)와 instance ref(또는 React state)(해당 컴포넌트?)에 새 QueryClient
인스턴스를 만든다.<QueryClientProvider>
로 감싸고 queryClient
인스턴스를 prop으로 전달한다<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 하기 위한 준비가 끝났다. (두 방식은 똑같은 방법으로 적용된다.)
각 페이지 요청마다 새 QueryClient
인스턴스를 만든다.
(서버로 페이지 요청이 갈 때 마다 = getStaticProps
또는 getServerSideProps
함수 내에)
➡️ 이렇게 하면 서로 다른 사용자와 요청들 간에 데이터가 공유되지 않는다.
queryClient.prefetchQuery
메서드를 사용해 데이터를 prefetch(캐싱)하고, 완료될 때까지 기다린다.
dehydrate
를 사용해 쿼리 캐시를 dehydrate하고, dehydratedState
prop으로 페이지에 넘겨준다.
(위의 _app.tsx
에서 앱 컴포넌트를 감싼 <Hydrate>
의 prop으로 전달된 pageProps.dehydratedState
🦄가 이것이다.)
pages/posts.jsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query' export async function getStaticProps() { const queryClient = new QueryClient() // 1. 새 QueryClient 인스턴스 만들기 await queryClient.prefetchQuery(['posts'], getPosts) // 2. 데이터를 prefetch(캐싱) return { props: { dehydratedState: dehydrate(queryClient), // 3. dehydrate한 캐시를 props로 페이지에 넘겨준다. }, } } function Posts() { // dehydrate - hydrate 사용 ✅ const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts }) // dehydrate - hydrate 사용 ❌ const { data: otherData } = useQuery({ queryKey: ['posts-2'], queryFn: getPosts, }) ... }
posts
쿼리
: dehydrate - hydrate 사용 ✅ (getStaticProps
에서 prefetch한 데이터와 같은 쿼리 키(posts
) 사용)
: 이 쿼리는 해당 페이지(Posts
)의 아무 자손 페이지에서나 해당 방식으로 사용될 수 있으며, 받아온 데이터는 즉시 사용될 수 있다.
posts-2
쿼리
: dehydrate - hydrate 사용 ❌ (다른 쿼리 키(posts-2
) 사용)
: 서버에서 쿼리를 미리 캐싱하지 않으며, 클라이언트에서 가져올 때까지 fetching 하지 않는다.
queryClient
(앱 전체의 queryClient)에 가져와서 쓰는 것도 가능하다. Ngether 상세 페이지 개요
- Next.js에서 각 요청 시마다 서로 다른 데이터를 받아오는 동적 페이지를 구현하기 위해 SSR 방식을 사용해야 한다.
- react-query를 사용해 여러 옵션들을 사용하고 페이지 상세 데이터의 캐시를 관리하려고 한다.
_app.tsx
... import { Hydrate, QueryClient, QueryClientProvider, } from '@tanstack/react-query'; ... export default function App({ Component, pageProps }: AppPropsWithLayout) { ... const queryClient = new QueryClient(); // 앱 전체 QueryClient 인스턴스 ... return ( <> <Head> <title>Ngether</title> </Head> <QueryClientProvider client={queryClient}> // queryClient 인스턴스 <Hydrate state={pageProps.dehydratedState}> // dehydratedState를 Hydrate <ReactQueryDevtools initialIsOpen={false} /> {renderWithLayout( <main className={notoSansKR.className}> <Component {...pageProps} /> // 컴포넌트 </main> )} </Hydrate> </QueryClientProvider> </> ); }
pages/nearby/[id].tsx
import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'; export async function getServerSideProps(context: any) { const { id } = context.params; const queryClient = new QueryClient(); // 새 QueryClient 인스턴스 만들기 await queryClient.prefetchQuery(['productDetail'], () => getProductDetail(id) ); // 데이터를 prefetch(캐싱) return { props: { id, dehydratedState: dehydrate(queryClient) } }; } // dehydrate한 데이터 캐시를 props로 페이지에 넘겨준다. ... export default function ProductDetail({ id }: productDetailType) { const { data } = useQuery(['productDetail'], () => getProductDetail(id)); // 같은 쿼리키(productDetail)로 데이터 받음
AxiosResponse
타입(Promise 객체)이어서 serializing error가 발생하는 경우,Server Error
Error: Error serializing.dehydratedState.queries[0].state.data.headers
returned fromgetServerSideProps
in "/nearby/[id]".
Reason:object
("[object AxiosHeaders]") cannot be serialized as JSON. Please only return JSON serializable data types.
api/detail.ts (productDetail 데이터 요청 함수)
export async function getProductDetail(id: string) { const res = await axios.get(`${REQUEST_URL}/api/boards/${id}`, { // axios가 처리될 때까지 기다려야 하므로 async-await 처리 headers: { Authorization: Cookies.get('access_token'), Refresh: Cookies.get('refresh_token'), }, }); return res.data; // res에서 data를 추출해서 리턴 }
pages/nearby/[id].tsx
export async function getServerSideProps(context: any) { const queryClient = new QueryClient(); const { id } = context.params; await queryClient.prefetchQuery(['productDetail'], () => getProductDetail(id) ); return { props: { id, dehydratedState: dehydrate(queryClient), }, }; } ... export default function ProductDetail({ id }: productDetailType) { const { data } = useQuery(['productDetail'], () => getProductDetail(id));
레퍼런스