현재, Gloddy프로젝트에서 Tanstack Query를 이용하여 서버 상태를 관리하고 있다. 그리고, Gloddy 프로젝트의 웹은 Next.js 13 + App Routing을 사용하고 있다. App Routing에서는 기본적으로 모든 컴포넌트가 RSC이다.
<aside> 💡 RSC?
RSC는 React18에서 도입된 개념으로, 서버에서만 렌더링되는 컴포넌트이다. 서버에서만 렌더링되기에 onClick 같은 사용자와 상호작용하는 함수가 없어야 하며, 함수를 props로 넘겨줄 수도, 넘겨 받을 수도 없다(이는 RSC가 직렬화된 JSON형태로 변환되는 것과 연관이 있다).
RSC로 인한 장점에는 서버에서 브라우저에 넘겨주는 데이터가 Zero Bundle 사이즈라는 점, 스트리밍 방식으로 넘겨주기에 렌더링된 컴포넌트 먼저 보여줄 수 있다는 점, 서버나 DB에 접근할 수 있다는 점 등이 있다.
자세한 내용 : React Server Component
</aside>
RSC에서는 오로지 서버에서만 동작하기에, 클라이언트에서만 동작하는 훅을 사용하지 못한다. 이러한 경우 서버에서 데이터 prefetch를 어떻게 구현해야 할까?
참고 : https://tanstack.com/query/latest/docs/framework/react/guides/ssr
Page Routing에서는 SSR을 구현하기 위해서 getServerSideProps 메서드를 사용했다.
페이지 컴포넌트
export async function getServerSideProps() {
const posts = await getPosts()
return { props: { posts } }
}
function Posts(props) {
const { data } = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts,
})
// ...
}
페이지 컴포넌트에서 위와 같이 getServerSideProps내부에서 데이터를 패칭한다. 그리고, 이 데이터는 페이지 컴포넌트에 props로 전달하여 이 데이터를 useQuery의 initialData에 등록해준다. 그럼, 이 데이터는 캐싱이 되어 하위 컴포넌트에서, 혹은 다른 페이지에 들어갔을 때 캐싱된 데이터를 불러오게 된다.
다른 컴포넌트
function PostItem(){
const posts = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
}
1번 페이지 컴포넌트에서, 데이터를 캐싱해두었기에, 이 컴포넌트에서는 캐싱된 데이터를 활용할 것이다.
위와 같은
page컴포넌트
// pages/posts.jsx
import {
dehydrate,
HydrationBoundary,
QueryClient,
useQuery,
} from '@tanstack/react-query'
// This could also be getServerSideProps
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
export default function PostsRoute({ dehydratedState }) {
return (
<HydrationBoundary state={dehydratedState}>
<Posts />
</HydrationBoundary>
)
}
getStaticProps에서 데이터를 prefetch할 것이다. getStaticProps는 SSG, 즉 빌드타임 때 페이지를 미리 생성하는 것으로 위에서 SSR을 위해 사용했던 getServerSideProps 사용해도 무방하다.
QueryClient 인스턴스를 생성해서, QueryClient의 prefetchQuery 메서드를 활용해 데이터를 prefetch한다. 그리고 dehydrate한 상태로 HydrationBoundary컴포넌트를 이용하여 데이터를 뿌려준다.
<aside> 💡 dehydrate?
: 서버에서 가져온 데이터를 클라이언트로 전송할 수 있는 형태로 만든다.
dehydrate를 이용하여 dehydrate한 상태로 만들어, 클라이언트에 전송한다. 그리고 HydrationBoundary나 hydrate를 이용하여 클라이언트에서 데이터를 활용할 수 있도록 변환한다.
즉, Dehydrate는 수산을 빼는 행위로 이는 서버에서 클라이언트에서 전송하기 수월하기 위함이다. 그리고, Hydrate는 수분을 보충하는 것으로 클라이언트에 이제 데이터가 전송되었으니 데이터를 살리는 과정을 말한다.
</aside>
<aside> 💡 hydrate?
: dehydrate된 데이터를 캐싱된 데이터로 변환한다.
</aside>
<aside> 💡 HydrationBoundary?
: dehydrate된 데이터를 queryClient에 넣는다. 이 queryClient는 useQueryClient를 이용하여 가져와 사용할 수 있다.
</aside>
자식 컴포넌트
function Posts() {
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
const { data: commentsData } = useQuery({
queryKey: ['posts-comments'],
queryFn: getComments,
})
// ...
}
PostsRoute에서 캐싱한 데이터를 활용하게 된다.
SSR은 페이지 컴포넌트에서만 데이터를 서버에서 패칭할 수 있다. 그리고, props로 해당 데이터를 컴포넌트로 전달해주거나, 혹은 위에서 사용한 것처럼 useQuery의 initialData로 캐싱하거나, dehydrate 후 HydrationBoundary이 state로 넘겨주어 캐싱할 수 있다.
RSC는 서버 컴포넌트이다.