Next.js와 React Query를 함께 사용하는 경우, 서버에서 데이터를 패칭하고 이를 초기 데이터로 클라이언트에 전달하는 방식이 매우 유용합니다. 이 글에서는 서버 컴포넌트를 활용해 데이터를 initialData로 설정하는 방법과 그 장점에 대해 설명하겠습니다.
현대 웹 애플리케이션은 빠른 초기 로딩과 SEO(검색 엔진 최적화)가 중요한데, 특히 콘텐츠 중심의 웹사이트는 사용자가 페이지에 들어왔을 때 콘텐츠가 곧바로 렌더링되는 것이 필수입니다. 이에 대한 해답 중 하나는 SSR(서버 사이드 렌더링)을 통해 서버에서 데이터를 미리 가져와 초기 상태로 설정하는 것입니다.
Next.js의 서버 컴포넌트를 활용하면, 페이지 로드 시 서버에서 데이터를 가져와 클라이언트에 전달할 수 있습니다. React Query의 initialData 속성은 이를 쉽게 활용할 수 있게 해주는 도구입니다.
아래 코드는 서버 컴포넌트에서 데이터를 패칭하여 initialData로 전달하는 방법을 보여줍니다.
// Page.tsx
export default async function Page() {
const articleList = await getCommunityArticleListWithSession();
const censoredWords = await getCensoredWords();
return (
<>
<Suspense fallback={<CommunityTabsSkeleton />}>
<CommunityTabs />
</Suspense>
<CommunityArticleList initialData={articleList} censoredWords={censoredWords} />
</>
);
}
// CommunityArticleList.tsx
'use client';
import { useInfiniteQuery } from 'react-query';
export default function CommunityArticleList({
initialData,
category,
productId,
query,
censoredWords,
}) {
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery(
['getCommunityArticleList', category, query, productId],
({ pageParam = 2 }) =>
getCommunityArticleList(
{ page: pageParam, ...(category && { category }), ...(productId && { product_id: productId }), ...(query && { search: query }) },
),
{
getNextPageParam: (lastPage, allPages) => (lastPage.next ? allPages.length + 1 : undefined),
initialData: { pages: [initialData], pageParams: [1] },
},
);
const handleInfiniteScroll = () => {
fetchNextPage();
};
return (
<div>
{/* 데이터를 활용하여 화면 렌더링 */}
<InfiniteScrollLoader
loading={isFetchingNextPage}
handleInfiniteScroll={handleInfiniteScroll}
skeletonType={SKELETON_TYPE.CARD_ARTICLE}
skeletonSize={10}
/>
</div>
);
}
'use client';
import { useEffect } from 'react';
import { useInView, IntersectionOptions } from 'react-intersection-observer';
import Loading, { SKELETON_TYPE } from '@/components/common/Loading';
interface InfiniteScrollLoaderProps {
loading: boolean;
handleInfiniteScroll: () => void;
skeletonType?: SKELETON_TYPE;
skeletonSize?: number;
}
const InfiniteScrollLoader: React.FC<InfiniteScrollLoaderProps> = ({
loading,
handleInfiniteScroll,
skeletonType,
skeletonSize,
}: InfiniteScrollLoaderProps) => {
const options: IntersectionOptions = {
threshold: 0.1,
};
const [loadingRef, inView] = useInView(options);
useEffect(() => {
if (inView && !loading) {
handleInfiniteScroll();
}
}, [inView, loading]);
return (
<>
{loading && <Loading type={skeletonType} size={skeletonSize} />}
<div ref={loadingRef}></div>
</>
);
};
export default InfiniteScrollLoader;
위 코드에서 서버 컴포넌트인 Page에서 초기 데이터를 패칭한 후, 클라이언트 컴포넌트인 CommunityArticleList에 initialData로 전달하고 있습니다.
서버 컴포넌트에서 데이터를 미리 가져오면, Next.js가 SSR을 통해 HTML을 생성할 때 초기 데이터를 HTML에 직접 포함시킬 수 있습니다. 이는 검색 엔진이 쉽게 인덱싱할 수 있는 구조를 만들어 SEO에 도움이 되며, 사용자가 첫 페이지를 로드할 때 기다리지 않고 즉시 콘텐츠를 볼 수 있어 사용자 경험이 크게 개선됩니다. 해당 예제에선 seo 를 포함하지 않지만 추후 포스팅 하겠습니다.
서버에서 초기 데이터를 미리 가져와 클라이언트에 전달하기 때문에, 클라이언트 측에서 추가적인 첫 번째 데이터를 요청할 필요가 없어 네트워크 비용이 절감됩니다. 네트워크 요청이 줄어드는 만큼 로딩 시간이 줄어들고, 서버와 클라이언트 간의 중복 요청을 방지할 수 있습니다.
React Query는 initialData로 초기 상태를 설정할 수 있어, 서버에서 전달된 데이터를 이용해 클라이언트에서 첫 로드 시 추가 요청 없이 바로 콘텐츠를 표시할 수 있습니다. 이후에는 React Query가 필요에 따라 데이터를 갱신하며, 무한 스크롤 등 추가 데이터를 로드할 때도 효율적으로 관리할 수 있습니다.
서버에서 가져온 초기 데이터를 initialData로 사용하면, 클라이언트 컴포넌트에서 별도의 상태 관리가 필요 없어집니다. React Query는 initialData로 설정된 데이터를 캐시에 저장하여 필요할 때 빠르게 재사용하고 갱신할 수 있으므로, 상태 관리가 간소화되고 유지보수가 쉬워집니다.
서버에서 초기 데이터를 제공하고 이후 필요한 경우에만 추가 데이터를 요청하는 방식은 무한 스크롤 같은 UX를 구현하기에 적합합니다. 예를 들어, 사용자가 스크롤을 내릴 때 추가 데이터를 로드하여 성능을 최적화할 수 있습니다.
Next.js 서버 컴포넌트와 React Query의 initialData 속성을 함께 사용하면, 데이터 로딩 성능과 SEO 최적화, 네트워크 효율성, 사용자 경험까지 개선할 수 있습니다. 이 접근 방식은 특히 초기 데이터가 중요한 콘텐츠 중심의 웹사이트에 적합하며, 서버 컴포넌트와 클라이언트 컴포넌트 간의 역할을 분명히 구분해 효율적인 구조를 제공합니다.