증상
개발 환경(npm run dev)에서는 잘 동작하는데, 프로덕션 빌드(npm run build && npm run preview 혹은 배포) 후 흰 화면(Blank Screen) 이 뜨고 라우트가 렌더링되지 않았다.
대부분의 경우 콘솔에서 아래 중 하나를 볼 수 있다.
- Uncaught Error: Dynamic require of "xxx" is not supported
- Failed to fetch dynamically imported module
- 혹은 라우트 전환 시점에만 아무것도 렌더링되지 않음
이런 유형은 “코드 스플리팅/지연 로딩 조각(chunk)”을 가져오는 과정이 깨졌을 때 자주 나타난다.
원인: React.lazy는 결국 dynamic import()를 쓴다
React.lazy(() => import('./SomePage'))는 문법만 React스럽게 감싼 형태고, 본질은 import()(dynamic import) 기반 코드 스플리팅이다. 번들러가 이 import()를 분석해서 청크를 만들고, 런타임에 필요할 때 가져온다.
그런데 문제는 esbuild 타깃을 es2015로 낮춰 둔 설정이었다.
왜 es2015가 문제를 만들 수 있나?
import()(dynamic import)는 ES2015 스펙에 포함된 기능이 아니라, 더 이후의 모던 ESM 환경(브라우저 모듈 + dynamic import 지원)을 전제로 한다.
esbuild는 “타깃 환경이 dynamic import를 지원하지 않는다”고 판단하면, 어떤 경우 import()를 require() 형태로 바꿔버리는 변환을 해버릴 수 있는데(특히 과거 버전에서), 이게 브라우저 번들에서는 치명적이다. 브라우저에선 require가 없으니까 런타임에 터지고, 결국 화면이 안 뜬다.
실제로 esbuild 쪽에서도 “브라우저 타깃에서 dynamic import를 require로 바꿔버려 문제가 된다”는 이슈가 보고된 바 있다.
(https://github.com/evanw/esbuild/issues/1281)
재현 예제 (Vite + React Router + React.lazy)
1) 라우트 Lazy Loading
// src/App.tsx
import { Suspense, lazy } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./pages/Home"));
const About = lazy(() => import("./pages/About"));
export default function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2) 문제를 일으킨 빌드 설정 (예: target: "es2015")
이 상태에서 빌드하면, 개발 서버에서는 정상인데 빌드 산출물에서 lazy chunk 로딩이 깨지면서 화면이 안 뜰 수 있다.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
target: "es2015", // 👈 이 설정이 문제의 트리거였던 케이스
},
});
이 상태에서 빌드하면, 개발 서버에서는 정상인데 빌드 산출물에서 lazy chunk 로딩이 깨지면서 화면이 안 뜰 수 있다.
해결 1) build.target을 ‘modules’/‘es2020’/‘esnext’로 상향
Vite의 build.target 기본값은 “모듈을 지원하는 브라우저” 묶음(modules)이고, 여기에 native dynamic import 지원이 포함된다. 가능하면 target을 굳이 es2015로 낮추지 않는 게 안전하다.
예시:
// vite.config.ts
export default defineConfig({
plugins: [react()],
build: {
target: "es2020", // 또는 "modules" / "esnext"
},
});
해결 2) esbuild 버전 업그레이드
내 케이스에서는 target을 유지해야 하는 제약이 있어(레거시 정책 등) esbuild를 최신으로 올리는 방식으로 해결했다.
# npm
npm i -D esbuild@latest
# pnpm
pnpm add -D esbuild@latest
# yarn
yarn add -D esbuild@latest
Vite/번들러 체인에서 esbuild가 간접 의존성일 수도 있으니, 실제로 어떤 버전이 쓰이는지 확인하려면:
npm ls esbuild
# 또는
pnpm why esbuild
정리: “빌드 후 흰 화면”에서 가장 먼저 볼 체크리스트
- 콘솔 에러에 dynamic import, require, Failed to fetch dynamically imported module, chunk errorr 같은 문구가 있는지 확인
- React.lazy/라우트 코드스플리팅을 쓰고 있다면 build.target이 너무 낮게(es2015) 잡혀 있지 않은지 확인
- 가능하면 modules/es2020/esnext로 올리기
- 필요하면 esbuild 업그레이드 + 의존성 트리에서 실제 적용 버전 확인
- 그래도 재현되면 esbuild의 “dynamic import → require 변환”류 이슈를 의심
'코딩 > 문제 해결' 카테고리의 다른 글
| [Claude code] Git에도 안 올렸는데, Claude Code Auto Compact 때문에 작업 내용을 잊어버렸다면? 복구할 수 있는 방법이 있습니다 (0) | 2026.03.15 |
|---|---|
| pyenv 바로 사용 불가능 (0) | 2025.06.16 |
| Cursor MCP Client Closed(sequentialthinking) (10) | 2025.06.03 |