Sangcho.log
dev

Next.js 16 — 알아서 해주던 것에서, 내가 지시하는 것으로

8 min read|

Next.js 16이 2025년 10월에 나왔고, 그 위에 16.2가 2026년 3월에 나왔습니다. 버전 노트식 나열 대신, 진짜 이해해야 하는 큰 개념 5개만 골라 비유로 정리합니다.

시작 전에 오늘 이야기의 뼈대를 한 문장으로 깔아두면 — Next.js 16의 변화는 대부분 "프레임워크가 알아서 해주던 걸 → 개발자가 직접 지시하도록" 바꾼 것입니다. 이 문장을 계속 머릿속에 두고 읽으면 됩니다.

개념 1. "동적이 기본, 캐싱은 내가 지시" (가장 중요)

예전 방식 (13~15): 뷔페 같았습니다. 식당(프레임워크)이 "이건 미리 만들어 놓으면 되겠지?" 하고 알아서 음식을 미리 조리(캐싱)해 뒀어요. 문제는, 손님이 "방금 만든 신선한 걸 원했는데?"라고 할 때 이미 식은 음식이 나오는 상황이 자주 생겼다는 겁니다. fetch 하나가 캐싱되는지 아닌지 예측이 안 돼서 다들 헷갈려 했죠.

새 방식 (16): 이제 주문 즉석 조리(동적)가 기본입니다. 캐싱하라고 명시하지 않는 한, 모든 동적 코드는 요청이 들어온 시점에 실행됩니다. 미리 만들어 두고 싶은 것만 개발자가 콕 집어서 말합니다. 그 말이 바로 use cache 한 줄입니다.

async function ProductInfo({ id }) {
  'use cache'   // ← 이 한 줄이 "이 결과는 캐시해도 돼"라는 지시
  const data = await getProduct(id)
  return <div>{data.name}</div>
}

예전엔 "왜 캐싱이 됐지/안 됐지?"를 추적하는 게 일이었는데, 이제는 코드에 use cache가 있으면 캐시, 없으면 실시간 — 눈으로 보이니까 예측 가능해집니다.

캐시를 지우는 개념도 세 가지로 정교해졌습니다.

  • revalidateTag → "이 태그, 다음 요청 때 새로 만들어" (지연 갱신)
  • updateTag → "지금 당장 새로고침해" (즉시 갱신)
  • refresh → 현재 화면 데이터만 다시 가져오기

주의할 점 하나 — cacheComponents: true를 켜면 "기본 동적"으로 동작이 바뀌므로, 기존 코드가 암묵적으로 기대던 캐싱 가정을 한 번 감사(audit)해야 합니다.

개념 2. PPR — "정적 껍데기 + 동적 구멍"

한 페이지를 상품 상세 페이지로 상상해 보세요. 페이지를 두 종류의 조각으로 나눕니다.

┌───────────────────────────────────────────┐
│  [헤더 / 네비게이션]          ← 잘 안 바뀜 → 캐시    │
├───────────────────────────────────────────┤
│  상품 이름, 설명, 대표 사진     ← 잘 안 바뀜 → 캐시    │
│                                           │
│  ┌─────────────────────────────────┐      │
│  │  재고 "3개 남음" / 실시간 할인가        │ ← 계속 바뀜 │
│  │  (처음엔 회색 스켈레톤 → 곧 채워짐)     │    → 동적   │
│  └─────────────────────────────────┘      │
│                                           │
│  [구매 버튼 / 푸터]           ← 잘 안 바뀜 → 캐시    │
└───────────────────────────────────────────┘

시간 순으로 무슨 일이 일어나냐면:

0ms   ─── 사용자가 페이지 열자마자
          캐시된 "껍데기"(헤더/상품정보/버튼)가 통째로 즉시 뜸.
          동적인 구멍 자리엔 회색 스켈레톤만 보임.

+few  ─── 서버가 실시간 데이터(재고/가격)만 계산해서
          그 구멍 자리로 흘려보냄(스트리밍).
          스켈레톤이 실제 값으로 슉 바뀜.

예전엔 "페이지 전체가 다 준비돼야 한 번에 보여주는" 방식이라, 실시간 재고 하나 때문에 잘 만들어 놓은 나머지까지 사용자를 기다리게 했습니다. PPR은 "먼저 보여줄 수 있는 건 먼저 보여주고, 오래 걸리는 조각만 나중에 끼워 넣는" 방식입니다. 코드로는 동적인 조각만 <Suspense>로 감싸주는 게 전부입니다.

export default function ProductPage({ params }) {
  return (
    <div>
      <ProductDetails id={params.id} />   {/* 캐시된 껍데기: 즉시 */}

      <Suspense fallback={<span>재고 확인 중...</span>}>
        <InventoryBadge id={params.id} /> {/* 동적 구멍: 나중에 스트리밍 */}
      </Suspense>
    </div>
  )
}

편집 도구에 대입하면 — 프로젝트 목록 페이지에서 레이아웃·제목·썸네일은 껍데기로 즉시 띄우고, "처리 진행률 87%" 같은 실시간 상태만 구멍으로 흘려보내는 그림입니다.

개념 3. middleware → proxy.ts (경비실 이야기)

웹 요청이 사무실 건물에 들어오는 방문객이라고 해봅시다. middleware/proxy는 로비의 경비실입니다. 방문객(요청)이 실제 사무실(페이지/API)에 도착하기 전에 신분증 확인(인증), 다른 층으로 안내(리다이렉트)를 하는 곳이죠. 바뀐 건 두 가지입니다.

1) 이름이 명확해졌다. middleware(중간에 낀 무언가)라는 애매한 이름을, "요청을 가로채서 처리·전달하는 곳"이라는 뜻의 proxy로 바꿨습니다. 파일 이름만 바꾸면 되고 codemod가 자동으로 해주며, 기존 middleware.ts도 당분간 동작합니다.

2) 경비실의 "권한"이 세졌다. 실무적으로는 이게 더 중요합니다.

[예전 middleware]  →  Edge Runtime이라는 제한된 환경
                      경비원이 "기본 장비"만 가짐.
                      Node.js 도구(fs, crypto 등) 사용 불가.

[새 proxy.ts]      →  Node.js 환경
                      경비원이 "풀 장비"를 가짐.
                      fs, crypto, 모든 npm 패키지 사용 가능.

그동안 "이 검증 로직은 무거워서 middleware에선 못 하겠다"며 페이지 안으로 미뤄뒀던 작업들을, 이제 관문 단계에서 처리할 수 있게 됐습니다.

개념 4. Turbopack이 왜 빠른가 (공장 비유)

번들러는 흩어진 부품(수백 개의 .ts, .tsx, .css 파일)을 모아 브라우저가 이해할 완제품으로 조립하는 공장입니다. 기존 공장(Webpack)은 부품 하나를 고쳐도 관련 라인을 통째로 다시 돌리는 경향이 있어서, 프로젝트가 커질수록 저장할 때마다 기다림이 길어졌죠.

Turbopack이 빠른 이유는 두 가지입니다.

  • Rust로 지어진 공장 — JavaScript보다 훨씬 빠른 언어라 조립 자체가 빠릅니다.
  • 바뀐 부품만 골라서 재조립(incremental) — 한 파일 고치면 그 파일과 영향받는 부분만 재작업합니다.

숫자로는 Fast Refresh 5~10배, 빌드 최대 5배, 16.2에선 next dev 시작 시간이 약 400% 개선됐습니다. 16.2에는 서버 컴포넌트 수정에도 전체 리로드 없이 갱신되는 Server Fast Refresh도 들어왔습니다. 커스텀 webpack 설정이 있다면 next build --webpack으로 되돌릴 수 있어 마이그레이션은 점진적으로 가능합니다.

개념 5. Hydration diff — "두 사람이 같은 그림을 그렸는데 다를 때"

Next.js는 화면을 두 번 그립니다. 서버가 먼저 HTML을 그려 보내고(빨리 보이게), 브라우저가 그 위에 JavaScript를 덧입혀 살아 움직이게 합니다 — 이게 hydration(물 붓기)입니다.

문제는 서버가 그린 그림과 브라우저가 그린 그림이 다를 때입니다. 대표 원인이 new Date(), Math.random()처럼 서버와 클라이언트에서 값이 달라지는 코드죠. 예전엔 "뭔가 안 맞는다"까지만 알려줬는데, 이제 에러 오버레이가 서버 내용은 - Server, 클라이언트 내용은 + Client로 라벨을 붙여 git diff 보듯 차이를 보여줍니다. 에러의 원인 체인(Error.cause)도 최대 5단계까지 펼쳐 줍니다.

그 외 챙겨둘 것들

  • Server Function 로그 — dev 터미널에 함수명·인자·실행 시간·정의 파일까지 표시
  • unstable_retry() — 기존 reset()이 못 살리던 데이터 페칭 실패까지 재요청하는 "Try again" 표준 구현
  • ImageResponse 고속화 — OG 이미지 생성이 기본 2배, 복잡한 이미지는 최대 20배 빨라짐
  • Devtools MCP + AGENTS.md — AI 에이전트가 브라우저 로그·프레임워크 상태를 직접 참조
  • next start --inspect — 프로덕션 서버에 Node 디버거를 붙여 CPU·메모리 프로파일링

업그레이드 시 주의점

Breaking change는 다섯 군데에 집중됩니다 — async params, middleware.ts→proxy.ts, next/image 일부 prop 제거(priority·onLoadingComplete 등), next lint 제거, AMP 제거. 다행히 npx @next/codemod@latest upgrade가 앞의 네 개를 자동 처리합니다. 14 이하 버전이라면 15로 먼저 옮긴 뒤(async API·fetch 캐싱 변경) 16으로 가는 2단계 마이그레이션이 안전합니다.


한 줄씩 압축하면 — ① 캐싱은 이제 내가 지시한다(use cache) → ② 그 지시로 "먼저 보여줄 것/나중에 채울 것"을 나눈다(PPR) → ③ 관문(proxy) 권한이 세졌다 → ④ 공장(Turbopack)이 빨라졌다 → ⑤ 버그 위치가 눈에 보인다(hydration diff). ①②는 "새로 배워야 할 개념"이고 ③④⑤는 "가만히 있어도 좋아지는 것"이니, 학습 시간은 ①②에 몰아주는 게 남는 장사입니다.

스스로 점검할 질문

  1. use cache를 안 붙이면 전부 동적이 기본인데, 그러면 예전보다 서버 부하나 응답 속도가 오히려 나빠지지 않나? 어떻게 균형을 잡을까?
  2. PPR에서 어떤 조각을 껍데기(캐시)로 두고 어떤 걸 구멍(동적)으로 둘지 판단하는 기준은 뭘까?
  3. hydration mismatch를 애초에 안 만들려면 코드 짤 때 어떤 패턴들을 습관적으로 피해야 할까?

참고 링크