Published on

Tanstack Query - SSR & NextJs

Authors
  • avatar
    Name
    piano cat
    Twitter

How-to Tanstack Query with NextJs 13

NextJs는 13 버전 이후 app 디렉토리로 변경된 프로젝트에서 클라이언트 컴포넌트로부터 데이터 fetch 할수 있는 도구로 Tanstack Query, SWR 라이브러리를 추천한다.

Tanstack Query는 *데이터 fetch, 캐싱, 동기화, 서버상태 업데이트 등 을 쉽게 만들수 있는 기능을 제공한다.

필수조건

  • NextJs 13 버전 이후 프로젝트를 준비한다.
  • Tanstack Query 라이브러리를 설치한다.
  • 클라이언트 컴포넌트를 준비한다.

설명

  1. prefetching 방법
  2. <QueryClientProvider>
  3. using initialData
  4. using <Hydrate>

1. prefetching 방법

NextJs 13 버전에 Tanstack Query를 사용한 데이터 prefetching 방법으로 두가지가 있다.

  • initialData
  • <Hydrate>

initialData

서버 컴포넌트에 데이터를 prefetching 하고 클라이언트 컴포넌트에 prop으로 drilling 하는 방식이다.

  • 빠른 셋업과 간편한 사용법이 특징이다.
  • 여러 계층의 클라이언트 컴포넌트로 인한 prop drilling이 필요 할 수도 있다.
  • 동일한 쿼리가 사용된 여러 계층의 클라이언트 컴포넌트에 prop drilling이 필요할 수도 있다.
  • 쿼리 refetching 은 데이터가 서버에서 프리페치된 시점이 아닌 페이지가 로드되는 시점을 기반으로 한다.

<Hydrate>

서버에서 쿼리를 prefetch 하고 캐시를 dehydrate 한 후, <Hydrate> 로 클라이언트에게 rehydrate 해준다.

  • 더 많은 셋업이 필요하다.
  • prop drilling이 필요하지 않다.
  • 쿼리 refetching 은 쿼리가 서버에서 prefetch 되었을 시점을 기반으로한다.

2. <QueryClientProvider>

기본설정으로 initialData, <Hydrate> 방식 둘다 기본설정으로 <QueryClientProvider> 설정을 해주어야한다.

아래와 같이 <QueryClientProvider> 로 나의 컴포넌트를 감싸고, QueryClient 인스턴스를 전달해야한다.

// app/providers.jsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function Providers({ children }) {
  const [queryClient] = React.useState(() => new QueryClient())

  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
// app/layout.jsx
import Providers from './providers'
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

3. initialData

클라이언트 컴포넌트 보다 상위컴포넌트인 서버 컴포넌트에 initialData 를 prefetch 하고 클라이언트 컴포넌트에 prop으로 전달하는 방법이다.

// app/page.jsx
export default async function Home() {
  const initialData = await getPosts()
  return <Posts posts={initialData} />
}
// app/posts.jsx
'use client'
import { useQuery } from '@tanstack/react-query'
export function Posts(props) {
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: getPosts,
    initialData: props.posts,
  })
  // ...
}
  1. <Hydrate>

request-scoped 싱글톤 인스턴스인 QueryClient를 만든다. 이렇게 하면 여러 사용자와 요청 간에 데이터가 공유되지 않고 요청당 한 번만 QueryClient를 생성할 수 있다.

// app/getQueryClient.jsx
import { QueryClient } from '@tanstack/react-query'
import { cache } from 'react'
const getQueryClient = cache(() => new QueryClient())
export default getQueryClient

pre-fetching 된 쿼리를 사용하는 클라이언트 구성 요소보다 구성 요소 트리에서 상위에 있는 서버 구성 요소에서 데이터를 가져온다. 미리 가져온 쿼리는 구성 요소 트리 아래의 모든 구성 요소에서 사용할 수 있다.

  • 싱글톤 인스턴스 QueryClient를 가져온다.
  • 클라이언트 prefetchQuery 메서드를 사용하여 데이터를 prefetch 하고 완료될때까지 기다린다.
  • dehydrate를 사용하여 쿼리 캐시에서 prefetch된 쿼리의 dehydrate 상태를 가져온다.
  • prefetched queries가 필요한 컴포넌트 트리를 내부에 래핑하여 dehydrated state를 제공한다.
  • 여러 서버 컴포넌트 내부에 fetch 할수 있고, 여러 곳에서 <Hydrate> 할 수 있다.

참고: TypeScript 버전이 5.1.3 보다 낮거나, @types/react 버전이 18.2.8보다 낮으면 비동기 서버 컴포넌트 요소를 사용할 때 타입 오류가 발생합니다. 임시 해결 방법으로 다른 컴포넌트 내부에서 이 컴포넌트를 호출할 때 {/* @ts-expect-error Server Component */}을(를) 사용하십시오.

// app/hydratedPosts.jsx
import { dehydrate, Hydrate } from '@tanstack/react-query'
import getQueryClient from './getQueryClient'
export default async function HydratedPosts() {
  const queryClient = getQueryClient()
  await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts })
  const dehydratedState = dehydrate(queryClient)

  return (
    <Hydrate state={dehydratedState}>
      <Posts />
    </Hydrate>
  )
}

서버에서 렌더링되는 동안 useQuery를 호출하여 감싸진 <Hydrate> 내부 클라이언트 컴포넌트는 prefetch 되고 상태 속성에 제공된 데이터에 접근 할 수있다.

// app/posts.jsx
'use client'
import { useQuery } from '@tanstack/react-query'
export default function Posts() {
  // This useQuery could just as well happen in some deeper child to
  // the "HydratedPosts"-component, data will be available immediately either way
  const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix
  const { data: otherData } = useQuery({
    queryKey: ['posts-2'],
    queryFn: getPosts,
  })
  // ...
}

위 예시로 data 와 otherData의 차이는 queryKey가 다르다는 점이다. 상위 서버 컴포넌트(app/hydratedPosts.jsx)에서 prefetchQuery로 queryKey와 일치한 data는 클라이언트에서 요청하기전에 미리 prefetch된 데이터를 바로 보여줄 것이다. (SEO에 장점!, 빠른 로딩)

하지만 otherData는 queryKey가 다르다. 일반 CSR로 구현한 리액트에서 서버에 데이터를 요청하고 받은것 처럼 응답을 받게 된다.