- Published on
동적 import 에러 재현하고 해결하기
동적 import(Dynamic Import) 에러
SPA(Single Page Application) 서비스를 운영하다 보면 센트리등 모니터링 도구에서 동적 import 관련 오류를 접하게 된다. 개발 도구에 따라 에러 이름은 조금씩 다른데, chunkLoadError
, vite:preloaderror
등으로 나타난다.

이 동적 import 에러가 발생하는 주요 원인은 다음과 같다.
- 프로젝트를 빌드하면 번들 파일 이름에 해시(hash) 값이 붙는다. (일반적으로
contenthash
전략을 사용한다) - Code Splitting을 통해 하나의 번들을 여러 개의 청크로 분할한다.
- 사용자가 특정 청크(A)를 요청하는 시점에 새 배포가 이루어지면, 청크 파일의 해시가 바뀐다.
- 이때 이전 청크를 여전히 불러오려고 하면 더 이상 존재하지 않는 파일을 요청하게 되어
chunkLoadError
가 발생한다.
로컬에서 재현하기
이 문제를 재현하기가 쉽지 않다. 그래서 운영 환경에서 처음 이 오류를 접하고 원인을 파악하느라 시간을 허비하는 경우가 생긴다. 이번 글에서는 로컬에서 동적 import 에러를 간단히 재현해 보고, 이를 해결하는 과정을 정리해본다. 예시는 vite 기반 프로젝트를 기준으로 작성한다.
예시 프로젝트 준비
이번 예시에서는 React, React Router, vite를 사용한다. 다음과 같이 Home 페이지와 About 페이지가 있다고 가정한다. 페이지 단위로 동적 import를 구현한다.
import { lazy } from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home.tsx'))
const About = lazy(() => import('./pages/About.tsx'))
const AppRouter = () => {
return (
<Router>
<Routes>
<Route path="/" Component={Home} />
<Route path="/about" Component={About} />
</Routes>
</Router>
)
}
export default AppRouter
// pages/Home.tsx
import { useNavigate } from 'react-router-dom'
export default function Home() {
const navigate = useNavigate()
return <button onClick={() => navigate('/about')}>Go to About</button>
}
// pages/About.tsx
export default function About() {
return <>About</>
}
이 상태에서 yarn build를 실행해 프로덕션 빌드를 진행하면, 다음과 같은 형태로 번들 파일이 생성된다. (해시 값은 단순화를 위해 숫자로 표시했다.)
dist/
└── assets/
├── index-1.js
├── About-1.js
├── Home-1.js
여기서 1
부분이 해시 값이다. 실제로는 랜덤한 긴 문자열이 붙는다.
빌드 결과 실행 및 에러 재현
빌드가 끝난 뒤에는 yarn preview 명령어로 production 환경에서 앱을 실행한다.
yarn preview
이 상태에서 웹 브라우저로 접속한 뒤 Home 페이지의 버튼을 눌러 /about 경로로 이동하면, About.js 번들이 정상적으로 동적으로 로드되는 과정을 확인할 수 있다.

그렇다면 해시가 달라져서 발생하는 동적 import 에러를 어떻게 로컬에서 재현할 수 있을까? 이를 위해 우선 vite 설정에서 minify를 false로 두어 번들 파일을 쉽게 볼 수 있도록 하자.
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
minify: false, // 코드 난독화 비활성화
},
})
이제 빌드를 다시 한 다. 빌드 결과물 폴더인 dist 폴더를 열어 dist/index.js 파일이 불러오는 실제 About-1.js 대신 About-2.js처럼 해시 값을 바꿔본다.
// dist/index.js
const About = reactExports.lazy(() =>
// index.js가 불러오는 About.js의 해쉬값 변경
__vitePreload(() => import('./About-2.js'), true ? [] : void 0)
)
그 뒤 브라우저에서 /about 경로로 이동하는 버튼을 눌러보면, 콘솔에 vite:preloaderror와 같은 에러가 발생하는 것을 볼 수 있다.

동적 import 에러 처리하기
동적 import 에러를 해결하는 방법은 다양하다. 여기서는 새로고침을 통해 해결하는 간단한 방법을 살펴본다.
vite 공식 문서에 따르면, vite 기반 프로젝트에서는 동적 import 에러가 발생할 때 vite:preloaderror 이벤트가 발생한다. 이 이벤트를 활용하여 에러 상황에서 새로고침을 유도할 수 있다.
window.addEventListener('vite:preloadError', () => {
// 새로고침한다
window.location.reload()
})
에러가 발생하면 페이지를 새로고침해 최신 index.html을 불러오게 만든다. index.html은 일반적으로 캐시를 적용하지 않기 때문에, 항상 최신 번들을 로드할 수 있게 된다. 결과적으로 사라진 청크를 불러오려다 발생하는 문제를 방지할 수 있다.
로컬에서는 다음과 같이 재현해볼 수 있다. 직접 해시 값을 바꿔가며 새로고침에 걸리는 타이밍을 확인해야 하므로, 무한 새로고침이 일어나지 않도록 setTimeout을 사용해 일시적으로 새로고침을 지연시키는 식이다.
// 로컬에서 테스트를 위해 setTimeout을 사용, 실제 운영 환경에서는 제거
// index.ts 또는 main.tsx
setTimeout(() => {
window.location.reload()
}, 3000)
이 상태에서 dist/index.js 파일을 수정해 About-2.js처럼 해시 값을 바꾼 뒤, /about 페이지로 이동하여 동적 import 에러를 발생시킨다.
그리고 실제 dist/About.js 파일의 이름을 About-2.js로 변경하면, 다음 새로고침 시 정상적으로 앱이 실행되는 것을 확인할 수 있다.
// dist/index.js
const About = reactExports.lazy(() =>
__vitePreload(() => import('./About-2.js'), true ? [] : void 0)
)
// 해시 값을 일치시킨다
dist/
└── assets/
├── About-2.js
정리하자면, 동적 import 에러는 새 배포로 인해 빌드된 청크 파일 이름이 변경되면서 발생한다. SPA에서 사용자가 머무르는 동안 청크가 갱신되면 이전 청크를 찾을 수 없어 에러가 생기게 된다. 이를 방지하기 위한 대표적 방법으로는 새로고침을 통한 최신 번들 파일 로드가 있다. vite의 경우 vite:preloaderror 이벤트를 활용해 에러 발생 시 새로고침하는 방식을 사용할 수 있다.
하지만 새로고침을 하게되면 앱이 초기화되면서 전역상태, 지역상태등 앱 메모리에 저장된 상태들이 사라지게 되는 문제점이 있다. 이에 대한 해결책은 다음 글에서 다루도록 한다.