-
[Next.js13] react-query로 SSR 및 Hydrate 구현 해보기front-end/Next.js 2023. 8. 5. 00:47
개요
저는 개인적으로 진행 하던 Next.js 사이드 프로젝트에서 axios와 prisma client를 이용하여 비동기 통신을 주로 해왔었습니다.
요새 프론트엔드 개발 경력직 요구사항을 보면, react-query 요구 하는 회사가 꽤 많이 있는것 같았습니다.
과거에 간단한 CSR 리액트로 사이드프로젝트를 할때 react-query 를 이용해봤었는데요. react-query는 unique한 key값으로 컨트롤이 되며 , useQuery로 loading .error , data 가 기본으로 제공 됩니다., 비동기 통신할때 받아올 데이터의 data state , error state , loading state 등 useState로 하나씩 구현을 안해도 된다는 점이 굉장히 매력적(?) 이였고 또한 서버 상태 가져오기, 캐싱, 동기화 및 업데이트하는 작업도 지원을 해줍니다.
*이 외에도 react-query 의 강력함은 많습니다.
Next.js13 의 SSR
우선 Next.js 13은 default가 Server component 입니다. 그래서 useState 와 useEffect 등 리액트 hook을 사용하려면 Client component에서만 가능 합니다. default가 Server component이니까, 리액트 훅같은걸 사용하려면 'use client' 를 최상단에 선언 해주기만 해면 됩니다.
서버사이드 렌더링을 위해서 server component에서 data fetching 작업이 일어나고 , fetch해온 데이터를 클라이언트 컴포넌트로 props를 통해 전달 한 후 렌더링을 하면 됩니다. 아래 예제를 보시면 좀더 직관적이니 이해하기 쉬울 것 입니다.
// util GET API export default async function getData() { try { const res = await axios.get(process.env.API_URL as string) return res.data; } catch (error: any) { console.log(error); } }// Server Component export default async function LostDog() { const getLostDogListing: LostDogTypes[] = await getLostDogList(); return <LostDogClient lostGetDogList={getLostDogListing} />; }//Client Component 서버컴포넌트에서 전달 받은 props로 데이터 뿌리기 "use client"; interface DataI { data: Data[]; } export default function LostDogClient({ data }: DataI) { return ( <> {lostGetDogList.map((item) => ( <DataComponent title={item.title} name={item.id} age={item.age} /> ))} </> ); }대강 Next.js13의 서버사이드 렌더링 하는법을 알아봤는데요.
이제 react-query를 이용해서 서버사이드 렌더링을 하는법을 알아보겠습니다.
React-query 공식문서에 의하면 Next.js에서 SSR 이나 SSG를 하기 위한 두가지 방법이 있습니다.
initialData 와 Hydration
initialData
initialData 방법은 세팅이 비교적 간단하고 쉬운 편입니다. 우선 Next.js1 의 app 폴더 내에서 접근 가능한 방법입니다
이 방식은 데이터를 가져오는 것이 간단하고 직관적이지만, 데이터를 여러 곳에서 사용하거나 중첩된 컴포넌트에서 사용할 때 번거로울 수 있습니다. 하지만 여러 곳에서 같은 쿼리를 사용하는 경우, 모든 위치에 초기 데이터를 전달해야 하며, 데이터의 최신 상태를 유지하기 위해 수동으로 리페치를 관리해야 합니다
Hydration and Dehydration
간단히 말해, initialData 방식은 초기 데이터를 프롭으로 전달하는 방식으로 간단하지만, 데이터의 유지 및 관리가 복잡할 수 있습니다.hydration 방식은 React Query를 사용하여 서버와 클라이언트 간에 데이터를 효율적으로 관리하고 업데이트할 수 있도록 지원합니다.
저는 우선 효율 적인 Hydration 방법만 알아보겠습니다.
Hydration 방법을 알아보기전에 간단하게 용어 정리를 하자면,
Hydrate
Hydrate는 일반적으로 클라이언트 측에서 서버에서 렌더링한 마크업을 가져와서 이를 인터랙티브한 웹 애플리케이션으로 "복원"하는 과정을 의미합니다. 서버 측에서 생성된 HTML, 데이터 및 상태를 클라이언트 측 JavaScript와 병합하여 초기 애플리케이션 로드 시 기존 서버 렌더링된 컨텐츠를 재사용할 수 있도록 합니다. 이것은 페이지 로드 시 성능을 향상시키는 데 도움이 됩니다.
Dehydrate
Dehydrate는 React 애플리케이션의 현재 상태 및 데이터를 직렬화하여 클라이언트 측으로 전송하는 과정을 말합니다. 서버 측에서 React 컴포넌트의 상태 및 데이터를 클라이언트 측으로 보내고, 이 정보를 사용하여 클라이언트에서 렌더링된 마크업과 초기 데이터를 동기화하는 데 사용됩니다. 이를 통해 초기 데이터와 상태를 보존하고, 클라이언트에서는 데이터를 다시 가져오지 않고도 마크업을 복원할 수 있습니다.
Rehydrate
Rehydrate는 클라이언트 측 JavaScript가 초기화된 후, 클라이언트에서 서버에서 전달받은 데이터와 상태를 사용하여 애플리케이션 상태를 다시 "활성화"하는 과정을 의미합니다. 서버 측에서 전달받은 데이터와 클라이언트 측에서 가져온 상태를 조합하여 애플리케이션을 빠르게 인터랙티브하게 만듭니다. 이렇게 요약하면, Hydrate는 서버 렌더링된 마크업을 복원하여 초기 로딩 성능을 향상시키고, Dehydrate는 서버에서 클라이언트로 데이터를 보내 초기 데이터와 상태를 유지합니다. Rehydrate는 클라이언트 측에서 받은 데이터와 상태를 사용하여 애플리케이션을 초기화하고 인터랙티브하게 만드는 과정을 말합니다
접근 방식은 React Query가 Next.js의 서버에서 가져온 여러 쿼리를 사용하고, 해당 쿼리를 클라이언트 측에서 재사용할 수 있도록 지원합니다.
서버에서 페이지의 초기 렌더링을 할 때 필요한 데이터를 가져오고, 이 데이터를 React Query로 "hydration"합니다. 그런 다음 클라이언트 측에서는 해당 쿼리를 "dehydrate"하여 이전에 가져온 데이터를 사용하고, 필요한 경우 새로운 데이터를 가져와 "refetch"할 수 있습니다.
Create React Query Provider
// provider.tsx "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { useState } from "react"; type ProviderProps = { children: React.ReactNode; }; export default function Providers({ children }: ProviderProps) { const [queryClient] = useState(() => new QueryClient()); return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen /> </QueryClientProvider> ); }우선 React-query를 사용하기 위에선 Provider로 layout 전체를 래핑 해줘야합니다.
useState 같은 hook이 사용되기때문에 최상단에 "use client" 를 선언 해줍니다.
Wrapping layout(app) with Provider
// layout.tsx import Providers from './provider' export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body className={inter.className}> <Providers>{children}</Providers> </body> </html> ) }Provider로 감싸줍니다.
// app/getQueryClient.jsx import { QueryClient } from '@tanstack/react-query' import { cache } from 'react' const getQueryClient = cache(() => new QueryClient()) export default getQueryClient요청 범위의 싱글톤 인스턴스를 생성합니다. 이렇게 하면 데이터가 서로 다른 사용자와 요청 사이에서 공유되지 않지만, 여전히 각 요청 당 한 번만 QueryClient가 생성됩니다.
SSR 및 CSR 렌더링 확인
페이지 라우팅을 위해 이제 app 디렉토리에 test 라는 폴더를 만들어서 http://localhost:3000/test로 들어가서 data를 받아오는 모습을 보겠습니다. 그리도 또 테스트할 api 는 jsonplaceholder 을 이용해보겠습니다
// utils/getData.ts import axios from "axios"; export async function getData(){ const response = await axios.get('https://jsonplaceholder.typicode.com/posts/'); const data = await response.data; return data; }// app/test/page.tsx import { Hydrate, dehydrate } from "@tanstack/react-query"; import TestClient from "./TestClient"; import getQueryClient from "../getQueryClient"; import { getData } from "../utils/getData"; export default async function Test() { const queryClinet = getQueryClient(); await queryClinet.prefetchQuery(['posts'],getData); const dehydratedState = dehydrate(queryClinet); return ( <Hydrate state={dehydratedState}> <TestClient/> </Hydrate> ) }// app/test/TestClient.tsx "use client"; import { useQuery } from "@tanstack/react-query"; import { getData } from "../utils/getData"; export default function TestClient() { const { data } = useQuery({ queryKey: ["posts"], queryFn: getData }); //pre-fetch //비교를 위해 client side rendering 차이점은 , hydrate 로 된 ['post']키가 아님. const { data:clientData } = useQuery({ queryKey: ["postsClients"], queryFn: getData }) //none-pre-fetch; return ( <div style={{display:'flex'}}> <div> <h1>Server side render</h1> {data.map((item: any) => ( <div>{item.title}</div> ))} </div> <div> <h1>Client side render</h1> {clientData?.map((item: any) => ( <div>{item.title}</div> ))} </div> </div> ); }포스팅 맨위의 예제처럼 위 예시는 서버컴포넌트에서 pre-fetch를 합니다.
prefetchQuery를 통해 위 jsonplaceholder 데이터를 갖고오고 이 쿼리는 unique 한 키값의 ['posts']로 컨트롤 되고있습니다.
또 정확한 SSR과 CSR을 비교하기 위해 prefetchQuery 가 아닌 같은 데이터를 기본 react-query로 사용 해보겠습니다.
기본 react-query 는 쿼리의 키값을 ['postsClient'] 로 하겠습니다.1. 데이터 미리 가져오기: 이 클라이언트의 prefetchQuery 메서드를 사용하여 데이터를 미리 가져옵니다. 이때 가져오기 작업이 완료될 때까지 기다립니다.
2. dehydrate을 사용하여 미리 가져온 쿼리의 디하이드레이션 상태 얻기: 쿼리 캐시에서 미리 가져온 쿼리의 dehydrate 상태를 얻기 위해 dehydrate를 사용합니다.
3. prefetched queries가 필요한 컴포넌트 트리를 내부에 래핑하여 dehydrated state를 제공합니다.
같은 jsonplaceholder의 데이터를 갖고오는데 하나는 pre-fetch로 갖고온 ['post'] 키의 쿼리이고 나머지는 , Client에서 fetch를 해오는 ['postClient'] 키 입니다.

화면을 보면 왼쪽이 리액트 쿼리의 pre-fetch 와 hydrate 를통해 SSR이 적용된 것을 알 수 있고, 오른쪽은 SSR이 적용이 안된 CSR 방식으로 처리된 것을 알 수 있습니다.
더 확실한 비교를 위해서 postman을 이용해 SSR과 CSR을 비교 하겠습니다.
postman에 http://localhost:3000/test 를 입력해보면 pre-fetch 해온 SSR으 html에 데이터가 있는것을 볼 수 있고 , CSR에서는 초기 페이지 요청 시 서버에서 빈 HTML 페이지를 받아오고, 이후 JavaScript 코드를 사용하여 클라이언트에서 데이터를 가져와서 페이지를 동적으로 생성기때문에 텅빈 div 태그만 있습니다.
SSR

CSR

이번 과제를 통해 리액트쿼리를 이용해서 next.js에서 어떻게 SSR 을 구현하고 hydarte , rehydrate , dehydrate의 개념이 완벽히는 아니여도 한걸음 다가간거 같습니다.
정보가 잘못 되거나 더 좋은 방법 및 설명이 있으면 언제든지 댓글로 공유 부탁드립니다.
Reference
https://tanstack.com/query/v4/docs/react/guides/ssr#streaming-suspense-and-server-side-fetching
SSR | TanStack Query Docs
React Query supports two ways of prefetching data on the server and passing that to the queryClient. Prefetch the data yourself and pass it in as initialData
tanstack.com
'front-end > Next.js' 카테고리의 다른 글
[Next.js13] Next.js13에서 Styled-components 적용 이슈 (0) 2023.08.05 Incremental Static Regenerate 렌더링 (feat: SSR,SSG,CSR) (0) 2023.07.27 [Next.js] Next.js의 Hydrate 이란? (feat: SSR , CSR) (0) 2023.07.24 [Next.js 13] OAuth SNS Login 구현 ( github, google, naver ) (0) 2023.07.18 [Next.js 13] 1. 토이프로젝트 하면서 공부한 내용 정리 (Prisma) (0) 2023.06.25