Next.js app라우터와 React-Query를 같이 사용하는 사람들이 보면 좋을 글
Next.js 프로젝트에서 react-query를 세팅하면서 최신 문서로 작성된 SSR처리를 공부하고, 그 내용을 기록 & 공유한 글입니다.
서버 렌더링이 무엇일까요?
서버 렌더링은 서버에서 초기의 HTML을 생성하는 행위이고, 그래서 유저가 페이지가 로드 되자마자 어떠한 컨텐츠를 볼 수 있는 것을 의미합니다.
서버사이드 렌더링은 페이지가 SSR을 요구할 때 또는, 빌드 타임에도 일어날 수 있습니다.(SSG)
1. |-> Markup (without content)
2. |-> JS
3. |-> Query
위와 같이 CSR방식이면, 페이지의 컨텐츠가 전부 보여지기 전 최소 3번 이상의 서버 통신이 일어납니다.
반면, SSR 방식은 아래와 같이 일어납니다.
1. |-> Markup (with content AND initial data)
2. |-> JS
첫째로, 서버에서 렌더링된 컨텐츠가 보여져서 유저는 컨텐츠를 볼 수 있습니다.
둘째로, 페이지가 인터렉티브하게 변합니다. 첫 번째로 우리가 필요한 초기 데이터를 포함하고 있기 때문에, 데이터를 revalidate 할 필요가 없는 이상 3번 과정은 없어도 됩니다.
서버 사이드 렌더링을 수행하기 위해 서버 쪽에서는,
데이터가 포함된 페이지를 만들기 위해 렌더링하기 전에 데이터를 prefetch해야 합니다.
(QueryClient.prefetch
)
그리고 prefetch한 데이터를 dehydrate해서 직렬화할 수 있는 포맷으로 만들어야 합니다.
리액트 쿼리를 Next.js에 적용할 수 있는 방법은 크게 두 가지가 안정적인 버전으로 제공됩니다.
(저는 그 중에서 프리패치 방식을 선호합니다 데이터를 별도 Props로 안넘겨줘도 되기 때문에)
서버 ≠ 서버 컴포넌트
클라이언트 ≠ 클라이언트 컴포넌트
서버 컴포넌트는 서버에서만 돌아감,
클라이언트 컴포넌트는 둘 다에서 돌아갑니다.(인터렉션, 훅 제외)
// In Next.js, this file would be called: app/providers.jsx
'use client'
// We can not useState or useRef in a server component, which is why we are
// extracting this part out into it's own file with 'use client' on top
import { useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// ServerSide
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (typeof window === 'undefined') {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Providers({ children }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
최신 버전의 공식 문서 예제에서는 데이터를 가져오는 각 서버 컴포넌트마다 새로운 queryClient를 생성합니다.
이렇게 React의 cache API를 사용해 싱글톤 인스턴스로 만들 수는 있지만, 장단점이 존재합니다.
// app/getQueryClient.jsx
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'
const getQueryClient = cache(() => new QueryClient())
export default getQueryClient
위의 방식의 장점은, 서버 컴포넌트에서 호출되는 어디에서나 getQueryClient()를 호출하여 이 클라이언트를 사용할 수 있다는 것입니다.
단점은 dehydrate(getQueryClient())를 호출할 때마다 queryClient 전체를 직렬화하는데, 이는 이미 직렬화된 쿼리를 포함하고 현재 서버 컴포넌트와 관련이 없는 불필요한 오버헤드가 될 수 있습니다.
Next.js는 이미 fetch()를 사용하는 요청을 중복 제거하지만, queryFn에서 다른 것을 사용하거나 자동으로 이러한 요청을 중복 제거하지 않는 프레임워크를 사용하는 경우 위에서 설명한 대로 단일 queryClient를 사용하는 것이 의미가 있을 수 있습니다.
이 내용에 대해서는 처음엔 잘 이해가 가지 않았지만, QueryClient를 매번 생성한다 -> 매번 새로운 요청을 보낸다 -> Next.js는 fetch 요청을 중복 제거한다 -> 매번 새로운 QueryClient를 생성해도 무리가 없다. 는 내용으로 받아들였습니다.
별개로, react-query docs별로 내용이 다른게 많기 때문에, 꼭 URL을 확인해서 최신 버전 혹은 자기가 사용하고 있는 버전의 docs가 맞는지 확인해야 할 것 같습니다😅
참고한 글 & 더 알아보면 좋을 글
next.js와 react-query : https://github.com/wpcodevo/nextjs13-react-query
아직 리액트 쿼리를 사용하는 이유 : https://www.youtube.com/watch?v=9kjc6SWxBIA
react-query ssr 설정법 :
https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#server-components--nextjs-app-router
https://tanstack.com/query/latest/docs/framework/react/guides/ssr#using-the-hydration-apis