React Query로 서버 사이드에서 GraphQL 쿼리 Prefetch 하기
Next.js
GraphQL
React Query
SSR
2024년 2월 27일
도입 배경 #
서버사이드에서 미리 데이터를 Prefetch 하게 되면 클라이언트에서는 Prefetch된 데이터를 캐시에서 가져온 후 화면에 보여주기 때문에 데이터를 로딩하느라 보여주는 로딩 화면을 보지 않아도 됩니다. 또한 로딩된 데이터가 HTML에 담겨서 오기 때문에 SEO에도 좋은 영향을 미칩니다.
Prefetch 과정 #
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'
export default async function PostsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: getPosts,
})
return (
// Neat! Serialization is now as easy as passing props.
// HydrationBoundary is a Client Component, so hydration will happen there.
<HydrationBoundary state={dehydrate(queryClient)}>
<Posts />
</HydrationBoundary>
)
}
위 코드는 React Query 문서 예제 코드입니다. 코드를 보면 Prefetch 과정은 두가지로 나눌 수 있습니다.
queryClient.prefetchQuery
를 사용해 prefetch를 수행한다.HydrationBoundary
컴포넌트의 prop으로 dehydrate된 queryClient를 state로 제공한다.
위 코드는 prefetch를 수행하는 하나의 컴포넌트로 추상화 가능해보입니다.
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
export default async function PrefetchedQueryHydrationBoundary({
children,
queryList,
}) {
const queryClient = new QueryClient()
// 모든 쿼리를 병렬로 prefetch함
await Promise.all(
queryList.map(({ queryKey, queryFn }) =>
queryClient.prefetchQuery({
queryKey,
queryFn,
}),
),
)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{children}
</HydrationBoundary>
)
}
import {
dehydrate,
HydrationBoundary,
QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'
import PrefetchedQueryHydrationBoundary from '../PrefetchedQueryHydrationBoundary'
export default async function PostsPage() {
return (
<PrefetchedQueryHydrationBoundary
queryList={[{ queryKey: ['posts'], queryFn: getPosts }]}
>
<Posts />
</PrefetchedQueryHydrationBoundary>
)
}
위와 같은식으로 PrefetchedQueryHydrationBoundary
라는 컴포넌트는 prop으로 미리 가져올 queryList
를 받아 prefetch 하고 그 값을 hydration하는 컴포넌트로 추상화가 가능합니다.
GraphQL을 위한 코드 변경 #
이제 GraphQL 쿼리를 위한 변경을 해보겠습니다.
우선 GraphQL 쿼리를 prefetch하는 함수를 작성해보겠습니다.
// param으로 client를 받고 해당 client로 prefetch를 수행하는 함수를 리턴한다.
// 타입은 편의상 생략됨.
function generatePrefetchGraphQLQuery({
graphQLClient: _graphQLClient,
queryClient,
}) {
return function prefetchGraphQLQuery({
document,
variables,
graphQLClient,
...rest
}) {
const queryKey = [document, variables]
return queryClient.prefetchQuery({
queryKey,
queryFn: async ({ queryKey }) =>
(graphQLClient ?? _graphQLClient).request(queryKey[0], queryKey[1]),
...rest,
})
}
}
위 함수는 prefetch를 수행하는 함수를 반환하는 고차 함수입니다. 반환된 함수는 항상 같은 client를 사용하여 prefetch를 하게 됩니다.
그리고 위 PrefetchedQueryHydrationBoundary
코드와 같이 작성하면 됩니다.
interface PrefetchGraphQLQuery<R> {
(
callback: (
options: PrefetchGraphQLQueryOptions<
TQueryFnData,
Variables,
TError,
TData
>,
) => R,
): R
}
export function prefetchGraphQLQuery<
TQueryFnData = unknown,
Variables extends DefaultVariables = DefaultVariables,
TError = DefaultError,
TData = TQueryFnData,
>(
options: PrefetchGraphQLQueryOptions<TQueryFnData, Variables, TError, TData>,
): PrefetchGraphQLQuery {
return (callback) => callback(options)
}
// prop으로 prefetch할 쿼리 배열을 받고 hydration 한다.
// 타입은 편의상 생략됨.
export async function PrefetchedGraphQLQueryHydrationBoundary({
graphQLClient,
prefetchQueryList,
children,
}) {
const queryClient = new QueryClient()
// props으로 받은 graphQLClient와 queryClient를 사용하는 prefetch 함수
const prefetchGraphQLQuery = generatePrefetchGraphQLQuery({
queryClient,
graphQLClient,
})
// prefetchQueryList의 값을 사용하여 prefetchGraphQLQuery를 진행한다.
await Promise.all(
prefetchQueryList.map((callback) => callback(prefetchGraphQLQuery)),
)
const dehydratedState = dehydrate(queryClient)
return (
<HydrationBoundary state={dehydratedState}>{children}</HydrationBoundary>
)
}
사용 예제 #
import { gql } from 'graphql-request'
export const COUNTRY = gql`
query Query {
country(code: "BR") {
name
native
capital
emoji
currency
languages {
code
name
}
}
}
`
'use client'
import { COUNTRY } from '@/gql/country'
import { useGraphQLQuery } from '@/util'
function Country() {
const { data, isLoading } = useGraphQLQuery({
document: COUNTRY,
variables: {
code: 'BR',
},
})
return (
<div>
<div>isLoading: {`${isLoading}`}</div>
<div>{JSON.stringify(data)}</div>
</div>
)
}
export default Country
'use client'
import Country from '@/components/Comp'
import { GraphQLClient } from 'graphql-request'
import {
PrefetchedGraphQLQueryHydrationBoundary,
prefetchGraphQLQuery,
} from '@/util/server'
import { COUNTRY } from '@/gql/country'
export default function Home() {
const graphQLClient = new GraphQLClient(
'https://countries.trevorblades.com/graphql',
)
return (
<PrefetchedGraphQLQueryHydrationBoundary
graphQLClient={graphQLClient}
prefetchQueryList={[
prefetchGraphQLQuery<CountryQuery, CountryQueryVariables>({
document: COUNTRY,
variables: {
code: 'BR',
},
}),
]}
>
<Country />
</PrefetchedGraphQLQueryHydrationBoundary>
)
}
이제 COUNTRY
쿼리는 서버 사이드에서 미리 가져오게 됩니다.