FEInterview Prep

build-tools

번들러와 빌드 시스템: Webpack, Vite, ESM, Tree Shaking, Code Splitting

번들러는 프론트엔드 개발 환경의 핵심입니다. "Vite가 왜 빠른가"를 ESBuild와 Native ESM으로 설명할 수 없다면, "Tree Shaking이 왜 CommonJS에서는 안 되는가"를 모른다면 시니어 수준이 아닙니다. 빌드 시스템 선택은 개발자 경험과 프로덕션 성능 모두에 직접 영향을 줍니다.

시작 전
이해도
매우 낮음

학습 개요

탄생 배경

해결하려 했던 문제

초기 웹에서는 모든 JavaScript를 하나의 파일에 작성하거나 script 태그로 순서에 의존하여 로드했습니다. 의존성 관리가 전혀 없어 변수 충돌, 로드 순서 오류가 빈번했습니다. 모듈 시스템(CommonJS, AMD)이 등장했지만, 브라우저는 이를 직접 지원하지 않았습니다. 브라우저가 이해할 수 있는 형태로 변환하는 도구가 필요했습니다.

역사적 맥락

2009년 Node.js와 CommonJS 모듈 시스템 등장 → 2011년 RequireJS(AMD) → 2012년 Browserify(CommonJS를 브라우저용으로) → 2014년 Webpack 등장 → 2015년 ES6 Modules(ESM) 표준화 → 2016년 Rollup(Tree Shaking 대중화) → 2020년 Vite 등장(ESM + ESBuild) → 2021년 ESBuild, SWC(Rust 기반 트랜스파일러) → 2022년 Turbopack(Vercel, Rust 기반) → 2023년 Rspack(Webpack 호환 Rust 번들러)

이전에는 어떻게 했나

Grunt, Gulp 같은 태스크 러너가 번들러 이전에 빌드 자동화를 담당했습니다. 파일 연결(concatenation)과 최소화(minification)를 수동으로 설정했습니다.

멘탈 모델

동작 원리

[모듈 시스템의 역사] IIFE 패턴: (function() { /* 전역 변수 오염 방지 */ })() CommonJS (Node.js): require(), module.exports → 동기적, 런타임 평가 AMD: define([], function() {}) → 비동기 로딩, 브라우저용 UMD: CommonJS + AMD + 전역 변수 지원하는 호환 포맷 ESM (ES6): import/export → 정적 분석 가능, 비동기, 브라우저 네이티브 지원 [CommonJS vs ESM의 근본적 차이] CommonJS: const module = require('./module'); // 런타임에 평가 if (condition) { const mod = require('./conditional'); // 조건부 require 가능 } ESM: import { func } from './module'; // 파싱 타임에 정적으로 분석 // import는 항상 최상위에 위치해야 함 // 번들러가 빌드 타임에 어떤 export가 사용되는지 알 수 있음 Tree Shaking이 ESM에서만 가능한 이유: - ESM: 의존성 그래프가 정적으로 결정 → 사용되지 않는 export를 빌드 타임에 제거 가능 - CommonJS: require가 런타임에 동적으로 실행 → 어떤 export가 사용될지 알 수 없음 (require는 조건부, 반복문, 변수명으로도 사용 가능) [Webpack 동작 원리] 1. Entry: 의존성 그래프 탐색 시작점 (index.tsx) 2. 의존성 그래프 생성: index.tsx → import App → import Button → import styled... 모든 import를 재귀적으로 추적하여 모듈 그래프 생성 3. Loader 적용: 각 파일을 JavaScript로 변환 (ts-loader, css-loader, file-loader 등) module.rules에서 test(파일 패턴)에 맞는 loader 적용 4. Plugin 실행: HtmlWebpackPlugin: HTML 파일 생성 MiniCssExtractPlugin: CSS를 별도 파일로 추출 5. Chunk 생성: SplitChunksPlugin: 공통 의존성 분리 Dynamic import: 코드 스플리팅으로 별도 청크 생성 6. Output: 최종 번들 파일 생성 [Vite가 개발 서버에서 빠른 이유] 문제: Webpack 개발 서버는 시작 시 전체 번들을 생성 → 파일 수 많을수록 느림. Vite의 접근: 1. 사전 번들링 (Pre-bundling): node_modules의 CommonJS/UMD 패키지를 ESBuild로 ESM으로 변환 및 캐시. ESBuild는 Go 언어로 작성되어 Webpack/Babel보다 10~100배 빠름. 2. Native ESM 개발 서버: 전체 번들을 만들지 않음. 브라우저가 필요한 파일을 요청할 때마다 On-demand로 변환하여 제공. import 체인을 따라 필요한 파일만 변환. 3. HMR (Hot Module Replacement): 변경된 파일만 재변환하여 교체. Webpack은 변경된 모듈과 그 영향을 받는 모듈 체인을 재번들링. Vite는 변경된 파일만 서버에서 무효화, 브라우저가 해당 모듈만 재요청. 프로덕션 빌드: Vite도 Rollup으로 번들링 (브라우저 수백 개의 모듈 요청 비효율 방지). [Code Splitting] 목적: 초기 번들 크기를 줄여 첫 로딩을 빠르게. Dynamic import를 사용하면 번들러가 별도 청크로 분리: const HeavyComponent = lazy(() => import('./HeavyComponent')); → HeavyComponent.js는 별도 청크로 생성되어 필요할 때만 로드. Route-based splitting: 라우트별로 청크 분리 (가장 일반적인 패턴). Component-based splitting: 초기에 필요 없는 무거운 컴포넌트 지연 로드. Vendor splitting: react, lodash 등 변경이 적은 라이브러리를 별도 청크로 분리 → 캐시 효율. [Source Map] 번들된 코드를 원본 소스 코드로 매핑하는 파일. 프로덕션 에러 발생 시 Sentry 같은 도구가 Source Map을 사용하여 원본 파일/라인을 특정. .map 파일을 서버에 업로드하고 브라우저에 공개하지 않는 것이 보안 모범 사례.

핵심 구성 요소

Entry

Webpack이 의존성 그래프를 시작하는 진입점 파일

Loader

JavaScript가 아닌 파일(TS, CSS, 이미지)을 모듈로 변환

Plugin

번들 최적화, HTML 생성, 환경변수 주입 등 빌드 과정 확장

Chunk

번들을 나누는 단위. Entry chunk, Dynamic import chunk, Vendor chunk

Tree Shaking

사용되지 않는 export를 번들에서 제거. ESM에서만 완전히 동작

ESBuild

Go로 작성된 초고속 JavaScript/TypeScript 번들러. Vite의 핵심

흐름 설명


[Vite 개발 서버 요청 처리 흐름]
1. 브라우저: localhost:5173/index.html 요청
2. Vite: index.html 반환 (bundled X, 원본 그대로)
3. 브라우저: HTML 파싱 → <script type="module" src="/src/main.tsx"> 발견
4. 브라우저: /src/main.tsx 요청
5. Vite: main.tsx → TypeScript 변환 → ESM 형식으로 반환
6. 브라우저: main.tsx 파싱 → import 발견
7. 브라우저: 각 import 파일 병렬 요청
8. Vite: 각 파일 On-demand 변환 후 반환
(사전 번들링된 node_modules는 캐시에서 즉시 반환)

[HMR 업데이트 흐름]
1. 개발자: Button.tsx 파일 저장
2. Vite: 파일 변경 감지 (file watcher)
3. Vite: Button.tsx가 포함된 HMR 경계(boundary) 계산
4. Vite → 브라우저: WebSocket으로 업데이트 알림
5. 브라우저: 해당 모듈만 새로 요청
6. 브라우저: 기존 모듈 교체, 상태 유지 (React Fast Refresh)
    

코드 예제


// Tree Shaking이 동작하는 경우 (ESM)
// utils.ts
export function usedFunction() { return 'used'; }
export function unusedFunction() { return 'unused'; } // 번들에서 제거됨!

// main.ts
import { usedFunction } from './utils'; // unusedFunction은 import하지 않음
usedFunction();

// Tree Shaking이 안 되는 경우 (사이드 이펙트)
// sideEffect.ts
console.log('모듈 로드 시 실행!'); // import만 해도 이 코드가 실행됨
export function func() {}

// package.json에서 사이드 이펙트 없는 파일 명시
// { "sideEffects": false } 또는 ["./src/polyfills.ts"]

// Code Splitting - Route-based
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const HomePage = lazy(() => import('./pages/HomePage'));      // 별도 청크
const AboutPage = lazy(() => import('./pages/AboutPage'));    // 별도 청크
const DashboardPage = lazy(() => import('./pages/DashboardPage')); // 별도 청크

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/dashboard" element={<DashboardPage />} />
      </Routes>
    </Suspense>
  );
}

// Webpack 설정 - Vendor splitting
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\/]node_modules[\/]/,
          name: 'vendors',
          chunks: 'all',
          // react, react-dom은 별도 청크 (거의 변경 안 됨 → 장기 캐싱)
        },
        react: {
          test: /[\/]node_modules[\/](react|react-dom)[\/]/,
          name: 'react-vendor',
          chunks: 'all',
        },
      },
    },
  },
};

// Vite 설정
import { defineConfig } from 'vite';
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-tooltip'],
        },
      },
    },
    sourcemap: true, // 프로덕션 Source Map 생성
  },
});
    

비교 분석

번들러와

vs

Webpack

비교 관점
이 방식
Webpack
개발 서버 시작 속도
Vite: 즉시 시작 (Native ESM, 번들링 없음)
Webpack: 전체 번들 생성 후 시작 (파일 수에 비례)
HMR 속도
Vite: 변경 파일만 교체 (거의 즉각)
Webpack: 영향 받는 모듈 체인 재번들링
프로덕션 빌드
Vite: Rollup 기반 (최적화된 번들)
Webpack: 검증된 최적화, 풍부한 플러그인
설정 복잡도
Vite: 제로 설정 시작 가능, 간결한 설정
Webpack: 강력하지만 복잡한 설정 (webpack.config.js)
생태계
Vite: 빠르게 성장 중, 주요 프레임워크 공식 지원
Webpack: 매우 성숙한 생태계, 다양한 플러그인

번들러와

vs

CommonJS

비교 관점
이 방식
CommonJS
Tree Shaking
ESM: 정적 분석으로 미사용 export 제거 가능
CommonJS: 동적 require로 불가능
실행 타이밍
ESM: 비동기 (Top-level await 지원)
CommonJS: 동기적 require (블로킹)
브라우저 지원
ESM: 네이티브 브라우저 지원
CommonJS: 브라우저에서 직접 사용 불가 (번들러 필요)
순환 참조
ESM: 라이브 바인딩으로 처리
CommonJS: 처리 가능하나 동작이 복잡

트레이드오프

이상적인 사용 사례

면접 질문