Failed to load chunk, 동적 import, CDN 캐시 | 배포 직후만 흰 화면이면?
Failed to load chunk는 열어 둔 탭이 옛 빌드 자바스크립트 주소를 요청한 상태다. 네트워크에서 청크 404를 보고, HTML은 짧게 캐시하고, ChunkLoadError에서 한 번만 새로고침한다. 넥스트 동적 import, 웹팩 청크, 버셀 스큐, 씨디엔, 프론트엔드, 개발자 기준. 2026년 9월 webpack·Next.js·Vercel 공식 문서.
Failed to load chunk는 번들이 깨진 게 아니라, 브라우저가 예전 빌드의 자바스크립트 주소를 들고 있는데 새 배포에서 그 파일이 없어진 상태입니다. 배포 직후에만 흰 화면이면 콘솔보다 네트워크 탭에서 _next/static/chunks 요청이 404인지부터 보세요.
관리자 목록을 켜 둔 채로 제가 프리빌트를 올리면, 다음 메뉴를 누르는 순간 넥스트 클라이언트 라우트가 옛 해시 파일을 찾다 실패합니다. 하드 리프레시는 그 탭만 고칩니다.
HTML은 짧게 다시 받게 하고, 해시가 붙은 청크는 길게 캐시하고, ChunkLoadError에서는 한 번만 새로고침하는 가드를 둡니다. 근거는 웹팩 코드 분할과 넥스트 지연 로드, 버셀 캐시 헤더에 있습니다.
배포 직후만 흰 화면이면 어디를 보나
옛 해시 주소가 404인지부터 봅니다. 넥스트는 빌드마다 /_next/static/chunks/ 아래 파일 이름에 내용 해시를 붙입니다. 탭이 예전 HTML을 붙잡고 있으면, 클라이언트 라우트나 동적 import가 그 옛 이름을 요청합니다. 새 배포에는 그 파일이 없으니 네트워크는 404, 화면은 하얗게 남습니다.
웹팩은 이 실패를 ChunkLoadError로 던집니다. 공식 코드 분할 문서는 청크를 실행하거나 해석하지 못했을 때 난다고 적습니다. 메시지에 네트워크 원인까지 다 담기지 않으니, 콘솔 한 줄만 보면 모듈이 없다고 착각하기 쉽습니다. 모듈을 못 찾는 줄은 Cannot find module이고, 여기는 브라우저가 이미 받은 HTML이 가리키는 스크립트 URL이 죽은 칸입니다.
한국에서 낮에 올리면 열어 둔 관리자 탭이 먼저 깨집니다. 제가 쓰는 사이트도 프리빌트 직후, 목록을 새로 고치지 않은 창에서만 재현됐습니다. 시크릿으로 처음부터 열면 새 HTML을 받으니 멀쩡합니다. 그래서 “내 컴퓨터만”이 아니라 “그 탭만”인 경우가 많습니다.
먼저 기억할 것 | 배포 직후만 깨지면 코드를 고치기 전에 실패한 청크 URL과 상태 코드를 보세요. 404면 옛 빌드 주소입니다. 200인데 실패면 받은 본문이 자바스크립트가 아닌 칸입니다.
탭이 옛 HTML을 붙잡으면 새 배포에 없는 청크 주소를 그대로 요청한다
동적 import와 클라이언트 라우트가 같은 줄인 이유
둘 다 이미 열린 페이지가 나중에 스크립트를 더 받기 때문입니다. next/dynamic과 import()는 버튼을 누를 때, 클라이언트 라우트는 메뉴를 누를 때 청크를 받습니다. 첫 페인트에 필요한 파일은 이미 받아 둔 상태라 배포 직후에도 화면이 보이다가, 다음 동작에서만 흰 화면이 납니다.
넥스트 지연 로드 문서는 클라이언트 컴포넌트와 라이브러리를 필요할 때 나누어 받는다고 설명합니다. 그 나누기가 배포 직후 사고의 입구입니다. 서버 컴포넌트는 기본적으로 코드가 나뉘지만, 여기서 깨지는 줄은 브라우저가 추가로 받는 클라이언트 청크입니다.
증상
어디가 엇갈리나
먼저 할 일
배포 직후, 열어 둔 탭만
옛 HTML이 옛 해시 청크를 가리킴
네트워크에서 chunks 404, HTML 캐시 헤더
메뉴 이동만 실패, 주소창 입력은 성공
클라이언트 라우트만 옛 청크를 요청
전체 새로고침과 비교, 문서 캐시
모달·차트 열 때만
동적 import 청크가 없음
실패한 파일명 해시가 현재 빌드에 있는지
배포와 무관하게 전원 실패
publicPath, 씨디엔 오리진, assetPrefix
청크 URL 호스트가 실제 파일 위치인지
200인데 ChunkLoadError
HTML이나 JSON을 스크립트로 받음
Content-Type, 응답 앞 글자
항상 깨지면 배포 타이밍이 아닙니다. 넥스트는 assetPrefix로 /_next/static만 다른 호스트에 올릴 수 있고, public 폴더는 그 접두어를 따라가지 않습니다. 씨디엔 오리진을 잘못 두면 배포 전에도 같은 줄이 납니다. 하이드레이션 불일치는 서버 HTML과 브라우저 첫 렌더 칸이고, 날짜 글자만 다른 줄은 로케일 미스매치입니다. 여기는 스크립트 파일을 못 받은 칸만 봅니다.
실패한 청크 URL을 네트워크에서 확인
// 개발자 도구 → Network → JS 필터
// 실패 행의 이름 예: 623-a1b2c3d4.js 상태 404
// 경로가 /_next/static/chunks/ 인지, 호스트가 앱 도메인인지 본다
// 주소창에 같은 URL을 붙여 새 탭으로 열면
// 새 배포 HTML을 받은 창과 실패 탭의 해시가 다른지 바로 보인다
HTML은 짧게, 청크는 해시로 길게
문서와 청크의 캐시 수명을 바꾸면 원인이 갈립니다. 넥스트는 /_next/static/ 아래 파일에 내용 해시를 넣고 public, max-age=31536000, immutable을 붙입니다. 파일 이름이 바뀌므로 일 년 캐시가 안전합니다. 반대로 HTML이 같은 시간만큼 남으면, 브라우저는 새 청크 목록을 받지 못한 채 죽은 주소를 두드립니다.
버셀 캐시 문서도 해시가 붙은 정적 자산은 immutable, 서버가 그리는 페이지는 브라우저 max-age=0에 가깝게 두라고 나눕니다. 씨디엔이 HTML까지 “전부 캐시”로 묶으면 배포 직후 사고가 납니다. 클라우드플레어를 앞에 둔 한국 1인 사이트에서 낮 배포 뒤에만 터지는 패턴이 이쪽입니다.
대상
권장 헤더
왜 갈리나
HTML 문서
max-age=0, must-revalidate
새 해시 목록을 받게
/_next/static 청크
max-age=31536000, immutable
이름에 내용 해시가 있음
로그인 HTML
private, no-store
사용자마다 다른 화면
서비스 워커
배포마다 스크립트 갱신
옛 HTML을 붙잡으면 같은 줄
로그인 HTML을 씨디엔에 두면 안 되는 이유와, 배포 뒤 청크 404는 자리가 다릅니다. 여기서는 해시 파일과 문서만 나눕니다. 이미지가 호스트 허용 목록에 없어 깨지는 줄은 remotePatterns입니다.
청크 파일명은 내용 해시라 길게 캐시하고, HTML은 짧게 다시 받는다
하드 리프레시가 답이 아닌 이유
하드 리프레시는 그 탭만 고칩니다. 주소창에서 문서를 다시 받으면 새 해시 목록이 들어오니 화면이 살아납니다. 같은 시각에 목록을 켜 둔 다른 창, 출근길에 열어 둔 휴대폰 탭은 그대로입니다. 배포할 때마다 독자에게 새로고침을 안내하는 운영은 한두 번이면 지칩니다.
버셀 스큐 프로텍션은 프레임워크가 관리하는 요청에 배포 아이디를 붙여, 그 탭이 처음 받은 배포의 청크를 계속 받게 합니다. 프로젝트 설정에서 켜져 있으면 열어 둔 탭은 옛 파일을 404 없이 받습니다. 주소창 전체 이동은 최신 프로덕션으로 가고, 버전이 다르면 한 번 새로고침합니다. 대시보드처럼 하루 종일 켜 두는 화면만 쿠키로 더 붙잡습니다.
한 가지 함정이 있습니다. 로컬에서 vercel build 뒤 vercel deploy --prebuilt로 올리면, 빌드 때 박힌 배포 아이디와 업로드 때 아이디가 다를 수 있습니다. 버셀 문서는 프리빌트에서 스큐를 쓰려면 커스텀 배포 아이디로 둘을 맞추라고 적습니다. 제가 쓰는 사이트도 프리빌트라, 이 칸을 안 맞추면 가드 코드가 더 필요했습니다.
프리빌트 | 스큐 프로텍션이 켜져 있어도 빌드 아이디와 배포 아이디가 다르면 옛 청크를 못 찾습니다. 대시보드 스위치만 보고 넘어가지 마세요.
ChunkLoadError에서 한 번만 새로고침
에러 경계에서 이름을 보고, 세션 저장소로 한 번만 새로고침합니다. 웹팩의 import()는 실패를 프로미스로 넘기므로 호출부에서 받아도 됩니다. 넥스트 앱 라우트는 error.js가 같은 역할을 합니다. 가드 없이 location.reload()만 넣으면, 파일이 진짜 없는 배포에서 새로고침이 돕니다.
세션 저장소 키가 있으면 사용자에게 다시 시도 버튼을 보여 줍니다. 같은 탭에서 배포가 두 번 겹치면 키를 배포 해시와 묶을 수 있지만, 1인 사이트는 키 하나면 충분했습니다. 웹팩 문서는 실패 시 재시도를 예시로 두지만, 404가 난 옛 해시를 세 번 받아 봐야 파일이 생기지는 않습니다. 문서를 다시 받는 새로고침이 맞습니다.
버셀은 배포를 지우기 전까지 옛 정적 파일을 붙잡을 수 있습니다. 스큐 프로텍션 최대 수명은 보존 기간을 넘지 못합니다. 프로젝트를 2024년 11월 19일 이후에 만들었고 지원 프레임워크면 기본으로 켜져 있습니다. 예전 프로젝트는 설정 고급 칸에서 스위치를 확인하세요.
직접 호스팅이면 새 빌드가 .next/static을 통째로 덮어 옛 해시를 지웁니다. 리버스 프록시 앞에 해시 경로를 며칠 남겨 두거나, 무중단으로 이전 디렉터리를 심볼릭 링크 뒤에 두는 식이 필요합니다. 엔진엑스에서 HTML만 짧게 캐시하고 /_next/static/은 길게 두는 설정이 문서 헤더와 맞습니다.
청크 타임아웃 기본값은 웹팩이 12만 밀리초입니다. 느린 망에서만 실패하고 배포와 무관하면 타임아웃 칸입니다. 배포 직후 404와 섞지 마세요. 502는 업스트림이 응답을 못 준 줄이라 배드 게이트웨이에서 다룹니다.
대개 롤백할 버그가 아닙니다. 열어 둔 탭이 옛 해시 파일을 요청한 상태입니다. 시크릿으로 같은 주소를 열면 새 HTML이 들어와 통과합니다. 네트워크에서 chunks 404를 확인한 뒤 HTML 캐시와 한 번 새로고침 가드부터 보세요.
하드 리프레시로 고쳤는데 또 배포하면 반복됩니다.
그 탭의 문서만 새로 받은 겁니다. 다른 창과 휴대폰 탭은 옛 주소를 들고 있습니다. HTML을 짧게 캐시하고, 버셀이면 스큐 프로텍션과 프리빌트 아이디를 맞추고, ChunkLoadError에서 한 번만 새로고침하세요.
Cannot find module과 같은 줄인가요?
아닙니다. Cannot find module은 노드가 패키지 폴더를 못 찾은 런타임 줄입니다. Failed to load chunk는 브라우저가 청크 URL을 못 받은 줄입니다. 설치를 다시 해도 배포 직후 흰 화면은 그대로입니다.
버셀 스큐 프로텍션을 켜면 가드 코드는 필요 없나요?
플랫폼 빌드면 프레임워크 요청은 배포에 고정됩니다. 로컬 프리빌트는 빌드 아이디와 배포 아이디가 다를 수 있어 공식 문서가 커스텀 아이디를 요구합니다. 직접 호스팅이거나 서비스 워커가 문서를 붙잡으면 가드가 남습니다.
청크를 캐시하지 않게 하면 해결되나요?
청크는 이름에 해시가 있어서 길게 두는 편이 맞습니다. 짧게 바꿔야 하는 쪽은 HTML입니다. 청크까지 no-store로 두면 방문마다 같은 파일을 다시 받아 첫 페인트만 느려집니다.
error.js에서 reload가 무한으로 돕니다.
세션 저장소 가드가 없거나, 파일이 진짜 없는 배포입니다. 키를 넣었는데도 돌면 네트워크 상태 코드가 404인지, assetPrefix 호스트가 맞는지부터 보세요. 가드는 문서를 한 번 다시 받을 때만 씁니다.
Failed to load chunk는 열어 둔 탭이 새 배포에 없는 청크 주소를 요청한 상태입니다. 네트워크에서 404를 확인하고, HTML은 짧게, 해시 청크는 길게 두고, 에러 이름에서 한 번만 새로고침하세요. 관련 글: Cannot find module, 하이드레이션 미스매치, CORS.
Failed to load chunkChunkLoadError동적 importCDN 캐시next/dynamic스큐 프로텍션Next.jswebpack버셀프론트엔드개발자배포