정의
로그인이 필요한 페이지에서, 화면이 뜨자마자 데이터가 이미 채워져 보이게 하고 싶을 때 쓰는 패턴이다. 브라우저가 열린 뒤에야 데이터를 요청하면 로딩 스피너가 한 박자 늦게 사라지는데, 그 데이터를 서버가 미리 받아 HTML에 실어 보내는 방식으로 없앤다.
풀어 쓰면: 서버에서 필요한 API를 미리 호출(prefetch)하고, 그 결과를 클라이언트 데이터 라이브러리(React Query·SWR 등 — 서버 데이터를 캐시·재요청 관리해주는 도구) 캐시에 담아 넘긴다. 이때 서버 캐시를 문자열로 직렬화해 HTML에 심는 것을 dehydrate, 브라우저가 그걸 되살려 캐시를 채우는 것을 hydrate라 한다. 그러면 첫 렌더부터 캐시에 데이터가 있어 스피너가 안 보인다. 단, 캐시를 찾는 열쇠인 query key(예: ["items", "page=1&size=50"] — 어떤 요청의 결과인지 구분하는 배열)가 서버와 클라이언트에서 완전히 같아야 캐시 적중(cache hit)이 된다.
왜 필요한가
로그인이 필요한 앱 홈에서 화면·주요 이미지가 API 결과에 묶이면, 브라우저만으로는 "먼저 세션을 확인하고 → 그다음 데이터를 받는" waterfall(요청이 앞뒤로 줄줄이 이어져 화면이 늦게 뜨는 현상)이 생긴다. 여기서 LCP(Largest Contentful Paint — 화면에서 가장 큰 요소가 그려지는 시각, 체감 로딩 속도 지표)가 그 데이터에 발목 잡힌다. 서버 로더에서 Promise.all([fetchSession(), fetchList(query)])로 병렬 호출하고 HTML 응답과 함께 캐시를 넘기면 로딩 지연을 줄인다. 반대로 query key가 어긋나면 prefetch가 무효가 되어, 늦은 스켈레톤(로딩 자리표시 UI)과 CLS(Cumulative Layout Shift — 뒤늦게 콘텐츠가 들어와 레이아웃이 밀리는 정도)가 다시 나타날 수 있다.
동작 원리
- Server-only fetch —
server-only모듈에 API 클라이언트 분리. - Shared query keys —
queryKeys.list(serializedParams)를 서버·클라이언트가 동일 import. - Loader — 인증 실패 시 redirect; 성공 시 prefetch + dehydrate.
- Client boundary —
HydrationBoundary로 캐시 주입, 하위useQuery가 즉시data보유. - Loading UI —
isPending && rows.length === 0일 때만 skeleton(cls-skeleton-layout-reservation).
클라이언트 TableProvider의 default page/pageSize/filter가 buildQueryString과 바이트 단위로 동일해야 한다.
실무 적용
- LCP 이미지가 API URL에 의존하면: 서버에서 auth + metadata(첫 이미지 URL)만 prefetch; 모든 바이트를 SSR에 넣을 필요는 없음(carousel-viewport-image-deferral).
- TTFB는 prefetch로 늘 수 있음 — 목표가 client waterfall 제거·load delay 축소인지 명확히 한다.
- Streaming SSR은 HTML flush 시점 축이다. LCP-critical을 뒤 Suspense로 미루면 FCP만 오르고 LCP는 그대로일 수 있다(streaming-ssr-metric-impact).
- E2E: storageState로 로그인 유지 후 trace 재현.
트레이드오프
| 선택 | 이득 | 비용 |
|---|---|---|
| Server parallel prefetch | 초기 data·LCP delay ↓ | TTFB·서버 작업 ↑ |
| Client-only fetch | 서버 단순 | waterfall·늦은 paint |
| Aggressive prefetch 전부 | cache hit 높음 | 불필요 데이터·직렬화 비용 |
사용하면 안 되는 경우
- 공개 정적 페이지(인증·개인화 없음) — CDN/SSG만으로 충분한 경우.
- query param이 클라이언트에서만 결정되고 서버가 알 수 없는 경우(키 정합 불가).
흔한 실수
- 서버
page=1&size=50, 클라이언트page_size=50등 직렬화 불일치. isLoading만 보고 hydration된data무시.- prefetch 추가 후 skeleton 설계 미조정 → CLS 회귀.
관련 개념
- ssr-hydration-mismatch — 서버 HTML vs 첫 렌더
- largest-contentful-paint — auth-gated LCP
- cls-skeleton-layout-reservation — prefetch + skeleton 세트
- carousel-viewport-image-deferral — 메타만 prefetch, 바이트는 defer
- streaming-ssr-metric-impact — streaming flush vs LCP 청크 배치