Next.js와 React Query의 initialData를 활용한 서버 데이터 패칭 및 초기 로딩 최적화 , 무한 스크롤 구현
Next.js와 React Query를 함께 사용하는 경우, 서버에서 데이터를 패칭하고 이를 초기 데이터로 클라이언트에 전달하는 방식이 매우 유용합니다. 이 글에서는 서버 컴포넌트를 활용해 데이터를 initialData로 설정하는 방법과 그 장점에 대해 설명하겠습니다.
1. 서버 컴포넌트와 초기 데이터 설정이 필요한 이유
현대 웹 애플리케이션은 빠른 초기 로딩과 SEO(검색 엔진 최적화)가 중요한데, 특히 콘텐츠 중심의 웹사이트는 사용자가 페이지에 들어왔을 때 콘텐츠가 곧바로 렌더링되는 것이 필수입니다. 이에 대한 해답 중 하나는 SSR(서버 사이드 렌더링)을 통해 서버에서 데이터를 미리 가져와 초기 상태로 설정하는 것입니다.
Next.js의 서버 컴포넌트를 활용하면, 페이지 로드 시 서버에서 데이터를 가져와 클라이언트에 전달할 수 있습니다. React Query의 initialData 속성은 이를 쉽게 활용할 수 있게 해주는 도구입니다.
2. 예제 코드
아래 코드는 서버 컴포넌트에서 데이터를 패칭하여 initialData로 전달하는 방법을 보여줍니다.
Page.tsx (서버 컴포넌트)
// 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 (클라이언트 컴포넌트)
// 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> ); }
InfiniteScrollLoader.tsx (클라이언트 컴포넌트)
'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로 전달하고 있습니다.
3. 서버 컴포넌트와 initialData를 사용하는 장점
1) SEO 최적화와 빠른 초기 로딩
서버 컴포넌트에서 데이터를 미리 가져오면, Next.js가 SSR을 통해 HTML을 생성할 때 초기 데이터를 HTML에 직접 포함시킬 수 있습니다. 이는 검색 엔진이 쉽게 인덱싱할 수 있는 구조를 만들어 SEO에 도움이 되며, 사용자가 첫 페이지를 로드할 때 기다리지 않고 즉시 콘텐츠를 볼 수 있어 사용자 경험이 크게 개선됩니다. 해당 예제에선 seo 를 포함하지 않지만 추후 포스팅 하겠습니다.
2) 클라이언트-서버 간 네트워크 요청 감소
서버에서 초기 데이터를 미리 가져와 클라이언트에 전달하기 때문에, 클라이언트 측에서 추가적인 첫 번째 데이터를 요청할 필요가 없어 네트워크 비용이 절감됩니다. 네트워크 요청이 줄어드는 만큼 로딩 시간이 줄어들고, 서버와 클라이언트 간의 중복 요청을 방지할 수 있습니다.
3) React Query와의 자연스러운 통합
React Query는 initialData로 초기 상태를 설정할 수 있어, 서버에서 전달된 데이터를 이용해 클라이언트에서 첫 로드 시 추가 요청 없이 바로 콘텐츠를 표시할 수 있습니다. 이후에는 React Query가 필요에 따라 데이터를 갱신하며, 무한 스크롤 등 추가 데이터를 로드할 때도 효율적으로 관리할 수 있습니다.
4) 클라이언트 상태 관리의 간소화
서버에서 가져온 초기 데이터를 initialData로 사용하면, 클라이언트 컴포넌트에서 별도의 상태 관리가 필요 없어집니다. React Query는 initialData로 설정된 데이터를 캐시에 저장하여 필요할 때 빠르게 재사용하고 갱신할 수 있으므로, 상태 관리가 간소화되고 유지보수가 쉬워집니다.
5) 점진적 데이터 로딩 가능
서버에서 초기 데이터를 제공하고 이후 필요한 경우에만 추가 데이터를 요청하는 방식은 무한 스크롤 같은 UX를 구현하기에 적합합니다. 예를 들어, 사용자가 스크롤을 내릴 때 추가 데이터를 로드하여 성능을 최적화할 수 있습니다.
4. 결론
Next.js 서버 컴포넌트와 React Query의 initialData 속성을 함께 사용하면, 데이터 로딩 성능과 SEO 최적화, 네트워크 효율성, 사용자 경험까지 개선할 수 있습니다. 이 접근 방식은 특히 초기 데이터가 중요한 콘텐츠 중심의 웹사이트에 적합하며, 서버 컴포넌트와 클라이언트 컴포넌트 간의 역할을 분명히 구분해 효율적인 구조를 제공합니다.