Frontend Build System
Bundle & Build 가이드
패키지 매니저부터 번들러, 빌드 최적화, 배포 속도까지
Build Pipeline Overview
패키지 매니저
npm · yarn · pnpm — 의존성을 설치하고 관리하는 도구
프로젝트에 필요한 라이브러리(의존성)를 다운로드하고, 버전을 고정하고,node_modules폴더에 정리하는 도구다. 셋 다 같은 역할이지만 설치 방식과 디스크 구조가 다르다.
npm / yarn (flat)
node_modules/
├─ react/
├─ react-dom/
├─ lodash/
├─ scheduler/ ← 유령 의존성
└─ ...수백 개 flat
모든 패키지가 최상위에 flat하게 설치됨.
직접 설치하지 않은 패키지도 접근 가능 (유령 의존성)
pnpm (symlink + content-addressable)
node_modules/
├─ .pnpm/ ← 실제 파일 저장소
│ ├─ react@18.2.0/
│ ├─ react-dom@18.2.0/
│ └─ lodash@4.17/
├─ react → symlink
└─ lodash → symlink
글로벌 저장소에서 하드링크 → 디스크 절약
직접 설치한 패키지만 접근 가능 (유령 의존성 차단)
pnpm은 하드링크 + 심볼릭 링크로 디스크를 절약하면서 유령 의존성을 차단한다
| 항목 | npm | yarn | pnpm |
|---|---|---|---|
| 설치 속도 | 보통 | 빠름 | 가장 빠름 |
| 디스크 사용 | 프로젝트마다 복사 | 프로젝트마다 복사 | 글로벌 저장소 공유 |
| Lockfile | package-lock.json | yarn.lock | pnpm-lock.yaml |
| 유령 의존성 | 허용 (flat) | 허용 (flat) | 차단 (strict) |
| Monorepo | workspaces | workspaces | workspace + 필터 |
| Node 버전관리 | nvm 별도 | corepack | corepack / 내장 |
Lockfile이란?
package.json에는 "react": "^18.2.0"처럼 버전 범위가 적혀 있다. 설치 시점에 따라 실제 설치되는 버전이 달라질 수 있다. Lockfile은 “이번에 정확히 이 버전을 설치했다”를 기록해서, 팀원 전원이 동일한 버전을 쓸 수 있게 보장한다. 반드시 Git에 커밋해야 한다.
모듈 시스템
CommonJS vs ES Modules — 코드를 나누고 합치는 규칙
JavaScript 파일 하나가 다른 파일의 함수를 가져다 쓰려면 “모듈 시스템”이 필요하다. 현재 두 가지가 공존한다.
CommonJS (CJS)
// 내보내기
module.exports = { add, multiply };
// 가져오기
const { add } = require('./math');· 동기 로딩
· Tree Shaking 어려움
· Node.js 전통 방식
ES Modules (ESM)
// 내보내기
export function add(a, b) { ... }
export default multiply;
// 가져오기
import { add } from './math';· 비동기 로딩 (정적 분석 가능)
· Tree Shaking 가능
· 브라우저 + Node.js 표준
현대 프론트엔드는 ESM이 표준 — 정적 분석이 가능해서 Tree Shaking, Code Splitting 등 최적화에 유리
Module Resolution Order
import X from 'react'를 만나면 어디서 찾는가?
node_modules를 디렉토리 트리를 타고 올라가며 탐색한다. pnpm은 심볼릭 링크로 이 과정을 최적화.
번들러
Webpack · Vite · Rollup · esbuild · Turbopack
번들러는 수백 개의 파일을 브라우저가 이해할 수 있는 소수의 파일로 합치는 도구다. import/export 관계를 분석하고, 변환하고, 최적화해서 최종 번들을 만든다.
What Does a Bundler Do?
Input (수백 개)
App.tsx
Header.tsx
utils/format.ts
styles/main.css
assets/logo.svg
...200+ files
번들러 처리
· 의존성 그래프 구축
· 코드 변환 (JSX, TS)
· Tree Shaking
· Code Splitting
· Minification
Output (최적화됨)
main.a1b2c3.js
vendor.d4e5f6.js
page-home.g7h8.js
styles.i9j0.css
= 4 files, gzipped
가장 오래되고 널리 쓰이는 번들러. 풍부한 플러그인 생태계. 설정이 복잡하지만 무엇이든 가능.
적합: 대규모 프로젝트, 레거시, 세밀한 제어 필요 시
개발 서버는 ESM 네이티브, 프로덕션은 Rollup 기반. 설정이 간단하고 DX가 뛰어남.
적합: 새 프로젝트, React/Vue/Svelte
Go로 작성, 극한의 속도. 다른 번들러의 내부 트랜스파일러로도 사용됨.
적합: 라이브러리 빌드, 빠른 트랜스파일
Vercel이 만든 Webpack 후속. Rust로 작성, 증분 빌드 최적화. Next.js에 내장.
적합: Next.js 프로젝트
Build Speed Comparison (상대적)
esbuild
Turbopack
SWC
Vite (dev)
Rollup
Webpack
네이티브 언어(Go, Rust)로 작성된 도구가 JS 기반 도구보다 10~100배 빠르다
Webpack 핵심 개념 5가지
webpack.config.js를 이해하는 뼈대
Entry
번들링 시작점. 의존성 그래프의 루트.
entry: './src/index.tsx'
Output
번들 결과물 경로와 파일명.
output: { path: '/dist', filename: '[name].[hash].js' }
Loaders
JS가 아닌 파일을 변환. (CSS, 이미지, TS)
module: { rules: [{ test: /\.tsx$/, use: 'babel-loader' }] }
Plugins
번들 프로세스를 확장. (압축, HTML 생성 등)
plugins: [new HtmlWebpackPlugin()]
DevServer
개발용 로컬 서버. HMR 지원.
devServer: { hot: true, port: 3000 }
Vite의 핵심 아이디어
“개발은 빠르게, 빌드는 안정적으로”
Dev Server (개발)
브라우저의 네이티브 ESM을 활용. 파일을 번들링하지 않고 그대로 서빙한다. 변경된 파일만 다시 보내므로 HMR이 수 ms 내 완료.
브라우저 → import App from './App.tsx' 서버 → 해당 파일만 변환해서 전달 = 번들링 없이 즉시 실행
Build (프로덕션)
Rollup을 기반으로 프로덕션 번들을 생성. Tree Shaking, Code Splitting, Minification 모두 적용. esbuild로 트랜스파일 가속.
vite build → Rollup 번들링 → esbuild 미니파이 → 최적화된 정적 파일
빌드 핵심 개념
Tree Shaking · Code Splitting · Minification · HMR · Transpilation · Source Map
Tree Shaking
사용하지 않는 코드를 번들에서 제거하는 기술. ESM의 정적 분석이 가능해야 동작한다.
Before (전체 포함)
math.js
├─ add() ← 사용됨
├─ multiply() ← 미사용
├─ divide() ← 미사용
└─ subtract() ← 미사용
After (Tree Shaking 후)
math.js
└─ add() ← 이것만 번들에 포함
나머지 3개 함수는 번들에서 제거됨
= 번들 사이즈 75% 감소
Code Splitting
하나의 거대한 번들 대신, 라우트나 기능 단위로 나눠서 필요할 때만 로드. 초기 로딩 속도를 크게 개선한다.
단일 번들
2.1 MB
bundle.js
모든 페이지 코드 포함
분할된 청크
main
120KB
vendor
340KB
/map
45KB
/detail
30KB
/list
60KB
/admin
90KB
사용자가 /home에 접속하면 main + vendor + /home만 로드 = 초기 505KB (vs 2.1MB)
Minification & Compression
코드에서 공백, 주석, 긴 변수명을 제거(Minify)하고, 네트워크 전송 시 압축(gzip/brotli).
Size Reduction Pipeline
원본 소스
Minified
gzip
Brotli
원본 대비 92% 감소 — Brotli가 gzip보다 ~20% 더 작다
HMR (Hot Module Replacement)
코드를 수정하면 전체 페이지를 새로고침하지 않고, 변경된 모듈만 교체. 상태(state)가 유지되므로 개발 속도가 빠르다.
Full Reload (전체 새로고침)
· 전체 페이지 리로드
· 모든 상태 초기화
· 네트워크 요청 재시작
· 2~5초 소요
HMR (핫 교체)
· 변경된 모듈만 교체
· React 상태 유지
· CSS 즉시 반영
· 50ms 이내 (Vite)
Transpilation (트랜스파일)
최신 문법(TS, JSX, ES2024)을 구형 브라우저가 이해할 수 있는 JS로 변환. Babel이 표준이었지만, SWC(Rust)로 빠르게 전환 중.
| 항목 | Babel | SWC |
|---|---|---|
| 언어 | JavaScript | Rust |
| 속도 | 기준 (1x) | 20~70x 빠름 |
| 사용처 | 레거시 프로젝트 | Next.js, Vite, Turbopack |
| 플러그인 | 풍부한 생태계 | 제한적이나 빠르게 성장 |
| TypeScript | 타입 제거만 (체크 X) | 타입 제거만 (체크 X) |
Source Map
Minify된 코드를 디버깅할 때, 원본 소스 위치로 매핑해주는 파일..map 파일로 생성된다.
빌드 결과 (Minified)
function a(b,c){return b+c}
var d=a(1,2);console.log(d);원본 소스 (읽기 쉬움)
function add(x, y) {
return x + y;
}
const result = add(1, 2);
console.log(result);브라우저 DevTools에서 에러 발생 시 원본 파일의 정확한 줄 번호를 보여준다.
프로덕션에서는 공개하지 않는 것이 보안상 좋다
번들 분석 & 최적화
번들을 줄이는 실전 기법
Bundle Treemap (예시)
각 영역의 크기가 번들 내 비중을 나타낸다
react-dom
128KB
lodash
72KB
moment
66KB
axios
14KB
zod
12KB
framer
45KB
내 코드
35KB
기타
28KB
npx next build --profile 또는 webpack-bundle-analyzer로 실제 분석 가능
Dynamic Import & Lazy Loading
초기 번들에 포함하지 않고, 필요한 시점에 비동기로 로드.import()함수가 핵심이다.
정적 Import (항상 포함)
import HeavyChart from './HeavyChart'; // 사용 여부와 관계없이 // 항상 메인 번들에 포함됨
Dynamic Import (필요 시 로드)
const HeavyChart = lazy(
() => import('./HeavyChart')
);
// 컴포넌트가 렌더링될 때만 로드
// 별도 chunk로 분리됨Bundle Optimization Checklist
배포 속도 개선
빌드와 배포를 빠르게 하려면 무엇을 수정해야 하는가?
빌드가 느리면 배포도 느리다. 개발 생산성, CI/CD 파이프라인 속도, 배포 주기 모두 빌드 속도에 달려 있다.
Webpack → Vite 또는 Turbopack으로 전환. 개발 서버 시작이 수 초 → 수백 ms로 단축.
트랜스파일러를 SWC로 교체하면 20~70배 빌드 속도 향상. Next.js는 이미 기본 SWC.
사용하지 않는 패키지 제거. lodash 전체 대신 lodash-es, moment 대신 dayjs.
CI에서 node_modules 캐싱. pnpm store 캐싱. 빌드 캐시(.next/cache) 보존.
TypeScript 타입 체크와 빌드를 분리해서 병렬 실행. tsc --noEmit과 빌드를 동시에.
COPY package.json과 COPY . 을 분리. 의존성 레이어가 캐시되면 install 스킵.
라우트별 청크 분리. 거대한 vendor 번들을 분할해서 변경되지 않는 부분은 캐시 활용.
프로덕션에서 Source Map을 hidden 또는 off로 설정. 빌드 시간과 결과물 크기 모두 감소.
Before (캐시 안 되는 Dockerfile)
COPY . . RUN pnpm install RUN pnpm build # 코드 1줄만 바꿔도 # install부터 다시 실행됨
After (레이어 캐시 활용)
COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build # package.json 안 바뀌면 # install 레이어 캐시 히트!
CI/CD Speed Optimization Flow
pnpm install
TypeScript
Lint
Build
Docker
각 단계의 캐시와 병렬화를 조합하면 전체 파이프라인을 50~70% 단축할 수 있다
Module Federation
서로 다른 앱이 런타임에 코드를 공유하는 기술
Module Federation 한눈에 보기
한 줄 비유
앱 A가 빌드할 때 앱 B의 컴포넌트를 가져와서 쓸 수 있다.
마치 레고 세트끼리 블록을 빌려 쓰는 것.
npm으로 설치하는 게 아니라, 브라우저가 실행 중일 때 네트워크로 가져온다.
Host (소비자)
쇼핑몰 앱
자기 코드 + 다른 앱에서 가져온 컴포넌트
Remote (제공자)
결제 앱
Cart 컴포넌트를 외부에 노출
Remote (제공자)
리뷰 앱
Review 컴포넌트를 외부에 노출
기존 방식 (npm 패키지)
공유 코드를 npm에 배포
버전 올리고 publish
모든 앱에서 npm install
각 앱마다 다시 설치
모든 앱을 다시 빌드
공유 코드가 각 번들에 복사됨
모든 앱을 다시 배포
버튼 하나 바꿔도 전부 재배포
Module Federation
결제 앱만 빌드 & 배포
Cart 컴포넌트가 업데이트됨
쇼핑몰 앱은 아무것도 안 함
재빌드 X, 재배포 X
브라우저가 최신 Cart를 가져옴
네트워크로 최신 버전 로드
독립 배포 + 자동 반영
각 팀이 자기 앱만 관리
Module Federation = 각 앱이 독립 배포하면서도 런타임에 코드를 공유
How It Works (작동 순서)
Remote 앱이 빌드될 때
노출할 컴포넌트 목록이 담긴 remoteEntry.js 파일을 생성한다. 이 파일이 일종의 '메뉴판'이다.
Host 앱이 브라우저에 로드될 때
Remote의 remoteEntry.js URL을 알고 있다. 아직 아무것도 다운로드하지 않는다.
Remote 컴포넌트가 필요한 순간
사용자가 해당 컴포넌트가 있는 페이지에 진입하면, 그때 네트워크로 Remote 코드를 가져온다. (Lazy Loading)
공유 의존성은 중복 로드 안 함
Host와 Remote가 같은 React 버전을 쓰면, 한 번만 로드한다. 버전이 다르면 각각 로드. (Shared 설정)
Shared Dependencies (공유 의존성)
쇼핑몰 앱
react@18.2
react-dom@18.2
zustand@4.5
react 1번만 로드
결제 앱
react@18.2
react-dom@18.2
stripe@12.0
같은 버전의 react는 한 번만 로드된다. 버전이 다르면 각각 로드 (안전 우선).
이럴 때 쓴다
· 여러 팀이 각자 다른 앱을 만드는 대규모 조직
· 공통 컴포넌트(헤더, 로그인 등)를 여러 앱이 공유
· 앱마다 독립 배포 주기를 가져야 할 때
· 마이크로 프론트엔드 아키텍처
이럴 때 안 쓴다
· 앱이 1개뿐인 프로젝트
· 팀 규모가 작은 경우
· 공유할 코드가 별로 없는 경우
· 런타임 네트워크 의존이 부담될 때
Remote 앱 설정 (결제 앱)
// webpack.config.js
new ModuleFederationPlugin({
name: 'paymentApp',
filename: 'remoteEntry.js',
// 외부에 노출할 컴포넌트
exposes: {
'./Cart': './src/Cart',
'./Checkout': './src/Checkout',
},
// 중복 로드 방지
shared: ['react', 'react-dom'],
})Host 앱 설정 (쇼핑몰 앱)
// webpack.config.js
new ModuleFederationPlugin({
name: 'shopApp',
// 가져올 Remote 앱 목록
remotes: {
paymentApp:
'paymentApp@https://pay.example.com'
+ '/remoteEntry.js',
},
shared: ['react', 'react-dom'],
})
// 사용할 때
const Cart = lazy(
() => import('paymentApp/Cart')
);Micro Frontend Architecture
Module Federation으로 구성한 마이크로 프론트엔드 전체 구조
Shell App (Host)
/products
상품 앱
팀 A
독립 배포
/cart
결제 앱
팀 B
독립 배포
/account
계정 앱
팀 C
독립 배포
각 라우트가 별도 앱 → 별도 팀 → 별도 배포 → Shell이 런타임에 조합
Summary
pnpm으로 의존성을 빠르고 안전하게 설치하고
Vite / Turbopack으로 빌드를 가속하고
Tree Shaking + Code Splitting으로 번들을 최소화하고
캐시 + 병렬화로 배포를 단축한다
좋은 번들링은 개발 경험과
사용자 경험을
동시에 개선한다