Frontend Build System

Bundle & Build 가이드

패키지 매니저부터 번들러, 빌드 최적화, 배포 속도까지

Build Pipeline Overview

소스코드.ts .jsx .css
의존성 설치pnpm install
번들러Webpack / Vite
최적화Minify · Tree Shake
빌드 결과물.js .css chunks
배포CDN / Server
1

패키지 매니저

npm · yarn · pnpm — 의존성을 설치하고 관리하는 도구

프로젝트에 필요한 라이브러리(의존성)를 다운로드하고, 버전을 고정하고,node_modules폴더에 정리하는 도구다. 셋 다 같은 역할이지만 설치 방식과 디스크 구조가 다르다.

npm / yarn (flat)

node_modules/

├─ react/

├─ react-dom/

├─ lodash/

├─ scheduler/ ← 유령 의존성

└─ ...수백 개 flat

모든 패키지가 최상위에 flat하게 설치됨.
직접 설치하지 않은 패키지도 접근 가능 (유령 의존성)

VS

pnpm (symlink + content-addressable)

node_modules/

├─ .pnpm/ ← 실제 파일 저장소

│  ├─ react@18.2.0/

│  ├─ react-dom@18.2.0/

│  └─ lodash@4.17/

├─ react symlink

└─ lodash symlink

글로벌 저장소에서 하드링크 → 디스크 절약
직접 설치한 패키지만 접근 가능 (유령 의존성 차단)

pnpm은 하드링크 + 심볼릭 링크로 디스크를 절약하면서 유령 의존성을 차단한다

항목npmyarnpnpm
설치 속도보통빠름가장 빠름
디스크 사용프로젝트마다 복사프로젝트마다 복사글로벌 저장소 공유
Lockfilepackage-lock.jsonyarn.lockpnpm-lock.yaml
유령 의존성허용 (flat)허용 (flat)차단 (strict)
Monorepoworkspacesworkspacesworkspace + 필터
Node 버전관리nvm 별도corepackcorepack / 내장

Lockfile이란?

package.json에는 "react": "^18.2.0"처럼 버전 범위가 적혀 있다. 설치 시점에 따라 실제 설치되는 버전이 달라질 수 있다. Lockfile은 “이번에 정확히 이 버전을 설치했다”를 기록해서, 팀원 전원이 동일한 버전을 쓸 수 있게 보장한다. 반드시 Git에 커밋해야 한다.

2

모듈 시스템

CommonJS vs ES Modules — 코드를 나누고 합치는 규칙

JavaScript 파일 하나가 다른 파일의 함수를 가져다 쓰려면 “모듈 시스템”이 필요하다. 현재 두 가지가 공존한다.

CommonJS (CJS)

// 내보내기
module.exports = { add, multiply };

// 가져오기
const { add } = require('./math');

· 동기 로딩

· Tree Shaking 어려움

· Node.js 전통 방식

VS

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'를 만나면 어디서 찾는가?

1./node_modules/react현재 프로젝트
2../node_modules/react상위 디렉토리
3../../node_modules/react계속 상위로
4전역 node_modules글로벌 설치 경로

node_modules를 디렉토리 트리를 타고 올라가며 탐색한다. pnpm은 심볼릭 링크로 이 과정을 최적화.

3

번들러

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

Webpack1세대 · JavaScript

가장 오래되고 널리 쓰이는 번들러. 풍부한 플러그인 생태계. 설정이 복잡하지만 무엇이든 가능.

Loader 시스템Plugin 생태계Code SplittingHMRModule Federation

적합: 대규모 프로젝트, 레거시, 세밀한 제어 필요 시

Vite차세대 · ESM + Rollup

개발 서버는 ESM 네이티브, 프로덕션은 Rollup 기반. 설정이 간단하고 DX가 뛰어남.

ESM Dev ServerRollup 프로덕션빠른 HMRCSS Modules 내장Plugin 호환

적합: 새 프로젝트, React/Vue/Svelte

esbuild네이티브 · Go

Go로 작성, 극한의 속도. 다른 번들러의 내부 트랜스파일러로도 사용됨.

10~100x 빠른 빌드TypeScript 네이티브JSX 변환Tree ShakingCSS 번들링

적합: 라이브러리 빌드, 빠른 트랜스파일

Turbopack차세대 · Rust

Vercel이 만든 Webpack 후속. Rust로 작성, 증분 빌드 최적화. Next.js에 내장.

Rust 기반증분 컴파일Next.js 통합Webpack 호환 목표메모리 캐싱

적합: Next.js 프로젝트

Build Speed Comparison (상대적)

esbuild

Go (네이티브)
95%

Turbopack

Rust (네이티브)
85%

SWC

Rust (트랜스파일)
80%

Vite (dev)

ESM + esbuild
70%

Rollup

JS (프로덕션)
40%

Webpack

JS (레거시)
25%

네이티브 언어(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 미니파이
→ 최적화된 정적 파일
4

빌드 핵심 개념

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

원본 소스

500 KB
100%

Minified

180 KB
36%

gzip

55 KB
11%

Brotli

42 KB
8%

원본 대비 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)로 빠르게 전환 중.

항목BabelSWC
언어JavaScriptRust
속도기준 (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);
.map↕

원본 소스 (읽기 쉬움)

function add(x, y) {
  return x + y;
}
const result = add(1, 2);
console.log(result);

브라우저 DevTools에서 에러 발생 시 원본 파일의 정확한 줄 번호를 보여준다.
프로덕션에서는 공개하지 않는 것이 보안상 좋다

5

번들 분석 & 최적화

번들을 줄이는 실전 기법

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

lodash → lodash-es (Tree Shaking 가능)
moment.js → dayjs (97% 작음)
큰 라이브러리는 dynamic import
이미지는 next/image 또는 WebP/AVIF
CSS-in-JS → Tailwind (런타임 0)
barrel export (index.ts) 최소화
사용하지 않는 의존성 제거
bundle-analyzer로 주기적 점검
6

배포 속도 개선

빌드와 배포를 빠르게 하려면 무엇을 수정해야 하는가?

빌드가 느리면 배포도 느리다. 개발 생산성, CI/CD 파이프라인 속도, 배포 주기 모두 빌드 속도에 달려 있다.

01번들러 교체

Webpack → Vite 또는 Turbopack으로 전환. 개발 서버 시작이 수 초 → 수백 ms로 단축.

02Babel → SWC

트랜스파일러를 SWC로 교체하면 20~70배 빌드 속도 향상. Next.js는 이미 기본 SWC.

03의존성 최소화

사용하지 않는 패키지 제거. lodash 전체 대신 lodash-es, moment 대신 dayjs.

04캐시 활용

CI에서 node_modules 캐싱. pnpm store 캐싱. 빌드 캐시(.next/cache) 보존.

05병렬 빌드

TypeScript 타입 체크와 빌드를 분리해서 병렬 실행. tsc --noEmit과 빌드를 동시에.

06Docker 레이어 최적화

COPY package.json과 COPY . 을 분리. 의존성 레이어가 캐시되면 install 스킵.

07Code Splitting 최적화

라우트별 청크 분리. 거대한 vendor 번들을 분할해서 변경되지 않는 부분은 캐시 활용.

08Source Map 프로덕션 비활성화

프로덕션에서 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

pnpm store 캐시 → 70% 단축
30%

TypeScript

tsc --noEmit 병렬 실행
20%

Lint

변경 파일만 lint (lint-staged)
15%

Build

SWC + 빌드 캐시 활용
40%

Docker

레이어 캐시 + multi-stage
25%

각 단계의 캐시와 병렬화를 조합하면 전체 파이프라인을 50~70% 단축할 수 있다

7

Module Federation

서로 다른 앱이 런타임에 코드를 공유하는 기술

Module Federation 한눈에 보기

한 줄 비유

앱 A가 빌드할 때 앱 B의 컴포넌트를 가져와서 쓸 수 있다.
마치 레고 세트끼리 블록을 빌려 쓰는 것.
npm으로 설치하는 게 아니라, 브라우저가 실행 중일 때 네트워크로 가져온다.

Host (소비자)

쇼핑몰 앱

Header자체
ProductList자체
Cart결제 앱
Review리뷰 앱

자기 코드 + 다른 앱에서 가져온 컴포넌트

런타임 로드↕

Remote (제공자)

결제 앱

Cart자체
Checkout자체

Cart 컴포넌트를 외부에 노출

Remote (제공자)

리뷰 앱

Review자체
Rating자체

Review 컴포넌트를 외부에 노출

기존 방식 (npm 패키지)

배포

공유 코드를 npm에 배포

버전 올리고 publish

설치

모든 앱에서 npm install

각 앱마다 다시 설치

빌드

모든 앱을 다시 빌드

공유 코드가 각 번들에 복사됨

배포

모든 앱을 다시 배포

버튼 하나 바꿔도 전부 재배포

VS

Module Federation

배포

결제 앱만 빌드 & 배포

Cart 컴포넌트가 업데이트됨

자동

쇼핑몰 앱은 아무것도 안 함

재빌드 X, 재배포 X

런타임

브라우저가 최신 Cart를 가져옴

네트워크로 최신 버전 로드

결과

독립 배포 + 자동 반영

각 팀이 자기 앱만 관리

Module Federation = 각 앱이 독립 배포하면서도 런타임에 코드를 공유

How It Works (작동 순서)

1

Remote 앱이 빌드될 때

노출할 컴포넌트 목록이 담긴 remoteEntry.js 파일을 생성한다. 이 파일이 일종의 '메뉴판'이다.

2

Host 앱이 브라우저에 로드될 때

Remote의 remoteEntry.js URL을 알고 있다. 아직 아무것도 다운로드하지 않는다.

3

Remote 컴포넌트가 필요한 순간

사용자가 해당 컴포넌트가 있는 페이지에 진입하면, 그때 네트워크로 Remote 코드를 가져온다. (Lazy Loading)

4

공유 의존성은 중복 로드 안 함

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)

Header
Navigation
Footer

/products

상품 앱

팀 A

독립 배포

/cart

결제 앱

팀 B

독립 배포

/account

계정 앱

팀 C

독립 배포

각 라우트가 별도 앱 → 별도 팀 → 별도 배포 → Shell이 런타임에 조합

Summary

pnpm으로 의존성을 빠르고 안전하게 설치하고

Vite / Turbopack으로 빌드를 가속하고

Tree Shaking + Code Splitting으로 번들을 최소화하고

캐시 + 병렬화로 배포를 단축한다

좋은 번들링은 개발 경험
사용자 경험
동시에 개선한다