어제의 나보다 성장한 오늘의 나

[Next.js] pre-fetch를 통한 초기 렌더링 최적화 본문

Next.js

[Next.js] pre-fetch를 통한 초기 렌더링 최적화

today_me 2025. 7. 5. 15:04

현재 서비스에는 현재 생성 상태를 확인하기 위해 주기적인 polling을 하거나, 사용자의 상호작용에 따라 데이터를 요청해야 하는 경우가 종종 있습니다.

이러한 경우, 클라이언트 컴포넌트에서 여러 번 fetch를 트리거해야 하며, 이를 효율적으로 관리하기 위해 useQuery를 적극 활용하고 있습니다.

 

useQuery를 사용하는 이유는 다음과 같은 처리를 간단하게 해결할 수 있기 때문입니다

캐시 관리

폴링 처리

로딩 및 에러 상태 핸들링

 

이번 글에서는 이러한 요구사항을 가진 클라이언트 컴포넌트에서 데이터 fetch를 어떻게 최적화했는지에 대한 경험을 공유해보려고 합니다

기존  구조

기존 요청 구조

 

데이터 관리 페이지는 다음과 같은 두 개의 요청을 처리합니다.
1. 데이터 목록 조회
2. 선택된 항목의 상세 분석 결과 조회

사용자는 먼저 목록을 보고, 그 중 하나를 선택하면 분석 결과가 출력되는 구조입니다.

기본적으로 목록이 렌더링 되면 자동으로 첫 번째 데이터가 자동 선택되는 기능이 있습니다.
이 구조에서 다음과 같은 두 가지 성능 문제가 존재했습니다.

 

문제점 1: 초기 로딩 문제

사용자는 페이지 진입 직후 상당한 시간 동안 로딩 UI를 봐야 합니다.

 

로딩 예시 화면

 

문제의 원인은 클라이언트에서 렌더링 된 후 보내는 늦은 요청 때문입니다.

이를 해결하기 위해 데이터를 서버에서 미리 프리패치하여, 페이지 진입 시 즉시 콘텐츠가 렌더링되도록 개선했습니다.

 

app/data/page.tsx

export default async function Page() {
  
  const dataList = await extDataService.getDataList(); // pre-fetch
  
  return (
    <DataView dataList={dataList} /> // client component
  );
}

 

서버 컴포넌트에서 데이터를 먼저 가져와 클라이언트 컴포넌트에 props로 전달하는 구조입니다.

페이지 진입 시 데이터를 미리 프리패치함으로써, 클라이언트에서 별도 요청 없이 즉시 렌더링이 가능합니다.

문제점 2: 워터폴 요청 (Waterfall Requests)

두 번째 문제는 데이터 간의 의존성으로 인해 발생하는 워터폴 요청입니다.
목록 중 첫 번째 항목이 기본적으로 자동 선택되는 로직을 넣다 보니 목록을 가져온 후 상세 분석 데이터를 추가로 요청하게 됩니다.

 

get_series 워터폴 요청 발생


서버에서 목록을 가져올 때, 목록의 첫 번째 항목에 대한 상세 데이터도 함께 미리 요청합니다.
이 데이터를 React Query의 prefetchQuery로 캐시에 등록한 뒤,
dehydrate를 사용해 클라이언트로 직렬화해 전달합니다.

클라이언트에서는 useQuery로 동일한 key로 요청을 보내면,
이미 캐시에 존재하는 값이 즉시 사용되므로 별도의 fetch가 발생하지 않습니다.

app/data/page.tsx

export default async function Page() {

  const dataList = await dataService.getDataList();
  const firstItem = dataList[0];

  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: ["detailData", firstItem.id],
    queryFn: () =>
      fetchDetailData(firstItem.id),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <DataView dataList={dataList} defaultItem={firstItem} />
    </HydrationBoundary>
  );
}

 

app/components/client-component/data-view.tsx

'use client';

export default function DataView({
	dataList, 
	defaultItem
}){
	const [selectedItem, setSelectedItem] = useState(defaultItem)

	const { data } = useQuery({
		queryKey: ["detailData", selectedItem.id],
		queryFn: () => fetchDetailData(selectedItem.id),
		refetchOnWindowFocus: false,
	});
}

 

prefetchQuery 이후 캐시가 생성된 것을 devtools에서 확인할 수 있습니다.

 

react query devtools에서 캐시 조회

 

 

개선 된 결과

 

 

html의 응답이 이전 보다 좀 늦어졌습니다. 서버에서 프리패치를 진행하기 때문입니다.

그래서 덕분에 그 이후로 fetch 요청이 발생하지 않았습니다.

초기 로딩 문제와 워터풀 요청을 모두 해결한 것으로 확인하였습니다 :)

응답 시간도 개선 전 930ms -> 개선후 420 ms 로 약 55% 개선 되었습니다

 

개선된 시스템 구조

 

Comments