Webpack 기초
entry·output·loader·plugin의 역할을 이해하고 JavaScript·CSS·이미지 의존성을 브라우저용 번들로 빌드합니다.
React 같은 SPA는 수많은 자바스크립트 모듈과 CSS, 이미지를 씁니다. 이 파일들은 개발할 때는 나뉘어 있는 편이 관리하기 좋지만, 그대로 브라우저에서 실행하려 하면 몇 가지 문제가 생깁니다. 수백 개의 파일을 하나하나 요청하면 네트워크 지연이 커지고, 모듈끼리 서로 의존하는 순서를 직접 관리하기도 어렵습니다. JSX나 타입스크립트, Sass 같은 문법은 브라우저가 바로 이해하지 못하고, 코드 압축 같은 배포 전 최적화를 손으로 하기도 비효율적입니다.
이런 문제를 풀어 주는 도구가 모듈 번들러(module bundler)입니다. webpack은 모듈의 의존 관계를 따라가며, 로더(loader)와 플러그인(plugin)을 직접 설정해 원하는 결과물을 만들 수 있는 번들러입니다. 이 글에서는 webpack의 핵심 개념을 익히고, 간단한 React 프로젝트를 webpack으로 빌드해 봅니다. 글을 쓰는 시점의 버전은 webpack 5.112, webpack-cli 7.3, webpack-dev-server 6.1입니다.
모듈 번들러
모듈 번들러는 애플리케이션을 이루는 자바스크립트, CSS, 이미지 같은 모듈을 분석해 서로의 의존 관계를 파악하고, 브라우저에서 실행할 수 있는 하나 또는 몇 개의 파일, 즉 번들(bundle)로 합쳐 주는 도구입니다.
번들러는 먼저 import나 require()로 이어진 모듈을 찾아 하나의 의존성 그래프(dependency graph)를 만들고, 수많은 모듈을 브라우저가 효율적으로 불러올 수 있는 몇 개의 파일로 묶습니다. Babel 같은 도구와 연결해 최신 자바스크립트와 JSX를 대상 브라우저가 이해하는 코드로 바꾸고, CSS나 이미지, 글꼴 같은 자바스크립트가 아닌 에셋도 모듈처럼 다룹니다. 또 코드 압축과 트리 셰이킹으로 결과물의 크기를 줄이고, 개발할 때는 개발 서버를 띄워 코드가 바뀌면 페이지를 새로 고치거나, 바뀐 모듈만 실행 중에 교체하는 HMR(hot module replacement)로 빠르게 결과를 보여 줍니다.
webpack 말고도 React 기초에서 쓴 Vite나 Parcel, Rollup 같은 도구가 있습니다. 도구를 비교할 때는 개발 서버와 번들링, 설정을 확장하는 방식이 각각 어떤 역할을 하는지와 프로젝트의 조건을 함께 봅니다.
webpack의 핵심 개념
엔트리(entry)는 webpack이 번들링을 시작하는 파일로, 보통 애플리케이션의 메인 자바스크립트 파일입니다. webpack은 엔트리에서 시작해 의존하는 모듈을 차례로 따라가며 의존성 그래프를 만들며, webpack.config.js의 entry로 정합니다.
module.exports = {
entry: './src/index.js', // 단일 엔트리
// entry: { app: './src/app.js', vendor: './src/vendor.js' }, // 다중 엔트리
};주석으로 막아 둔 다중 엔트리처럼 엔트리를 여러 개 두면, 엔트리마다 의존성 그래프와 번들이 따로 만들어집니다.
출력(output)은 webpack이 만든 번들을 저장할 경로와 파일 이름으로, output으로 정합니다. path에는 절대 경로를 적어야 하므로, 다음 코드는 Node.js의 path.resolve()로 설정 파일이 있는 폴더(__dirname) 아래의 dist를 계산합니다.
const path = require('path');
module.exports = {
output: {
filename: 'bundle.js', // 번들 파일 이름
path: path.resolve(__dirname, 'dist'), // 번들 파일 저장 경로
clean: true, // 빌드 전 output 디렉토리 정리
},
};clean: true는 빌드하기 전에 출력 폴더의 이전 결과물을 지웁니다.
로더는 특정 종류의 파일을 webpack이 다룰 수 있는 모듈로 바꿉니다. webpack은 기본으로 자바스크립트와 JSON을 처리하고, webpack 5의 에셋 모듈(asset modules)로 이미지와 글꼴도 다룰 수 있지만, CSS를 처리하거나 JSX를 변환하려면 로더를 연결해야 합니다. css-loader, style-loader, babel-loader처럼 이미 만들어진 로더를 module.rules 배열에 추가해 씁니다.
module.exports = {
module: {
rules: [
{
test: /\.css$/, // .css 확장자를 가진 파일에 적용
use: ['style-loader', 'css-loader'], // 적용할 로더 배열
},
{
test: /\.js$/, // .js 확장자를 가진 파일에 적용
exclude: /node_modules/, // node_modules 디렉토리 제외
use: {
loader: 'babel-loader', // 최신 문법을 대상 환경에 맞게 변환
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
],
},
};한 규칙의 use에 로더를 여러 개 적으면 배열의 오른쪽부터 차례로 적용됩니다. 그래서 ['style-loader', 'css-loader']는 먼저 css-loader가 CSS 파일을 자바스크립트 모듈로 바꾸고, 그 결과를 받은 style-loader가 브라우저에서 실행될 때 <style> 요소로 넣는 코드를 만듭니다(webpack의 로더 문서).
플러그인은 로더가 파일 하나를 바꾸는 것과 달리, 번들링 과정 전체에 걸쳐 추가 작업을 합니다. 결과물 압축, HTML 파일 생성, CSS를 별도 파일로 꺼내기, 환경 변수 넣기처럼 로더로는 할 수 없는 일을 하며, plugins 배열에 정의합니다.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', // HTML 템플릿 파일 지정
}),
new MiniCssExtractPlugin({
filename: 'styles/[name].css', // CSS 파일을 별도 파일로 추출
}),
],
};HtmlWebpackPlugin은 템플릿 HTML을 바탕으로, 만든 번들을 불러오는 <script>가 들어간 HTML을 만들어 줍니다. MiniCssExtractPlugin은 CSS를 별도의 파일로 꺼내는 플러그인인데, 실제로 꺼내려면 CSS 규칙에서 style-loader 대신 이 플러그인의 로더도 함께 연결해야 하므로 이 예제는 역할을 보여 주는 것입니다.
모드(mode)는 development, production, none 가운데 하나로, 모드에 따라 webpack 내부의 최적화 설정이 자동으로 적용됩니다. production은 코드 압축과 트리 셰이킹 같은 배포용 최적화를 켜고, development는 빠른 빌드와 디버깅을 위해 이런 최적화를 끕니다(webpack의 mode 문서).
module.exports = {
mode: 'development', // 'production' 또는 'none'으로 설정 가능
};webpack으로 React 앱 빌드하기
이제 간단한 React 프로젝트를 webpack으로 빌드하며 핵심 개념을 직접 써 봅니다. npm과 패키지 관리의 Node.js, npm, package.json을 알고 있으면 따라갈 수 있습니다. webpack-dev-server 6은 Node.js 22.15 이상, Babel 8은 Node.js 22.18 이상의 22.x나 24.11 이상이 필요하므로, Node.js 22.18 이상이나 24.11 이상에서 실습하세요. 버전을 적지 않은 설치는 실행하는 시점의 최신 버전을 받으므로, 결과를 다시 만들려면 생성된 잠금 파일과 Node.js, npm의 버전을 함께 기록합니다.
먼저 빈 실습 폴더를 만들고 패키지를 설치합니다.
mkdir my-webpack-app
cd my-webpack-app
npm init -y
npm pkg delete type # npm 11 이상이 넣는 "type": "commonjs"를 지움
mkdir public src
# React 및 webpack 관련 패키지 설치
npm install react react-dom
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin style-loader css-loader babel-loader @babel/core @babel/preset-env @babel/preset-reactnpm 11 이상의 npm init -y는 package.json에 "type": "commonjs"를 넣습니다. webpack은 이 필드가 commonjs인 패키지의 .js 파일을 CommonJS 모듈로만 해석하므로, 그대로 두면 src 파일의 import 문에서 빌드가 실패합니다. 그래서 npm pkg delete type으로 이 필드를 지워, webpack이 import와 require()를 모두 받아들이는 기본 방식으로 읽게 합니다. 이 필드가 없는 npm 10에서는 이 명령이 아무것도 바꾸지 않습니다.
public/index.html을 만듭니다. React가 화면을 그릴 root 요소만 두며, 번들을 불러오는 <script>는 HtmlWebpackPlugin이 넣어 줍니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Webpack React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>root 요소는 비어 있으므로, 번들이 실행되기 전이나 번들을 불러오지 못했을 때는 빈 화면만 보입니다.
엔트리 파일 src/index.js를 만듭니다. 이 파일이 의존성 그래프의 출발점입니다. App과 styles.css를 import하므로 webpack은 두 파일도 따라가 번들에 넣고, 실행할 때는 createRoot()로 root 요소를 찾아 그 안에 App을 그립니다.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'; // App 컴포넌트 임포트
import './styles.css'; // CSS 파일 임포트
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);자바스크립트 파일에서 CSS를 import할 수 있는 것은 언어의 기능이 아니라, 앞에서 본 css-loader와 style-loader 규칙 덕분입니다.
React 컴포넌트 src/App.js를 만듭니다. 버튼을 누를 때마다 count 상태가 1씩 늘고 화면의 숫자가 다시 그려지는 컴포넌트입니다. 확장자는 .js지만 JSX를 쓰므로, Babel이 이 파일을 변환해야 브라우저에서 실행됩니다.
// src/App.js
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>Hello, Webpack & React!</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
</div>
);
}
export default App;setCount(prev => prev + 1)처럼 함수를 넘기면 React가 그 시점의 상태 값을 prev로 넘겨 주므로, 이전 값에 1을 더한 결과로 갱신됩니다.
스타일 파일 src/styles.css를 만듭니다. body를 Flexbox로 만들어 .App 상자를 화면 가운데에 놓고, 버튼에 색과 호버 효과를 줍니다.
/* src/styles.css */
body {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.App {
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #0056b3;
}Babel 설정을 프로젝트 루트의 babel.config.json에 둡니다. @babel/preset-env는 최신 자바스크립트 문법을 대상 환경에 맞게 바꾸고, @babel/preset-react는 JSX를 자바스크립트로 바꿉니다.
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}Babel 8의 @babel/preset-react는 JSX를 바꿀 때 기본으로 automatic 런타임을 써서, JSX만 쓰는 App.js는 React를 가져오지 않아도 됩니다. index.js는 React.StrictMode를 쓰므로 React를 가져옵니다. Babel 설정은 이 파일 한곳에만 두고 webpack 설정에는 다시 적지 않아, 두 곳의 설정이 서로 달라지는 일을 막습니다. 대상 환경을 정하는 방법은 다음 글에서 다룹니다.
이제 webpack 설정 파일 webpack.config.js를 만듭니다.
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development', // 개발 모드 (배포할 때는 'production')
entry: './src/index.js', // 엔트리 (webpack이 빌드를 시작할 파일)
output: {
filename: 'bundle.js', // 빌드한 자바스크립트 파일 이름
path: path.resolve(__dirname, 'dist'), // 결과물을 저장할 폴더 (프로젝트 루트의 dist)
clean: true, // 빌드할 때 이전 결과물 삭제
},
resolve: {
extensions: ['.js', '.jsx'], // import할 때 확장자를 생략할 수 있게 함
},
module: {
rules: [
// .css 파일 처리 규칙
{
test: /\.css$/, // .css 파일에 적용
use: ['style-loader', 'css-loader'], // 오른쪽부터: css-loader가 모듈로 바꾸고 style-loader가 <style>로 주입
},
// .js, .jsx 파일 처리 규칙 (Babel로 최신 문법과 JSX 변환)
{
test: /\.jsx?$/, // .js 또는 .jsx 파일에 적용
exclude: /node_modules/, // node_modules는 제외 (빌드 시간 단축)
use: 'babel-loader', // babel.config.json의 설정을 사용
},
// 이미지 파일 처리 규칙 (webpack 5의 에셋 모듈)
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource', // 파일을 dist로 내보내고 URL을 반환
},
// 글꼴 파일 처리 규칙
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html', // HTML 템플릿 파일 경로
filename: 'index.html', // 만들 HTML 파일 이름
}),
],
devServer: {
port: 3000, // 개발 서버 포트
open: true, // 서버를 시작하면 브라우저 열기
hot: true, // HMR 사용
},
};webpack은 확장자 없이 가져온 경로에 기본으로 .js, .json, .wasm을 차례로 붙여 찾으므로, index.js의 import App from './App'은 이 설정이 없어도 App.js로 해석됩니다. resolve.extensions는 이 목록을 .js와 .jsx로 바꿔 .jsx 파일도 확장자 없이 가져올 수 있게 하며, 기본 목록을 덮어쓰므로 .json과 .wasm은 더 이상 저절로 붙지 않습니다. 개발 서버는 빌드한 결과를 메모리에 두고 제공하므로, dist 폴더를 따로 지정하지 않아도 됩니다.
package.json의 scripts에 start와 build를 추가합니다. 기존의 scripts 객체에 두 항목을 더하는 것이며, 다른 내용을 이 짧은 객체로 덮어쓰지 않습니다.
{
"scripts": {
"start": "webpack serve --mode development",
"build": "webpack --mode production"
}
}start는 개발 서버를 실행하고, build는 배포용 번들을 만듭니다. 개발 서버를 실행합니다.
npm start브라우저가 열리고 http://localhost:3000에서 React 앱이 실행됩니다. 코드를 고치면 페이지가 자동으로 새로 고쳐지거나 HMR이 적용됩니다. 배포용으로 빌드하려면 다음 명령을 실행합니다.
npm run builddist 폴더에 최적화된 index.html과 bundle.js가 만들어지며, 이 파일들을 웹 서버에 올리면 배포할 수 있습니다.
빌드와 실행 나누기
Webpack 빌드와 브라우저 실행 파이프라인 — 상태 추적
본문 Count 예제의 소스·빌드 산출물·브라우저 상태가 다른 시점에 존재한다. CSS loader 처리/주입과 클릭 상태 변화가 Webpack 전체 재빌드와 다름을 추적할 가치가 있다.
BUILD → RUNTIME
Webpack은 의존성을 빌드하고, 브라우저는 그 산출물을 실행합니다
이 절의 최종 설정은 src/index.js에서 App과 CSS를 따라가 dist/index.html과 bundle.js를 만듭니다. style-loader 설정에서는 CSS도 JavaScript를 통해 실행 시 주입됩니다.
입력 · 소스 의존성
src/index.js
├─ App.js
└─ styles.cssentry에서 import 관계를 따라갑니다. 이미지·폰트는 실제 import한 경우 asset/resource 규칙의 처리 대상이 됩니다.
Node에서 수행하는 빌드
JS·JSX: babel-loader가 문법과 JSX를 변환합니다.
CSS: css-loader → style-loader 순서로 처리합니다. 설정 배열은 [style-loader, css-loader]이지만 실행은 오른쪽부터입니다.
HtmlWebpackPlugin은 템플릿으로 HTML을 만듭니다. 플러그인은 빌드 생명주기의 여러 단계에 관여할 수 있습니다.
최종 설정의 주요 산출물
dist/
├─ index.html
└─ bundle.js이 설정의 JavaScript 파일명은 bundle.js입니다. CSS는 별도 파일로 추출하지 않고 번들에 포함해 실행 시 주입합니다. 추가 산출물은 가져온 자산과 플러그인 설정에 따라 달라집니다.
브라우저 · HTML → bundle.js
생성된 HTML이 번들을 로드합니다. style-loader 코드가 CSS를 style 요소로 넣고, React의 createRoot가 App을 렌더링합니다.
사용자 입력 → 화면 갱신
App의 Count 버튼을 누르면 상태가 바뀌고 React가 화면을 갱신합니다. Webpack 빌드 단계와 브라우저의 상태 갱신은 서로 다른 작업입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 빌드의 책임과 브라우저 상태 갱신을 나눈다 |
| 전체 관계·조건 설명 | 노드 5개·진행 관계 4개다. CSS chain은 css-loader 뒤 style-loader이며 최종 HTML+bundle을 브라우저가 실행한다. CSS style 주입과 Count 사용자 갱신은 런타임이고 클릭마다 Webpack 전체 빌드를 하는 것은 아니다. 실제 import한 이미지/폰트만 asset 규칙의 처리 대상이며 mobile도 같다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 소스와 import | 입력: JS·CSS import / 화면: 빌드 전 | entry에서 의존 파일을 찾는다 | 의존 그래프가 변환 대상 파일을 정한다 |
| 변환과 실행 | 산출: 번들 자원 / 브라우저: 초기 UI | loader/번들 산출을 브라우저가 읽고 CSS를 적용한다 | 빌드 단계와 브라우저 실행을 구분한다 |
| Count 클릭 | 앱 state: 증가 / 번들: 같은 파일 | 실행 중 앱의 count가 바뀐다 | 사용자 클릭은 소스 재빌드를 수행하지 않는다 |
Webpack 빌드와 브라우저 실행 파이프라인 — 상태 추적
본문 Count 예제의 소스·빌드 산출물·브라우저 상태가 다른 시점에 존재한다. CSS loader 처리/주입과 클릭 상태 변화가 Webpack 전체 재빌드와 다름을 추적할 가치가 있다.
BUILD → RUNTIME
Webpack은 의존성을 빌드하고, 브라우저는 그 산출물을 실행합니다
이 절의 최종 설정은 src/index.js에서 App과 CSS를 따라가 dist/index.html과 bundle.js를 만듭니다. style-loader 설정에서는 CSS도 JavaScript를 통해 실행 시 주입됩니다.
입력 · 소스 의존성
src/index.js
├─ App.js
└─ styles.cssentry에서 import 관계를 따라갑니다. 이미지·폰트는 실제 import한 경우 asset/resource 규칙의 처리 대상이 됩니다.
Node에서 수행하는 빌드
JS·JSX: babel-loader가 문법과 JSX를 변환합니다.
CSS: css-loader → style-loader 순서로 처리합니다. 설정 배열은 [style-loader, css-loader]이지만 실행은 오른쪽부터입니다.
HtmlWebpackPlugin은 템플릿으로 HTML을 만듭니다. 플러그인은 빌드 생명주기의 여러 단계에 관여할 수 있습니다.
최종 설정의 주요 산출물
dist/
├─ index.html
└─ bundle.js이 설정의 JavaScript 파일명은 bundle.js입니다. CSS는 별도 파일로 추출하지 않고 번들에 포함해 실행 시 주입합니다. 추가 산출물은 가져온 자산과 플러그인 설정에 따라 달라집니다.
브라우저 · HTML → bundle.js
생성된 HTML이 번들을 로드합니다. style-loader 코드가 CSS를 style 요소로 넣고, React의 createRoot가 App을 렌더링합니다.
사용자 입력 → 화면 갱신
App의 Count 버튼을 누르면 상태가 바뀌고 React가 화면을 갱신합니다. Webpack 빌드 단계와 브라우저의 상태 갱신은 서로 다른 작업입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 빌드의 책임과 브라우저 상태 갱신을 나눈다 |
| 전체 관계·조건 설명 | 노드 5개·진행 관계 4개다. CSS chain은 css-loader 뒤 style-loader이며 최종 HTML+bundle을 브라우저가 실행한다. CSS style 주입과 Count 사용자 갱신은 런타임이고 클릭마다 Webpack 전체 빌드를 하는 것은 아니다. 실제 import한 이미지/폰트만 asset 규칙의 처리 대상이며 mobile도 같다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 소스와 import | 입력: JS·CSS import / 화면: 빌드 전 | entry에서 의존 파일을 찾는다 | 의존 그래프가 변환 대상 파일을 정한다 |
| 변환과 실행 | 산출: 번들 자원 / 브라우저: 초기 UI | loader/번들 산출을 브라우저가 읽고 CSS를 적용한다 | 빌드 단계와 브라우저 실행을 구분한다 |
| Count 클릭 | 앱 state: 증가 / 번들: 같은 파일 | 실행 중 앱의 count가 바뀐다 | 사용자 클릭은 소스 재빌드를 수행하지 않는다 |
위 그림은 webpack의 빌드와 브라우저의 실행이 서로 다른 단계라는 것을 보여 줍니다. 입력은 소스의 의존 관계로, 엔트리인 src/index.js가 App.js와 styles.css를 가져옵니다. 이미지와 글꼴은 실제로 import했을 때만 asset/resource 규칙으로 처리됩니다. 빌드는 Node.js에서 실행되며, babel-loader가 문법과 JSX를 바꾸고, CSS는 css-loader 다음에 style-loader 순서로 처리하며, HtmlWebpackPlugin이 템플릿으로 HTML을 만듭니다.
이 설정의 결과물은 dist/index.html과 dist/bundle.js입니다. CSS는 별도 파일로 꺼내지 않고 번들 안에 넣어, 실행할 때 주입됩니다. 브라우저는 HTML이 불러온 번들을 실행해 style-loader의 코드가 CSS를 <style> 요소로 넣고, React의 createRoot()가 App을 그립니다. 마지막으로 사용자가 Count 버튼을 누르면 상태가 바뀌고 React가 화면을 갱신합니다.
그림 아래쪽의 설명처럼 이 상태 갱신은 이미 실행 중인 React 코드가 하는 일이며, 클릭할 때마다 webpack이 다시 빌드하는 것이 아닙니다. webpack 설정은 파일을 나열하는 일이 아니라, 무엇을 어떻게 변환하고 최적화할지 빌드의 목표에 맞게 나누는 작업입니다. 또 production 모드의 최적화도 빌드할 때 한 번 적용될 뿐, 사용자가 버튼을 누를 때 다시 일어나지 않습니다.
더 깊이: bundle.js라는 이름으로 배포하면 생기는 일
이 글의 설정은 결과물을 언제나 bundle.js라는 같은 이름으로 만듭니다. 그런데 이 파일에 긴 캐시 기간을 주면 새로 배포해도 브라우저가 캐시의 옛 파일을 계속 쓰고, 짧게 주면 바뀌지 않은 날에도 매번 다시 받습니다. webpack은 이를 위해 output.filename에 '[name].[contenthash].js'처럼 내용으로 계산한 해시를 넣는 방법을 제공합니다. 내용이 바뀔 때만 파일 이름이 바뀌므로, 해시가 붙은 파일에는 긴 캐시 기간을 주고 그 이름을 가리키는 HTML만 짧게 캐시하면 됩니다.
다만 코드를 고치지 않았는데도 해시가 바뀌는 경우가 있습니다. webpack 문서에 따르면 webpack은 모듈을 불러오는 런타임(runtime)과 매니페스트(manifest) 같은 기본 코드를 엔트리 청크에 넣기 때문입니다. 그래서 optimization.runtimeChunk: 'single'로 런타임을 별도 파일로 떼어 내고, optimization.splitChunks로 자주 바뀌지 않는 node_modules의 라이브러리를 vendors 같은 청크로 나누면, 내 코드만 고친 배포에서 라이브러리 파일의 이름과 캐시를 그대로 살릴 수 있습니다.
모듈의 번호도 해시에 영향을 줍니다. 모듈마다 붙는 ID를 불러오는 순서대로 매기면 파일 하나를 더했을 뿐인데 다른 모듈의 ID까지 바뀌어 여러 파일의 해시가 함께 달라집니다. webpack 5의 production 모드는 기본으로 optimization.moduleIds를 'deterministic'으로 두어 빌드 사이에 ID가 안정되게 합니다. 자세한 설정은 webpack의 캐싱 안내에 있습니다.
정리
- 모듈 번들러는 엔트리에서 시작해
import관계를 따라가 의존성 그래프를 만들고, 브라우저용 번들로 묶으며 변환과 최적화를 합니다. - webpack 설정의 핵심은 엔트리, 출력, 로더, 플러그인, 모드이며, 로더는 파일을 모듈로 바꾸고 플러그인은 빌드 과정 전체에서 추가 작업을 합니다.
- 한 규칙의 로더 배열은 오른쪽부터 적용되므로,
['style-loader', 'css-loader']는css-loader가 먼저 실행됩니다. style-loader는 CSS를 실행할 때 주입하고, CSS를 별도 파일로 꺼내려면MiniCssExtractPlugin과 그 로더를 함께 연결합니다.- Babel 설정은 한곳에 두고, 개발 서버는
webpack serve로, 배포용 번들은production모드로 만듭니다. - 빌드는 Node.js에서 한 번 일어나고, 브라우저에서의 상태 갱신은 이미 만든 번들의 코드가 처리합니다.
연습 문제
1. 세 번 누른 버튼
App의 Count는 0에서 시작합니다. 버튼을 세 번 누르면 값은 얼마가 될까요? 이 상태 갱신을 위해 webpack이 전체 빌드를 세 번 해야 할까요?
해설 보기
setCount(prev => prev + 1)이 매번 이전 값에 1을 더하므로 Count는 3이 됩니다.
상태 갱신은 브라우저에서 이미 실행 중인 React 코드가 처리하므로, 클릭할 때마다 webpack이 다시 빌드하지 않습니다. webpack의 빌드는 코드를 고쳤을 때 개발 서버가 다시 하거나, npm run build를 실행할 때 일어납니다.
2. 규칙만 있는 이미지
설정에 이미지용 asset/resource 규칙이 있지만, 코드 어디에서도 이미지를 import하지 않았습니다. src 폴더에 넣어 둔 이미지 파일이 반드시 dist에 들어갈까요?
해설 보기
들어가지 않습니다. webpack은 엔트리에서 시작해 실제로 import한 모듈만 처리하므로, 규칙이 있어도 가져오지 않은 파일은 의존성 그래프에 없고 결과물에도 나오지 않습니다.
이미지를 쓰려면 import logo from './logo.png';처럼 코드에서 가져오고 그 URL을 <img src={logo} /> 등에 써야 합니다. 그대로 복사만 하고 싶은 파일이라면 별도의 복사 플러그인이나 정적 파일 설정을 씁니다.
3. styles.css가 없는 결과물
npm run build를 했더니 dist에 styles.css 파일이 없습니다. 이 설정에서 이것은 실패일까요? CSS를 별도 파일로 만들고 싶다면 무엇을 바꿔야 할까요?
해설 보기
실패가 아닙니다. 이 설정은 style-loader로 CSS를 번들 안에 넣어 실행할 때 <style> 요소로 주입하므로, 별도의 CSS 파일이 나오지 않는 것이 정상입니다. 화면에 스타일이 적용되는지로 확인합니다.
CSS를 별도 파일로 만들려면 mini-css-extract-plugin을 설치하고, CSS 규칙의 style-loader를 MiniCssExtractPlugin.loader로 바꾼 뒤, plugins에 new MiniCssExtractPlugin()을 추가해야 합니다. 그러면 HtmlWebpackPlugin이 만든 HTML에 CSS 파일을 불러오는 <link>도 함께 들어갑니다.
다음 글 Babel과 Polyfill에서는 최신 자바스크립트를 대상 브라우저에 맞게 바꾸는 Babel과, 없는 기능을 채워 주는 폴리필을 대상 환경 설정과 함께 살펴봅니다.