Next.js나 Remix 같은 프레임워크 내에서 React-Query를 사용한다면, 서버 렌더링 될 때 요청 후 응답받은 데이터를 SPA 방식으로 전환되고 나서도 유지할 수 있을까?? 어떻게 가능할까??
React Query의 hydrate
와 dehydrate
는 서버에서 미리 가져온 데이터를 클라이언트 사이드에서 재사용 할 수 있게 해준다. 이번 글을 통해 서버 렌더링과 어떻게 이를 가능하게 하는지 hydrate와 dehydrate에 대해서 알아봅시다.
서버 렌더링은 사용자가 페이지를 로드하는 즉시 볼 수 있는 초기 HTML을 서버에서 생성하는 행위입니다. 이는 페이지 요청 시 즉시 발생할 수 있으며(SSR), 이전 요청이 캐시 되었거나 빌드 시간에 미리 생성(SSG) 할 수도 있다.
클라이언트 렌더링 애플리케이션에서는 사용자에게 화면에 콘텐츠를 표시하기 전에 최소 3번의 서버 왕복
이 필요하다.
1. 빈 html을 받아온다.(markup)
2. JS(chunk download)
3. Query(fetch)
서버 사이드 렌더링 방식은 아래와 같이 줄어든다.
1. 내용이 담겨 있고 데이터가 초기화 되어있음(markup)
2. JS(chunk download)
1번이 완료되면 사용자는 콘텐츠를 볼 수 있고, 2번이 끝나면 페이지가 상호작용 가능하고 클릭할 수 있게 된다. 마크업에 필요한 초기 데이터가 포함되어 있기 때문에, 적어도 데이터를 다시 검증할 필요가 있을 때까지는 클라이언트에서 3번을 실행할 필요가 없다.
서버 렌더링을 통해 1번 과정에서 내용이 채워져 있고 data가 초기화되어있는 html을 생성하기 위해서는 마크업을 생성/렌더링 하기 전에 해당 데이터를 미리 가져와야(prefetch) 하며, 데이터를 직렬화 가능한(serializable) 형식으로 dehydrate 시켜 마크업에 포함(embed) 시키고, 클라이언트에서는 React Query 캐시로 해당 데이터를 hydrate 하여 새로운 fetch를 클라이언트에서 추가적으로 할 필요가 없도록 해야 한다.
리액트쿼리에서 initalData를 넣는 방식은 아래와 같다.
// Next.js 페이지 라우터 예시
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
}
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
위 코드에든 몇가지 문제점이 있다.
모든 위치에 initialData를 전달
해야 한다.페이지가 로드된 시점
에 따라 결정됩니다이러한 단점은 Hydration
을 사용하여 해결할 수 있다.
서버에서 쿼리를 prefetch(캐싱)하고 캐시를 dehydrate한 후 클라이언트에서 rehydrate하는 방법
React Query에서는 dehydrate
와 hydrate
함수를 제공하여 이 과정을 간소화합니다.
dehydrate
는 서버에서 React Query의 상태를 클라이언트로 전송할 수 있는 형태로 만들기 위해 사용된다. 서버에서 데이터를 가져온 후, 이 데이터를 직렬화(serialization) 하여 클라이언트로 전송합니다. 직렬화된 데이터는 DehydratedState 형태로 표현되며, 클라이언트 측에서 hydrate 함수
를 통해 다시 React Query 상태로 변환됩니다.
hydrate
는 클라이언트 측에서 직렬화된 상태를 받아 이를 React Query의 상태로 변환한다. 이 과정은 서버에서 미리 가져온 데이터를 클라이언트의 쿼리 캐시에 적용하여, 네트워크 요청 없이 데이터를 사용할 수 있게 한다.
해당 예시는 Next app routing(서버컴포넌트) 와 page routing 방식 둘 다 사용했습니다
Next page routing
방식에서는 getServerSideProps
와 같은 함수를 사용해 페이지 컴포넌트에서 props를 통해 주입받는다.
export async function getServerSideProps() {
const queryClient = new QueryClient()
const user = await queryClient.fetchQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
if (user?.userId) {
await queryClient.prefetchQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
})
}
return { props: { dehydratedState: dehydrate(queryClient) } }
}
App routing
방식은 서버컴포넌트에 아래와 같은 예시로 작성하면 된다.
export default async function HydratedFunc() {
const queryClient = getQueryClient();
// 데이터를 서버(서버컴포넌트)에서 불러옴 prefetch
await queryClient.prefetchQuery('KeyList', () =>
getList(),
);
const dehydratedState = dehydrate(queryClient);
return (
<Hydrate state={dehydratedState}>
<Component />
</Hydrate>
);
}
React Query의 hydrate
와 dehydrate
는 서버 사이드 렌더링(SSR) 혹은 서버컴포넌트를 구현하는 데 필수적인 도구입니다. 이들 기능은 서버에서 클라이언트로 데이터를 원활하게 전달하고, 애플리케이션의 성능을 최적화하는 데 중요한 역할을 합니다.
dehydrate
를 사용하여 서버에서 데이터를 추출하고, hydrate
를 통해 클라이언트에서 이를 재사용함으로써 네트워크 오버헤드를 줄이고, 서버와 클라이언트 간의 데이터 일관성을 유지할 수 있습니다. 이러한 접근 방식은 효율적이고 원활한 SSR 경험을 제공하며, 사용자에게 더욱 빠르고 매끄러운 인터페이스를 제공합니다.