| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- template
- 5397
- red-black tree
- STL
- 구현
- Pair
- 문법
- Articulation_Point
- list
- deletion
- c++
- sstream
- connected_component
- Algorithm
- class_template
- data_structure
- Biconnected_Component
- qsort
- 총정리
- function_template
- 예제
- singly Linked List
- 13305
- '0'
- 알고리즘
- 백준
- sort
- 자료구조
- Heap
- Critical_Path_Analysis
- Today
- Total
- Today
- Total
- 방명록
어제의 나보다 성장한 오늘의 나
[Next.js] pre-fetch를 통한 초기 렌더링 최적화 본문
현재 서비스에는 현재 생성 상태를 확인하기 위해 주기적인 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)
두 번째 문제는 데이터 간의 의존성으로 인해 발생하는 워터폴 요청입니다.
목록 중 첫 번째 항목이 기본적으로 자동 선택되는 로직을 넣다 보니 목록을 가져온 후 상세 분석 데이터를 추가로 요청하게 됩니다.

서버에서 목록을 가져올 때, 목록의 첫 번째 항목에 대한 상세 데이터도 함께 미리 요청합니다.
이 데이터를 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에서 확인할 수 있습니다.

개선 된 결과

html의 응답이 이전 보다 좀 늦어졌습니다. 서버에서 프리패치를 진행하기 때문입니다.
그래서 덕분에 그 이후로 fetch 요청이 발생하지 않았습니다.
초기 로딩 문제와 워터풀 요청을 모두 해결한 것으로 확인하였습니다 :)
응답 시간도 개선 전 930ms -> 개선후 420 ms 로 약 55% 개선 되었습니다

'Next.js' 카테고리의 다른 글
| [Next.js] 데이터 요청 구조를 Server Action으로 통합해보기 (0) | 2025.07.21 |
|---|---|
| [Next.js] Middleware를 활용한 인증 처리 통합 (1) | 2025.07.06 |
| [Next.js] timezone 불일치로 인한 Hydration Failed 이슈 (0) | 2025.07.05 |
| [Next.js] 서버 컴포넌트에서의 에러 처리와 ErrorBoundary (2) | 2025.07.03 |
| [NextJS] Next.js 프로젝트에서 CSS 프레임 워크 결정 하기 (0) | 2024.03.25 |