버전 스큐(Version Skew)는 새 버전을 배포한 뒤에도 브라우저에 떠 있는 이전 빌드의 클라이언트가 새 서버와 통신하면서 생기는 불일치다. 대표 증상은 이전 HTML이 참조하던 JS 청크를 늦게 불러오려다 404가 나는 ChunkLoadError다.
왜 생기나
번들러는 청크 파일 이름에 내용 해시를 붙인다(chunks/a1b2c3.js). 빌드가 바뀌면 이름도 바뀐다.
사용자가 이전 빌드의 HTML을 받아 둔 사이에 배포로 트래픽이 새 빌드 서버로 넘어가면, 나중에 지연 로딩하는 이전 해시 청크 요청이 새 서버로 가서 404가 난다.
sequenceDiagram participant B as 브라우저 participant LB as 로드밸런서 participant O as 이전 빌드 서버 participant N as 새 빌드 서버 B->>LB: 페이지 요청 LB->>O: 전달 O-->>B: 이전 빌드 HTML (이전 해시 청크 참조) Note over LB,N: 배포 - 트래픽을 새 빌드 서버로 넘김 (블루-그린 · 롤링) B->>LB: 다른 화면에서 이전 해시 청크 지연 로딩 LB->>N: 전달 N-->>B: 404 (.next/static에 이전 해시 파일 없음)
정적 자산을 앱 서버가 직접 서빙하면, 지금 트래픽을 받는 빌드의 파일만 존재하는 구조라서 생긴다. Next.js 문서는 같은 원인으로 서버 함수 ID 불일치와 prefetch 데이터 비호환도 꼽는다.
assetPrefix로 정적 자산 떼어 내기
assetPrefix를 설정하면 /_next/static/...(.next/static 폴더의 JS·CSS) 참조 URL 앞에 CDN 호스트가 붙는다. URL은 빌드 때 HTML·JS에 박히므로 실행 서버의 환경 변수가 아니라 빌드 단계에 값을 넣어야 한다. public/ 폴더 파일은 바뀌지 않는다.
before /_next/static/chunks/X.js → 앱 서버
after https://cdn.example.com/_next/static/chunks/X.js → CDN·오브젝트 스토리지이렇게 정적 파일 서빙을 앱 서버에서 떼어 CDN에 넘기는 것을 오프로드(offload)라고 한다. 효과는 두 가지다.
- 스큐 404 해소: 빌드마다
.next/static을 같은 버킷에 지우지 않고 누적 업로드한다. 파일 이름이 내용 해시라 이전 빌드와 새 빌드 파일이 충돌 없이 공존한다. 이전 HTML이 이전 청크를 요청해도 버킷에 남아 있어서 200이 난다. 동기화 도구의 삭제 옵션으로 이전 파일을 지우면 404가 다시 생긴다. 오래된 파일은 수명 주기 규칙으로 정리한다 - 앱 서버 부하 감소: 페이지 하나에 청크·CSS·폰트 요청이 수십~수백 개씩 몰린다. 이 요청이 노드 프로세스를 거치지 않으니, 재시작 직후의 콜드 서버도 렌더에만 집중한다
업로드 대상은 .next/static뿐이다. .next의 나머지(서버 코드·설정)는 공개하면 안 된다. standalone 출력이 .next/static을 기본으로 빼는 것도 이 구성을 전제로 하기 때문이다.
deploymentId: 감지와 새로고침
deploymentId(또는 NEXT_DEPLOYMENT_ID)를 주면 Next.js는 정적 자산 URL에 ?dpl=<id>를 붙이고, 클라이언트 내비게이션 요청과 응답 헤더로 버전을 주고받는다. 서버와 클라이언트의 ID가 다르면 클라이언트 내비게이션 대신 전체 새로고침을 해서 새 버전으로 맞춘다(Next.js 16 기준).
?dpl=은 캐시 무효화용이다. Next.js 서버는 이 값을 보고 이전 버전 파일을 찾아 주지 않는다- 그래서 deploymentId는 "감지하고 복구 동작을 트리거"하는 장치이고, 이전 청크를 실제로 내주는 것은 누적 저장소다. 둘을 같이 써야 한다. 저장소가 없으면 새로고침 전 지연 로딩에서는 여전히 404가 날 수 있다
- 여러 인스턴스를 띄우면 같은 배포의 인스턴스는 모두 같은 ID를 써야 한다
- 새로고침되면
useState같은 컴포넌트 상태는 사라진다. URL이나 웹 스토리지에 둔 상태만 남는다
불변 정적 자산(Immutable Static Assets)
?dpl=은 배포마다 바뀌니, 내용이 그대로인 청크도 배포할 때마다 브라우저가 다시 받는다. Next.js 16.3은 어댑터가 지원하면 내용 해시로 이름 붙인 자산을 /_next/static/immutable/* 아래에 내보내고 ?dpl= 없이 요청한다. 이 파일들은 배포끼리 공유하는 이름 공간에 살아서, 바뀌지 않은 청크는 브라우저 캐시와 업로드를 배포를 넘어 재사용한다. 대신 그 파일을 쓰는 배포가 살아 있는 동안 지우거나 덮어쓰면 안 된다. 위의 "누적 저장소"를 프레임워크가 공식화한 셈이다.
- 켜는 쪽은 어댑터(
modifyConfig에서supportsImmutableAssets)다. 앱에서는supportsImmutableAssets: false로 끄는 용도이고, 지원하지 않는 환경에서 켜면 배포가 깨질 수 있다고 문서가 경고한다 public/파일 같은 나머지 정적 자산은 계속?dpl=이 붙는 배포 단위 자산이다
플랫폼의 Skew Protection
Vercel의 Skew Protection은 한 걸음 더 나아가 버전 고정(version locking)을 한다. 프레임워크가 정적 자산·내비게이션·prefetch 요청에 배포 ID를 붙이면, 플랫폼이 그 요청을 처음 페이지를 준 배포로 보낸다. 이전 배포를 통째로 살려 두는 방식이라 자산뿐 아니라 서버 함수도 맞는다. 기본 보존 기간은 배포 생성 후 1일이고, Pro·Enterprise 플랜 기능이다(2026 기준). 직접 운영한다면 위의 "누적 자산 + deploymentId" 조합이 그 일부를 흉내 내는 셈이다.
배포 전략 자체는 GitOps와 수직 확장과 수평 확장, CDN 캐시 동작은 브라우저 캐시과 이어진다.
출처: Next.js - Self-Hosting: Version Skew · Next.js - deploymentId · Next.js - supportsImmutableAssets · Next.js - Supporting Immutable Static Assets · Next.js - assetPrefix · Vercel - Skew Protection · Malte Ubl - Version skew