~/web-app-dev.md
WEB_APP_DEV

Next.js Partial Prefetching은 “빠른 링크”가 아니라 App Shell 예산 설계다

10분 읽기·2026.07.24·출처 5·00
오늘의 질문

Next.js 앱에서 모든 링크를 더 많이 미리 받게 만드는 대신, 사용자가 클릭 직후 반드시 봐야 할 “껍데기”만 따로 설계할 수 있을까?

web-app-dev.md
App Shell budget

Next.js Partial Prefetching은 “빠른 링크”가 아니라 App Shell 예산 설계다

1. 왜 지금 봐야 하나

Next.js 16.3 Preview 흐름에서 눈에 띄는 변화는 Instant NavigationsPartial Prefetching이다. 공식 글은 서버 주도 앱이 SPA처럼 즉시 반응하려면, 링크 클릭 전에 “전체 페이지”를 무조건 미리 받는 대신 재사용 가능한 App Shell을 미리 만들고 클라이언트 캐시에 올리는 방향을 제시한다.

이 변화가 중요한 이유는 단순히 “Next.js navigation이 빨라졌다”가 아니다. 작은 팀이 App Router, Server Components, Cache Components, streaming을 쓰기 시작하면 다음 문제가 자주 생긴다.

  • 링크는 많고, prefetch={true}는 편하지만, 서버와 네트워크 비용이 보이지 않는다.
  • 동적 페이지는 사용자가 클릭하기 전부터 cookies, headers, params, searchParams 기반 데이터를 끌어오려 한다.
  • root layout의 Suspense만 믿었는데, 실제 client navigation에서는 shared layout 아래만 다시 렌더링되어 fallback이 보이지 않는다.
  • “빠른 화면”을 만들려다 stale 데이터, 과한 prefetch, 빈 skeleton, 권한 데이터 선로딩 문제가 섞인다.

Next.js 16.3 Preview 문서는 이 문제를 “더 많이 prefetch하라”가 아니라 Stream / Cache / Block 중 무엇을 선택할지 route별로 명시하라는 설계 문제로 바꾼다.

오늘의 핵심 문장: Partial Prefetching은 성능 옵션이 아니라, 클릭 직후 보여줄 App Shell의 범위와 비용을 정하는 제품 계약이다.

2. 핵심 개념

기존 prefetch 감각은 대체로 이랬다.

사용자가 클릭할 것 같은 링크의 다음 페이지를 미리 받아두면 빠르다.

하지만 App Router와 Server Components에서는 “다음 페이지”가 한 덩어리가 아니다. 한 route 안에도 여러 층이 있다.

예시클릭 직후 즉시 보여도 되는가
Shared layoutnav, tab, product list frame대체로 예
Static UI제목 영역, 카드 틀, 빈 상태 문구대체로 예
Cached content자주 안 바뀌는 상품명, 카테고리 메타데이터조건부 예
Fresh dynamic data재고, 권한별 알림, 결제 상태보통 navigation 이후 stream
Request-only datacookies(), headers(), 일부 searchParams경계 분리 필요

Partial Prefetching은 prefetch의 기본 단위를 complete page에서 App Shell로 낮춘다. 공식 reference changelog 설명에 따르면 App Shell은 shared layout, cached content, static UI, Suspense fallback을 포함할 수 있고, dynamic data는 사용자가 실제로 navigation할 때 가져온다. 또한 App Shell은 filesystem route마다 한 번 다운로드되어 같은 동적 route의 여러 URL에서 재사용될 수 있다.

예를 들어 /store/[slug]에서 /store/shoes, /store/hats, /store/bags가 모두 같은 route 구조를 공유한다면, 세 URL의 전체 데이터를 각각 미리 받는 대신 “상품 페이지 껍데기”를 한 번 받아 재사용하는 식이다. 사용자는 클릭 직후 즉시 shell을 보고, 실제 상품 정보나 재고는 cache 또는 stream 전략에 따라 채워진다.

여기서 개발자가 고를 수 있는 선택지는 세 가지다.

  1. Stream: 신선해야 하는 데이터는 Suspense fallback 뒤에서 stream한다.
  2. Cache: 재사용 가능한 async work는 'use cache'로 캐시하고 cacheLife 같은 정책을 붙인다.
  3. Block: 어떤 route는 일부러 서버 응답 전까지 이동하지 않도록 instant = false 또는 preview 문서의 unstable_instant = false 같은 opt-out을 둔다.

중요한 점은 “모든 페이지를 instant로 만들라”가 아니다. 어떤 route는 즉시 shell이 중요하고, 어떤 route는 정확한 완성 화면이 더 중요하다는 판단을 코드로 남기는 것이다.

3. 최신 이슈와 연결

공식 자료에서 확인한 최신 흐름은 네 갈래다.

첫째, Next.js 16.3 Preview는 cacheComponents: truepartialPrefetching: true를 함께 켜는 실험 경로를 제시한다.

zsh — 생존확인.sh
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
  partialPrefetching: true,
}

export default nextConfig

공식 글은 이 동작들이 future major release에서 기본값이 될 계획이라고 설명하지만, preview reference는 동시에 중요한 주의점을 남긴다. 현재 preview는 주로 dynamic application에 잘 맞고, static-heavy application은 현재 prefetch 방식보다 회귀가 있을 수 있다. 즉 지금 해야 할 일은 무작정 production 전체에 켜는 것이 아니라, 동적 route 한두 개에서 shell 경계를 실험하는 것이다.

둘째, preview instant navigation guide는 “Suspense가 어디에 있느냐”를 강조한다. page load에서는 root layout의 Suspense가 많은 async work를 잡아줄 수 있다. 하지만 client navigation에서는 source와 destination이 공유하는 layout 아래만 다시 렌더링된다. root boundary가 그 위에 있으면 해당 navigation에는 도움이 되지 않는다.

이 말은 실무적으로 크다. “첫 로딩은 skeleton이 잘 보이는데 링크 이동은 멈춘 것처럼 보인다”면, 데이터 fetch가 느린 것이 아니라 navigation entry point 기준으로 boundary가 잘못 놓였을 수 있다.

셋째, instant-link-prefetch-partial 문서는 prefetch={true}의 의미가 바뀌는 지점을 짚는다. Cache Components가 켜진 상태에서 legacy full prefetch는 App Shell뿐 아니라 dynamic data까지 가져올 수 있어 느리고 비싼 prefetch가 된다. Partial Prefetching을 도입하면 기본 Link는 App Shell을 prefetch하고 dynamic data는 navigation 이후로 미룬다.

넷째, GitHub의 v16.3.0-preview.7 release에는 instant validation blocking navigation 수정, repeated navigation lock 수정, use cache cache handler error 관련 수정, fetch cache key 정확성 개선 등이 들어 있다. Preview 기능이기 때문에 아직 다듬는 중이라는 신호다. 그래서 이 글의 결론도 “지금 바로 전면 도입”이 아니라 지금부터 route별 navigation 계약을 문서화하고 테스트할 준비를 하자에 가깝다.

4. 개발자 관점 해석

웹앱 개발자가 이 변화를 읽는 가장 실용적인 방법은 “성능 튜닝”보다 “UX 예산”이다.

사용자는 링크를 클릭했을 때 세 가지를 느낀다.

  1. 클릭이 먹혔는가?
  2. 내가 어디로 가고 있는지 알 수 있는가?
  3. 중요한 데이터가 언제 확정되는지 알 수 있는가?

Partial Prefetching의 App Shell은 1번과 2번을 해결하기 위한 최소 UI다. 재고, 알림 개수, 권한별 버튼 상태처럼 틀리면 안 되는 데이터는 3번 영역으로 빼서 stream하거나 명시적으로 block해야 한다.

예를 들어 쇼핑몰 상품 페이지라면 다음처럼 나눌 수 있다.

  • 즉시 shell: 상단 nav, 카테고리 breadcrumb, 상품 상세 레이아웃, 이미지 skeleton
  • cache 가능: 상품명, 기본 가격, 브랜드 설명처럼 자주 안 바뀌는 정보
  • stream 필요: 재고, 배송 가능일, 사용자별 쿠폰
  • block 후보: 결제 완료 확인, 본인 인증 결과, 권한이 필요한 admin mutation 화면

이 구분을 하지 않으면 prefetch={true}는 만능 버튼처럼 보인다. 하지만 실제로는 사용자가 보지도 않을 링크의 dynamic data를 미리 당겨오고, 서버 비용을 늘리고, 권한 데이터가 prefetch 경로를 타는지 검토해야 하는 상황을 만든다.

AI 코딩 에이전트를 쓰는 팀이라면 더 조심해야 한다. 에이전트는 “느린 navigation을 고쳐줘”라는 지시를 받으면 prefetch={true}를 더 붙이거나 Suspense를 위로 끌어올릴 수 있다. 하지만 좋은 수정은 다음 질문에 답해야 한다.

  • 이 route의 App Shell은 무엇인가?
  • 어떤 데이터는 'use cache'가 가능한가?
  • 어떤 데이터는 반드시 fresh해야 하는가?
  • client navigation entry point에서 fallback이 실제로 보이는가?
  • instant() Playwright helper나 Navigation Inspector로 회귀를 잡을 수 있는가?

즉 앞으로의 Next.js navigation 최적화는 “링크를 빠르게”가 아니라 shell, cache, stream, test를 같이 설계하는 일이 된다.

5. 내 프로젝트에 적용할 체크포인트

아래 체크리스트를 한 route에만 적용해도 충분하다. 전체 앱을 한 번에 바꾸지 말자.

Route 분류

  • 사용자가 자주 오가는 동적 route 1개를 고른다. 예: /products/[slug], /dashboard/[teamId], /docs/[slug]
  • 그 route에서 클릭 직후 100~300ms 안에 보여야 할 UI를 적는다.
  • 없어도 되는 dynamic data와 반드시 있어야 하는 dynamic data를 나눈다.
  • “정확한 완성 화면 전에는 이동하면 안 되는 route”는 block 후보로 표시한다.

App Shell 설계

  • shared layout, static UI, loading.tsx, fallback이 shell에 들어갈 수 있는지 확인한다.
  • 모든 async work가 root Suspense에만 의존하지 않는지 본다.
  • sibling route 사이 client navigation 기준으로 boundary가 보이는 위치에 있는지 확인한다.
  • dynamic params를 읽는 컴포넌트가 boundary 없이 await하지 않는지 확인한다.

Cache / Stream 판단

  • 자주 변하지 않는 데이터 fetch는 'use cache' 후보로 표시한다.
  • 사용자별 민감 데이터는 cache key와 권한 경계를 확인하기 전까지 cache하지 않는다.
  • 재고, 알림, 실시간 상태는 Suspense fallback 뒤에서 stream하는 후보로 둔다.
  • 기존 prefetch={true} 링크가 dynamic data까지 당기고 있지 않은지 점검한다.

검증

  • preview 기능은 production 전체 적용 전에 별도 브랜치에서 검증한다.
  • Navigation Inspector 또는 DevTools Instant Navs로 shell을 눈으로 본다.
  • @next/playwrightinstant() helper로 “클릭 직후 보여야 할 것”을 테스트한다.
  • AI 에이전트에게는 “prefetch를 늘리지 말고 shell/cache/stream/block 표를 먼저 작성하라”고 지시한다.

6. 오늘 10분 액션

오늘은 코드를 크게 바꾸지 말고, route 하나를 표로 분해해 보자.

zsh — 생존확인.sh
Route: /products/[slug]

1. 클릭 직후 즉시 보여야 할 App Shell
- nav
- breadcrumb
- product layout
- image skeleton

2. Cache 후보
- product name
- brand summary
- category metadata

3. Stream 후보
- inventory
- delivery ETA
- user coupon

4. Block 후보
- checkout confirmation
- admin price edit

5. 현재 위험
- root Suspense에만 fallback이 있음
- product card links에 prefetch={true}가 남아 있음
- sibling navigation에서 skeleton이 보이는지 테스트 없음

이 표를 만든 뒤에야 설정을 만져도 늦지 않다. cacheComponentspartialPrefetching은 기능 플래그이지만, 실제 작업은 route의 UX 계약을 정리하는 일이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Supabase Pipelines/CDC 분석 분리, Next.js 월간 보안 릴리스와 패치 윈도우, Next.js SWC WASM/lockfile 계약, React Router Web Streams 런타임 계약, TypeScript 7 빌드 계약, Next.js 16.3 에이전트 검증 루프, pg_graphql introspection, PostgREST JWT kid, Supabase Auth URL 라우팅을 다뤘다. 2026-07-12 글이 Next.js 16.3의 AI 에이전트 도구와 검증 루프를 중심으로 Instant Navigations를 일부 언급했고, 2026-06-15 글이 Cache Components를 데이터 신선도 계약으로 다뤘지만, 이번 글은 Partial Prefetching과 App Shell을 route별 UX 예산·prefetch 비용·client navigation entry point 설계로 분리해 다룬다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)