
next14에서 클라이언트 컴포넌트에서 react query를 사용하는 경우, 서버 컴포넌트에서 데이터를 미리 가져온 뒤 클라이언트 컴포넌트에 전달하는 로직을 구현해보고자 한다.
App 디렉토리의 서버 컴포넌트에서 데이터를 미리 가져온 뒤 클라이언트 컴포넌트에 전달하는 방법은 총 2가지가 있다.
- props drilling 방식으로 pre-fetch
- hydrate 방식으로 pre-fetch
가장 먼저 클라이언트 컴포넌트로 ReactQueryProvider 컴포넌트를 만들어준다.
최상위 컴포넌트, app/layout 루트 레이아웃 컴포넌트의 body 태그에 들어가는 children 을 이 ReactQueryProvider 클라이언트 컴포넌트로 감싸주어야 한다.
'use client';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { PropsWithChildren } from 'react';
import React from 'react';
function ReactQueryProvider({ children }: PropsWithChildren) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
...옵션들
},
});
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default ReactQueryProvider;
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="ko">
<body>
<ReactQueryProvider>
<div className="flex w-full justify-center">
<div className="min-h-screen w-full max-w-md shadow-lg">{children}</div>
</div>
</ReactQueryProvider>
</body>
</html>
);
}
이렇게 해주면 Next14 프로젝트에 reactQuery 를 사용할 모든 준비는 끝났다. 그러면 방법 2가지를 활용해서 reactQuery를 활용해 Next의 SSR을 구현해볼 것이다.
구현이 무척 쉽다! 하지만 매번 자식 컴포넌트의 props 로 pre-fetch 한 데이터를 넘겨줘야하는 props drilling 이 생긴다는 단점을 가지고 있다.
아래는 간단한 예시 코드입니다.
서버 컴포넌트에서 클라이언트 컴포넌트로 props 를 전달해준다.
export default async function Home() {
const initialData = await getPosts()
return <Posts posts={initialData} />
}
그리고 useQuery option 인 initialData 에 props 로 받아온 데이터를 전달해준다.
'use client'
import { useQuery } from '@tanstack/react-query'
export function Posts(props) {
const {data} = useQuery({
queryKey: ['posts'],
queryFn: getPosts,
initialData: props.posts, // props 로 전달받은 값을 넣어줍니다.
})
// ...
}
간단하게 서버 컴포넌트에서 통신을 통해 받아온 데이터를 클라이언트 컴포넌트에 전달하는 코드를 구현할 수 있지만, 이보다 코드가 복잡해지는 경우 계속해서 prop으로 전달해주어야하는 단점이 발생하기에 BBOK 개발하면서는 이 방법이 아닌 다음 방법을 활용해 Next 의 SSR을 구현하였다!
이 방법은 props drilling 없이 미리 가져온 쿼리를 컴포넌트 트리 아래의 모든 컴포넌트에서 사용할 수 있다는 큰 장점을 가지고 있다.
이를 구현하기 위해서는 별도의 컴포넌트 파일을 만들어줘야 한다. 해당 컴포넌트 코드에 대해서 살펴보자
react-query 의 hydrate 요소를 클라이언트 컴포넌트로 래핑해서 클라이언트 컴포넌트에서 사용할 수 있도록 만들어준다.
'use client';
import { HydrationBoundary as HydrateOnClient } from '@tanstack/react-query';
export default HydrateOnClient;
미리 가져온 쿼리를 사용하는 클라이언트 보다 컴포넌트 트리에서 상위에 있는 서버 컴포넌트에서 데이터를 가져온다.
미리 가져온 쿼리는 컴포넌트 트리 아래의 모든 컴포넌트에서 사용할 수 있다.
동작을 간단히 살펴보면 다음과 같다.
dehydrate 를 사용하여 쿼리 캐시에서 pre-fetch 된 쿼리의 hydrate state 를 얻는다.import { QueryClient } from '@tanstack/query-core';
import type { QueryFunction, QueryKey } from '@tanstack/react-query';
import { dehydrate } from '@tanstack/react-query';
import { cache, type PropsWithChildren } from 'react';
import HydrateOnClient from './hydrate-on-client';
type Props = {
queryKey: QueryKey;
queryFn: QueryFunction;
};
const PrefetchHydration = async ({ queryKey, queryFn, children }: PropsWithChildren<Props>) => {
const getQueryClient = cache(() => new QueryClient());
const queryClient = getQueryClient();
await queryClient.prefetchQuery({
queryKey,
queryFn,
});
const dehydratedState = dehydrate(queryClient);
return <HydrateOnClient state={dehydratedState}>{children}</HydrateOnClient>;
};
export default PrefetchHydration;
그러면 prefetchHydration 컴포넌트로 감싸 안에 children에 해당하는 클라이언트 컴포넌트는 useQuery를 활용해 데이터를 get 해올 때 useQuery의 초기값으로 dehydratedState 상태를 가져오게 된다.
즉, 서버 렌더링 중에 hydrate 클라이언트 컴포넌트 내에 중첩된 useQuery 에 대한 호출은 상태 속성에 제공된 미리 가져온 데이터에 엑세스 할 수 있게 된다.
아래 화면은 BBOK 서비스에서 이전에 post 한 체크리스트를 api get 통신을 통해 5가지 항목을 보여주는 화면이다.

해당 페이지 코드는 다음과 같다. 앞에서 정의한 서버 컴포넌트에서 데이터를 미리 prefetch 하여 children에 해당하는 checklistCriteriaPage 클라이언트 컴포넌트에 데이터를 앞에서 정의한 컴포넌트를 활용하여 전달해준다.
이때 해당 페이지는 서버컴포넌트이기에 클라이언트 단에서 쿠키의 토큰을 접근해 통신하려고 하면 에러가 나기때문에 http 객체를 클라이언트, 서버 각각 구분해 코드를 구현해줘야 한다.
import memberServerApi from '@apis/member/member.server';
import { PrefetchHydration } from '@components/react-query';
import { MEMBER_KEYS } from '@constants/queryKeys';
import { ChecklistCriteriaPage } from '@features/checklist/components/detail';
const ChecklistDetailPage = () => {
return (
<PrefetchHydration queryKey={MEMBER_KEYS.lists()} queryFn={memberServerApi.getList}>
<ChecklistCriteriaPage />
</PrefetchHydration>
);
};
export default ChecklistDetailPage;
이제 클라이언트 컴포넌트에서는 어떻게 구현되었는지 살펴보고자 한다.
useGetMyChecklist 는 useQuery를 한번더 래핑해준 custom hook 이다. 이렇게 서버 렌더링 중에 hydrate 클라이너트 컴포넌트 내에 중첩된 useQuery에 대한 호출은 미리 가져온 pre-fetch 한 데이터에 엑세스해오는 것이다.
"use client"
const ChecklistCriteriaPage = () => {
const { data } = useGetMyChecklist();
return (
...
)
}
사전에 필요한 데이터를 서버에서 미리 가져와 클라이언트로 전달하는 방식을 활용하여 초기 페이지 로드 속도를 향상할 수 있었다.