왜 VanillaFront는 웹팩을 버리고 Dynamic Import 방식을 채택했나?
시작하며
2015년 무렵부터 프론트엔드 개발의 표준은 Webpack 이었습니다. React·Vue·Angular 어느 쪽을 쓰든 결국 Webpack config를 만지고, dist/main.[hash].js 를 배포하는 흐름이었죠. Webpack이 없으면 모던 프론트엔드 개발이 안 된다는 인식이 굳어질 만큼 표준이었습니다.
VanillaFront는 그 표준을 의도적으로 벗어난 프레임워크입니다. Webpack 없이, 번들러 없이, 브라우저가 ES6 모듈을 직접 로드하는 구조로 설계했습니다. 이 선택의 배경에는 단순히 "번들러 세팅이 귀찮아서" 라는 이유가 아닌, 번들 스플리팅이 야기하는 복잡도, 모던 브라우저의 성숙, HTTP/2 이후 성능 모델의 변화 라는 세 축이 있습니다.
이 글에서는 Webpack의 장점을 공정하게 짚은 뒤, 왜 VanillaFront가 Dynamic Import 중심 아키텍처를 선택했는지 구체적으로 설명합니다.
1. Webpack이 잘 하는 것 — 먼저 공정하게
Webpack을 폄훼하려는 글이 아니니 먼저 Webpack이 실제로 잘 하는 것을 정리해두겠습니다.
Tree Shaking (미사용 코드 제거) Lodash 전체 파일이 800KB인데 debounce 하나만 쓸 때, Webpack은 나머지 790KB를 잘라내고 필요한 코드만 남깁니다. Dead code elimination은 여전히 번들러의 강력한 이점입니다.
Asset Pipeline CSS 전처리 (Sass, Less, PostCSS), 이미지 최적화, SVG 인라인, 폰트 서브셋 등을 하나의 파이프라인으로 처리합니다. 대규모 웹 애플리케이션에서 이 정리 능력은 꾸준한 가치가 있습니다.
Minification과 Compression 프로덕션 빌드에서 코드를 축약하고 압축해서 다운로드 크기를 최소화합니다.
Legacy 브라우저 지원 Babel과 결합해 ES2020+ 코드를 ES5로 트랜스파일. IE·구형 Safari 지원이 필요한 프로젝트에선 필수 도구였습니다.
모듈 포맷 변환 CommonJS·AMD·UMD·ESM 등 서로 다른 포맷의 라이브러리를 모아 하나의 실행 가능한 번들로 만듭니다. Node.js 생태계와 브라우저를 잇는 브리지 역할.
Dev Server와 HMR (Hot Module Replacement) 개발 중 파일 저장하면 페이지 새로고침 없이 변경된 모듈만 교체. 개발자 경험에 크게 기여합니다.
이 능력들은 실제로 강력하고, 특정 상황에선 여전히 최적의 선택입니다. 다만 모든 상황에서 그런가를 다시 물을 시점이 왔습니다.
2. 번들 스플리팅이라는 복잡도의 늪
Webpack의 진짜 스트레스는 번들을 나눌 때 시작됩니다.
프로젝트가 커지면 하나의 main.js 가 2MB, 5MB로 부풀어 오릅니다. 첫 페이지 로딩이 느려지고, 사용자가 방문하지 않는 라우트의 코드까지 초기에 다 다운로드하는 낭비가 발생합니다. 그래서 번들을 나눠야 합니다. 여기서부터 지옥이 시작됩니다.
2.1 Chunk Splitting 설정의 미궁
Webpack의 splitChunks 설정을 한번 볼까요:
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 10,
},
},
},
},
};
이 설정 하나만 봐도:
- minSize, maxSize, minChunks, enforceSizeThreshold 등 크기 관련 파라미터가 6개 이상
- cacheGroups 로 어떤 코드가 어느 청크로 갈지 수동으로 지정
- priority, reuseExistingChunk, chunks: 'all' | 'async' | 'initial' 등 미묘한 옵션
- 각 설정이 서로 상호작용해서 결과 청크 구성이 달라짐
이걸 완벽하게 이해하는 팀이 얼마나 될까요? 대부분은 스택오버플로우와 블로그 글을 참고해서 그럴싸한 설정을 붙여넣고, 다음 사람이 건드리기 두려워하는 코드로 남깁니다.
2.2 React.lazy + Suspense의 강제성
React 앱에서 라우트 단위 코드 스플리팅을 하려면:
const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Reports = React.lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
라우트 하나마다 React.lazy 래핑, Suspense 감싸기, fallback UI 처리. 여기까지도 OK. 문제는 그 다음입니다:
- 에러 바운더리를 어디에 두지?
- 청크 로딩 실패했을 때 재시도 로직은?
- 네트워크가 느릴 때 사용자 경험은?
- 프리로딩 (다음 라우트 미리 받기) 은 어떻게 튜닝하지?
- 청크 이름이 [hash] 기반이라 CDN 캐시 무효화 정책은?
각각이 별도의 결정이고 별도의 코드입니다. 100라우트 규모 앱에서 이걸 다 손보는 시간과 스트레스는 예상보다 큽니다.
2.3 청크 간 의존성 그래프
번들을 나누면 청크 간 의존성이 생깁니다. Reports.chunk.js 가 Vendors.chunk.js 를 의존하고, Vendors.chunk.js 는 Common.chunk.js 를 의존하는 식으로.
문제는 이게 눈에 안 보인다는 겁니다. Webpack이 자동으로 관리하지만:
- 어떤 청크가 어떤 청크를 로드하는지 파악하려면 webpack-bundle-analyzer 같은 도구 필요
- 특정 페이지 로딩이 느려졌을 때 어느 청크의 어느 모듈이 원인인지 추적이 힘들
- 리팩토링 시 청크 구성이 재편되어 캐시 무효화가 예상 못한 규모로 발생
숙련된 프론트엔드 팀도 청크 최적화에 며칠~몇 주씩 쓰는 게 흔한 이유입니다.
2.4 배포 파이프라인의 무게
Webpack 기반 프로젝트는 배포가 이렇습니다:
- git pull
- npm install (수 분)
- npm run build (수 분)
- 생성된 dist/ 폴더를 서버로 업로드
- 정적 파일 서빙 서버 재시작
이걸 CI/CD 파이프라인으로 자동화하는 건 가능하지만, 파이프라인 자체가 유지보수 대상이 됩니다. 노드 버전, 캐시, 환경 변수, 시크릿 관리...
특히 사내 인트라넷·폐쇄망 환경에서 이 흐름은 큰 벽입니다. npm 레지스트리 프록시 구축, 노드 설치 승인, 빌드 서버 구성 — 모던 프론트엔드가 엔터프라이즈 사내 시스템에 쉽게 안 들어가는 이유가 여기 있습니다.
2.5 실전에서 자주 마주치는 함정들
- 동일 코드가 여러 청크에 중복 포함되는 문제 (splitChunks 세팅 실수)
- 동적 import 경로가 정적 분석 안 되면 청크로 안 나뉨
- CommonJS 라이브러리와 ESM 호환성 문제로 번들 크기 급증
- 환경 변수 처리 (development / production / staging)
- 소스맵 사이즈와 프로덕션 노출 이슈
- HMR 오작동 시 원인 파악의 어려움
각각이 한 번씩만 터져도 개발자 하루가 사라집니다. Webpack 관련 스택오버플로우 질문이 수십만 건 쌓여있는 이유죠.
3. 브라우저는 이미 준비되어 있다
Dynamic Import (import(...)) 이 ECMAScript 2020 표준에 편입된 지 5년 이상 지났습니다. 그동안 브라우저 지원이 어떻게 변했는지 짚어보겠습니다.
Dynamic Import 지원 현황 (2025년 기준)
- Chrome / Edge: 63+ (2017)
- Firefox: 67+ (2019)
- Safari: 11.1+ (2018)
- iOS Safari: 11.3+
- Samsung Internet: 8+
- Opera: 50+
Can I Use 통계: 전 세계 사용자의 96% 이상이 Dynamic Import을 지원하는 브라우저를 사용합니다. IE는 사실상 죽었고 (마이크로소프트도 2022년 지원 종료), Edge Legacy도 사라졌습니다. 브라우저는 장비나 구시스템에서 운용되는걸 감안한다면 이미 국내의 경우 99% 이상 모던 브라우저를 사용합니다.
ES6 Modules 자체: 마찬가지로 97%+ 지원. <script type="module"> 로 모듈을 그대로 브라우저에 로드할 수 있는 환경이 됐습니다.
HTTP/2·HTTP/3 지원: 주요 CDN·웹 서버가 기본 활성화. Multiplexing으로 다중 요청 성능 부담이 대폭 완화됐습니다.
Import Maps: 2023년 표준화. 모듈 경로 별칭을 브라우저 네이티브로 관리할 수 있게 됨.
즉 "Webpack이 아니면 못 하는 것" 이 5년 전보다 훨씬 줄었습니다. 특히 엔터프라이즈 사내 시스템처럼 모던 브라우저만 지원해도 되는 환경에선 Webpack의 존재 이유 중 상당 부분이 사라졌습니다.
4. Dynamic Import이 실제로 잘 하는 것
VanillaFront는 이 변화 위에서 설계됐습니다. Dynamic Import을 중심축으로 삼았을 때 어떤 이득을 얻는지 구체적으로 보겠습니다.
4.1 라우트 단위 코드 스플리팅이 "공짜"
VanillaFront의 router.js 는 이렇게 생겼습니다:
const routerPath = {
main: { path: '/view/main/Main' },
apibutton: { path: '/view/api/buttons/ApiButton' },
apigrid: { path: '/view/api/grids/ApiGrid' },
tmplgridandform: { path: '/view/tmpl/grid/TmplGridAndForm' },
price: { path: '/view/price/Price' },
// ... 400+ 라우트
};
Va.setRouterPath(routerPath);
라우트가 400개가 넘어도, 사용자가 방문한 라우트만 그 시점에 import() 되어 로드됩니다. Webpack의 React.lazy + Suspense + splitChunks 설정 조합으로 얻어야 할 결과가, 여기서는 Dynamic Import 하나로 자동으로 얻어집니다.
splitChunks 설정을 튜닝할 필요도 없고, cacheGroup 정책을 정할 필요도 없고, 청크 이름 규칙을 고민할 필요도 없습니다. 각 라우트 파일이 그 자체로 청크입니다.
4.2 캐시 세분화의 이득
Webpack 번들에선 이런 일이 자주 일어납니다:
- main.[hash].js 가 2MB
- 컴포넌트 하나 수정
- 새 hash가 붙어서 2MB 전체가 캐시 무효화
- 사용자가 다시 2MB 다운로드
Dynamic Import 방식에선:
- combobox.js 만 수정
- combobox.js 만 캐시 무효화
- 사용자가 다음 방문 시 그 파일만 다시 받음
- 나머지 파일은 브라우저 캐시에서 즉시 로드
CDN·브라우저 캐시 hit rate가 훨씬 높습니다. 자주 업데이트되는 엔터프라이즈 사내 시스템에서 이 차이는 사용자가 체감할 만큼 큽니다.
4.3 배포의 단순함
Webpack 빌드 파이프라인 없이:
- git pull
- 끝
정말 이게 끝입니다. 웹 서버 (Nginx, Apache) 나 Spring Boot 정적 리소스가 파일을 그대로 서빙하면 됩니다.
사내 인트라넷 환경에서의 의미는 큽니다:
- Node.js 설치 승인 불필요
- npm 프록시 구축 불필요
- 빌드 서버 구성 불필요
- 배포 시간이 몇 분 → 몇 초
VanillaFront가 실제 사내 시스템에서 1년 넘게 운영되며 자주 배포되는 사례를 만들어낸 배경입니다.
4.4 개발 반복 속도
Webpack Dev Server 콜드 스타트:
- npm install 처음 실행 시 몇 분
- npm run dev 초기화 30초 ~ 몇 분 (프로젝트 규모에 따라)
- HMR이 잘 붙으면 빠르지만, 안 붙으면 페이지 새로고침
VanillaFront 개발 흐름:
- IDE에서 파일 저장
- 브라우저 새로고침
- 즉시 반영
콜드 스타트가 0입니다. 프로젝트 열자마자 개발 시작. 이게 하루에 수십 번 반복되는 개발 사이클에서 체감 생산성 차이가 큽니다.
4.5 부분 실패의 격리
번들 하나가 파싱 에러를 내면 전체 앱이 죽습니다. Dynamic Import 방식에선 한 라우트 파일이 문제여도 다른 라우트는 살아있습니다. 프로덕션 회복력 관점에서 유리한 특성입니다.
4.6 HTTP/2와의 궁합
HTTP/1.1 시대엔 "요청 수 = 성능 비용" 이었습니다. Webpack이 파일을 뭉치는 것이 성능 개선이었죠. HTTP/2가 널리 배포된 이후엔 이야기가 달라졌습니다:
- Multiplexing — 한 연결 위에서 여러 요청을 병렬로
- Server Push (지금은 deprecated이지만 preload로 대체)
- Header Compression (HPACK)
여러 개의 작은 파일이 하나의 큰 번들보다 오히려 빠른 케이스가 흔합니다. 특히 캐시 상황을 고려하면 평균적으로 유리합니다.
5. 트레이드오프 — 솔직한 평가
Dynamic Import 중심 아키텍처가 만능은 아닙니다. 잃는 것도 있습니다. 정직하게 짚어보겠습니다.
Tree Shaking을 못 함 각 모듈이 통째로 로드됩니다. 거대한 유틸 라이브러리에서 함수 하나만 쓸 때 나머지가 다 딸려옵니다. 다만 VanillaFront는 자체 컴포넌트를 잘게 나눠뒀기 때문에 이 손실이 크지 않고, 외부 라이브러리 사용도 통제 가능합니다.
HTTP/2 이상이 사실상 전제 HTTP/1.1 환경이면 여러 파일 로드 시 waterfall이 발생. 다행히 요즘 대부분 인프라가 HTTP/2 이상 지원하지만, 확인은 필요합니다.
IDE의 정적 분석 한계 import('./views/' + name + '.js') 같은 동적 경로는 IDE가 추적하기 어렵습니다. Rename refactor 같은 도구가 놓칠 수 있음. VanillaFront는 router.js 중앙 등록으로 상당 부분 해결되지만 완전하진 않습니다.
초기 페인트 요청 수 첫 방문 시 shell + 첫 라우트 두 번 왕복. <link rel="modulepreload"> 로 완화 가능하지만 튜닝 여지입니다.
모던 브라우저만 타겟 가능 IE·구형 브라우저 지원이 필요하면 Webpack + Babel 조합이 여전히 필요합니다. 다만 이런 요구는 매년 줄어들고 있습니다.
6. VanillaFront의 사용 시나리오와의 정합
Dynamic Import 중심 아키텍처가 명확히 유리한 조건들이 있는데, VanillaFront의 타겟 사용 시나리오가 거의 다 해당됩니다:
- ✅ 모던 브라우저만 지원 — 사내 시스템·엔터프라이즈 관리 화면에선 브라우저 통제 가능(국내 99%가 모던 브라우저 사용)
- ✅ 라우트 수가 많음 — 관리자 페이지 400+ 화면이 흔함. 스플리팅이 필수
- ✅ 사용자당 세션에 소수 라우트만 방문 — 관리자가 한 세션에 5~10개 화면만 도는 게 일반적
- ✅ 자주 배포·핫픽스 — 캐시 세분화 이득 큼
- ✅ 사내 인트라넷·폐쇄망 — 빌드 파이프라인 없이 배포 가능한 것이 결정적 장점
- ✅ 개발 반복 속도 중요 — 관리자 페이지 다수 개발 시 빌드 대기 시간이 누적 스트레스
VanillaFront가 겨냥하는 시장 (엔터프라이즈 사내 시스템·관리자 페이지·대시보드) 에선 Dynamic Import이 이론적으로도 실용적으로도 우위입니다.
7. 코드로 보는 차이
같은 요구사항 — "여러 라우트를 lazy loading" — 을 각각 어떻게 해결하는지 비교해보겠습니다.
Webpack + React
webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
publicPath: '/'
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10
},
common: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
},
module: {
rules: [
{ test: /\.jsx?$/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }
]
}
};
App.jsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Reports = lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
package.json + node_modules + build 파이프라인이 딸려옵니다.
VanillaFront
router.js
const routerPath = {
home: { path: '/view/home/Home' },
dashboard: { path: '/view/dashboard/Dashboard' },
reports: { path: '/view/reports/Reports' }
};
Va.setRouterPath(routerPath);
index.html
<script type="module" src="./router.js"></script>
끝. Node.js 없음, package.json 없음, 빌드 없음. 각 View 파일은 방문 시점에 자동으로 import() 됩니다.
8. Webpack이 여전히 맞는 상황
공정하게, Webpack이 지금도 최선인 경우도 있습니다:
- 초대형 서드파티 라이브러리 — Moment (800KB)에서 몇십 KB만 쓴다면 Tree shaking이 필수
- 랜딩 페이지·이커머스 — 초기 페인트 극한 최적화가 매출로 직결
- 레거시 브라우저 지원 필수 — 공공기관·특정 산업
- 복잡한 CSS·에셋 파이프라인 — Sass, PostCSS, 이미지 최적화 등을 하나로 관리
- 대규모 프론트엔드 팀 — Webpack config 전담 인력이 있고, 팀 전체가 이 표준에 익숙
VanillaFront가 겨냥하는 시장은 이 조건들에 해당되지 않기 때문에, 다른 선택이 자연스러웠습니다.
9. 아키텍처 선택의 프레임
프레임워크 설계에서 "번들러를 쓸 것인가" 는 단순 기술 선택이 아니라 철학적 선택입니다. 두 축이 있습니다.
축 1: 최적화 지점을 어디에 둘까
- Webpack 방식: 빌드 타임에 최대한 최적화 — 런타임 부담 최소
- Dynamic Import 방식: 브라우저·네트워크 위에 최적화 위임 — 빌드 타임 부담 제로
HTTP/2·HTTP/3와 CDN이 성숙한 지금, 후자의 상대적 비용이 크게 낮아졌습니다.
축 2: 복잡도를 어디에 배치할까
- Webpack 방식: 빌드 도구의 복잡도를 감수
- Dynamic Import 방식: 웹 표준의 제약 안에서 살기
VanillaFront는 후자를 택했고, 그 결과로 얻은 단순함이 엔터프라이즈 사내 시스템에서 실제로 배포·운영되는 결정적 이점이 됐습니다.
10. 마무리
Webpack이 나쁜 도구인 게 아닙니다. 5~10년 전 웹의 제약을 극복한 훌륭한 엔지니어링이고, 특정 시나리오에선 여전히 최선의 선택입니다.
다만 웹 플랫폼이 그 사이 크게 성장했습니다:
- ES6 모듈이 브라우저 네이티브가 됨
- Dynamic Import이 표준이 됨
- HTTP/2가 널리 배포됨
- 브라우저 지원 하한선이 올라감
- Import Maps가 표준화됨
이 변화 위에서 다시 물어볼 만한 질문은 "Webpack이 없으면 안 되는가" 가 아니라 "지금 이 프로젝트에 Webpack이 필요한가" 입니다.
VanillaFront가 겨냥하는 엔터프라이즈 사내 시스템·관리자 페이지·대시보드 시장에선, 답이 "아니오" 입니다. Webpack의 이득이 스트레스보다 작고, Dynamic Import의 이득이 손실보다 훨씬 큽니다.
그래서 VanillaFront는 번들러 없는 프레임워크로 태어났습니다. 이건 편의를 위한 선택이 아니라, 모던 웹 플랫폼을 정확히 활용하기 위한 아키텍처적 결정입니다.
파일 하나 수정, 브라우저 새로고침, 즉시 반영. 배포는 git pull. 캐시는 파일 단위. 이 단순함이 5년 전엔 불가능했지만, 지금은 그냥 "웹이 원래 이랬어야 하는 모습" 에 가깝습니다.