리액트에서 엄청 유용하고 매 프로젝트마다 사용했던 React-Query.
React-Query는 아직도 가장 핫한 라이브러리라고 생각한다. React-Query를 통해 get과 post 방식의 query를 손쉽게 사용할 수 있으면서 데이터를 손쉽게 cache 할 수 있다는 점에서 많은 기업들이 상태관리를 위해 도입하는 무척이나 매력적인 라이브러리라고 할 수 있다.
계속 강조하지만 Next.js를 유명하게 만든 가장 큰 요소 중 하나는 SSR이라고 생각하는데, 아쉽지만 React-Query의 hook의 경우 SSR이 아닌 CSR 방식으로 데이터를 가지고 온다.
이번 글에서는 React-Query의 데이터를 SSR로 사용할 수 있는 2가지 방식을 지원한다.
- 직접 데이터를 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 initialData
초기에 가장 먼저 생각했으며 지금도 많이 사용하고 있는 방법인 initialData를 사용하는 방법이다. 이 방법의 경우 무척 직관적인 방법으로 작업하는 프로젝트에서 SSR이 적은 경우 자주 사용하고 있다.
Next.js의 getStaticProps 나 getServerSideProps 함수를 통해 fetch한 데이터를 useQuery의initialData 옵션을 통해서도 넘겨줄 수 있다.
React Query의 관점에서 이들은 같은 방식이라 볼 수 있으며, getStaticProps는 아래와 같이 볼 수 있다.
// apis.ts
...
export const getFeeds = () => axios.get('<https://api.test.com/feeds>');
...
// pages/feeds.tsx
function Feeds(props) {
const { data } = useQuery(['feeds'], getFeeds, { initialData: props.feeds })
// ...
}
export async function getStaticProps() {
const feeds = await getFeeds()
return { props: { feeds } }
}
initialData를 활용하는 방식은 설정할 것이 적고 몇몇 케이스에서는 가장 빠른 솔루션일 수 있겠지만, 전체 접근 방식과 비교했을 때는 몇 가지 주의사항이 존재한다.
- 만일 useQuery 를 컴포넌트 트리 깊숙이 존재하는 컴포넌트에서 호출한다면, initialData를 그 지점까지 넘겨줘야 한다.
- 만일 useQuery 를 같은 query로 여러 위치에서 호출한다면, 호출한 지점 모두에 initialData를 넘겨줘야한다.
- 해당 query가 서버로부터 fetch된 정확한 시점을 알 수 있는 방법이 없어서, 페이지가 로드된 시점을 기반으로 dataUpdatedAt이나 refetch가 필요한 시점에 대한 것을 결정한다.
Using hydration
본격적으로 SSR이 중요하고 다양한 곳에서 사용되는 프로젝트의 경우 많이 사용하는 방법으로 한 번의 설정 뒤로는 간편하게 SSR을 사용할 수 있다는 장점이 있다.
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.tsx
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>
)
}
// apis.ts
export const getFeeds = () => axios.get('<https://api.test.com/feeds>');
// pages/feeds.tsx
const Test = (props) => {
// 이 useQuery는 Test 페이지에 대한 더 깊은 자식 요소에서 사용될 수도 있으며, data는 어느 쪽에서 사용되든
// 즉시 사용할 수 있다
const { data } = useQuery('feeds', getFeeds);
...
// 이 query는 서버에서 prefetch된 것이 아니며 클라이언트에서 시작할 때까지 fetch하지 않는다.
// 두 가지 패턴(서버에서 prefetch, 클라이언트에서 fetch)은 혼합될 수 있다.
const { data: otherData } = useQuery(['feeds-2'], getFeeds)
// ...
}
export async function getServerSideProps() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery('feeds', getFeeds);
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
- 각 페이지의 request 별로 새로운 QueryClient instance를 생성하면 서로 다른 사용자와 요청 간에 데이터가 공유되지 않는다.
- client의 prefetchQuery 메서드를 사용해 데이터를 prefetch해오고 완료되기까지 기다리자
- query cache를 dehydrate하기 위해 dehydrate 메서드를 사용하고 dehydratedState prop을 통해 이를 페이지에 넘겨주자.
- 이는 _app.tsx에서 불러온 cache와 동일한 prop이다.
일부 query들은 prefetch하고 다른 query들은 queryClient에서 fetch하도록 하는 것이 가능하다. 이 말인 즉슨, 특정 query에 대해 prefetchQuery를 추가하거나 제거하지 않고 어떤 컨텐츠를 서버가 렌더링할지 제어할 수 있다는 것이다.
_app.tsx에 SSR 설정을 한 번 해두면 다른 모든 페이지에서 위와 같은 방법으로 사용할 수 있다.
결론
결론적으로 정리하자면 스텝은 다음과 같다.
- prefetch 해서 queryClient 를 dehydrate 함
- 서버사이드 렌더링시 prefetch 할 때와 같은 key 를 가지는 query 를 만나면 캐싱된 데이터를 반환하고 이에 따라 렌더링을 진행함
- 서버사이드 렌더링을 진행한 html 파일을 클라이언트로 보내줌
- 클라이언트에서 hydrate 진행시 queryClient 에서 dehydrate 했던 데이터를 바탕으로 hydrate 함
출처 : 아래의 사이트들을 보면서 큰 공부 하였습니다
https://velog.io/@eomttt/React-Query-와-SSR#react-query-에서-nextjs-ssr
https://kjhg478.tistory.com/m/44
https://cheoltecho.tistory.com/m/18
https://seogeurim.tistory.com/19
https://kir93.tistory.com/m/entry/NextJS에서-react-query-SSR-데이터-사용하기
https://velog.io/@familyman80/Culture-Place제작기-3-Nextjs와-함께하는-React-Query-1-
'Front-End > Next' 카테고리의 다른 글
[Next.js] 정적 사이트에 다크 모드 구현하기 (0) | 2022.11.06 |
---|---|
[Next.js] Sentry.io로 프론트엔드 모니터링 하기 (0) | 2022.11.06 |
[Next.js] Pre-Rendering (0) | 2022.11.04 |
[NextJS] Hydrate? (0) | 2022.10.12 |
Next.js를 사용하는 이유 (0) | 2022.09.28 |