Next.js 환경의 Server Component(이하 RSC)에서 React Query를 서버 사이드로 사용하는 방법에 대해 서술하려합니다.
이 글에서는...
- RSC에서 React Query를 왜 사용하는지 설명합니다.
- hydration과 serialization, deserialization에 대해 설명합니다.
- query가
Server → Client
로 이동하는 구조와 로직(구현 코드)에 대해 설명합니다.
Next.js 환경에서 React Query를 사용하면서 이런 의문이 들 수 있습니다.
이와 관련된 의문에 대해서 하나하나씩 설명해보겠습니다.
Next.js도 캐싱을 해주지만, React Query가 캐싱하는 목적과 관리 방식 모두 다릅니다.
특성/기술 | Next.js | React Query |
---|---|---|
목적 | 서버 사이드에서 발생하는 다중 페칭 효율화 | 클라이언트 사이드에서 발생하는 다중 페칭 효율화 |
관리 방식 | 엔드포인트와 option을 기준으로 fetch 상태 관리 | queryKey로 fetch 상태 관리 |
Next.js에서는 서버 사이드와 관련된 다중 페칭을 효율화시키는 캐싱을 합니다.
100개의 동일한 RSC가 똑같은 요청을 보내는 경우
해당 캐싱이 매우 유용할 수 있습니다.
다음과 같은 구조가 있다고 가정해보겠습니다.
const ServerComponent = async () => {
await fetch(...);
return <ClientComponent />
}
"use client";
const ClientComponent = () => {
const { data } = useQuery(...);
}
이런 식으로 구현하는 경우, 만약 React Query가 서버 사이드에 개입하지 않으면
fetch
가 요청을 한 번, 클라이언트 사이드에서 useQuery
가 또 요청을 한 번 보내기에
비효율적인 구조가 탄생하게 될 수 있습니다.
따라서 클라이언트 사이드에 존재하는 React Query와 서버 사이드의 fetching을 연결해줄 필요가 있습니다.
그렇다면 서버 사이드에서 fetching 진행 후, 클라이언트 컴포넌트에 props로 값을 넘겨주는 등으로도
구성할 수 있는데, 왜 React Query가 필요할까요?
게시판 하나가 있다고 생각해보겠습니다.
해당 플로우 중 2 → 3으로 넘어갈 때에 React Query를 사용해서 fetch가 진행되어있다면
queryKey로 캐싱되어있을 것이기에 서버 사이드에 부가적인 요청을 보내지 않고도 페이지를
빠르게 제공할 수 있습니다.
Next.js 13부터는 여러 기능을 제공하게 되며 Next.js만 써도 충분히 효율적인 개발이 가능해졌습니다.
하지만 무한 스크롤같은 유틸리티한 기능들을 제공하는 것,
그리고 React Query가 제공하는 데이터 플로우 등을 고려해보았을 때
기호에 맞게 React Query를 사용하는 것도 나쁘지 않다고 생각합니다.
serialization이 뭔가요?
라는 FE 리드 분의 질문에... 직렬화를 평탄화로 해석하여 답변했었습니다...
serialization(직렬화)은 데이터를 통신하는 과정에서 파싱할 수 있도록 변환하는 과정입니다.
TCP/IP를 공부해보셨다면 아시겠지만, 읽기 쉬운 데이터가 바로 상대방에게 적용되는 것이 아니라,
데이터 스트림, 패킷, 전파 등 데이터가 변환되어 전송되게 됩니다.
이를 직렬화라고 하며, 반대로 이렇게 변환된 데이터를 읽을 수 있게 변경하는 것이 역직렬화입니다.
조금 어색할 수 있지만 Next.js에서도 직렬화와 역직렬화 개념이 사용됩니다.
바로 우리가 친근한 hydration이 역직렬화 기법 중 하나라고 볼 수 있습니다.
hydration이 무엇인가요?
정적 HTML을 React Component로 변경해 동적인 요소로 바꾸어주는 작업입니다.
서버 사이드에서 클라이언트 사이드로 데이터를 내려줄 때에도 직렬화가 필요합니다.
이를 dehydrate라고 표현합니다.
클라이언트 사이드에서 서버 사이드로 데이터를 받을 때에도 역직렬화가 필요합니다.
이를 hydration이라고 표현합니다.
바로 예제를 보며 사용해보겠습니다.
패키지 구조는 다음과 같습니다.
project/
├── src/
│ ├── app/
│ │ └── post/
│ │ ├── page.tsx # Server Component
│ │ └── PostClient.tsx # Client Component
│ │
│ └── getQueryClient.ts
/* getQueryClient.ts */
import { QueryClient } from "@tanstack/react-query";
import { cache } from "react";
export const getQueryClient = cache(() => new QueryClient());
React가 제공하는 cache를 사용하여 동일한 하나의 QueryClient를 사용하게 합니다.
(필수 사항이 아닙니다. 각 페이지에서 사용하실 때마다 new QueryClient를 해도 무관합니다.)
/* app/post/page.tsx */
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import getQueryClient from "./getQueryClient";
import PostClient from "./PostClient";
const CreatePostPage = async () => {
const queryClient = getQueryClient();
// 또는 const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryFn: fetchPost,
queryKey: ["post"]
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PostClient />
</HydrationBoundary>
)
}
/* app/post/PostClient.tsx */
"use client";
import { useQuery } from "@tanstack/react-query";
const PostClient = () => {
const { data } = useQuery({ queryKey: ["post"] });
return <> ... </>
};
prefetchQuery
는 따로 데이터를 반환하지 않으며, fetching된 데이터는 queryClient
에fetchQuery
메서드를 사용할 수 있습니다.post
데이터를 사용할 수 있습니다.hydration 과정은 개발자가 따로 지정하지 않고, dehydrate된 데이터를 받으면
자동으로 hydration이 됩니다.
'de'hydrate는 serialization, hydrate는 'de'serialization이라 헷갈릴 수 있습니다.
한 줄로 정리하면...
- serialization : 데이터를 보낼 때 진행하는 파싱, dehydrate. ex) JSON.stringify()
- deserialization : 데이터를 받을 때 진행하는 파싱, hydrate. ex) JSON.parse()
정상적으로 서버 사이드에서 data가 불러와지는지 확인해보고 싶으시다면 다음과 같이 확인하실 수 있습니다.
두 가지 테스트에서 모두 정상적으로 작동한다면 서버 사이드에서 성공적으로 데이터를 불러온다는 뜻입니다!
Next.js app router와 React Query v5 모두 출시된 지 오래된 라이브러리가 아니기에
공식 문서를 보며 꼼꼼히 공부해야 사용할 수 있었던 개념들을 정리하게 됐습니다.
Next.js와 React Query 모두 좋아하시는 분들, 그리고 어플리케이션이 특성상
서버 사이드 페칭을 진행해야 하는 경우 해당 방법을 추천드립니다.