Webpack이 필요한 이유
JavaScript에서 ES2015(ES6)
부터 문법 수준에서 모듈을 지원하기 시작했습니다. import
와 export
키워드가 도입되었지만, 모든 브라우저에서 이를 사용할 수 있게 된 것은 Webpack
과 같은 번들러가 등장하면서부터입니다.
Webpack이 등장하기 이전에는 어떤 방식으로 모듈을 관리했는지 살펴보겠습니다.
모듈 시스템의 역사
모듈 시스템 이전의 JavaScript
과거에는 여러 JavaScript 파일을 HTML에 순서대로 로드하는 방식으로 코드를 구성했습니다. 간단한 예제를 통해 살펴보겠습니다.
// calculator.js
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
js
// app.js
console.log(multiply(5, 3)); // 15
console.log(divide(10, 2)); // 5
js
HTML 파일에서 이 스크립트들을 로드할 때는 순서가 매우 중요합니다. calculator.js
를 먼저 로드해야 app.js
에서 함수를 사용할 수 있습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Module Example</title>
</head>
<body>
<script src="src/calculator.js"></script>
<script src="src/app.js"></script>
</body>
</html>
html
open index.html
명령어로 브라우저에서 실행하면 콘솔에 다음과 같이 출력됩니다.
콘솔 출력:
15
5
전역 스코프 오염 문제
과거에는 index.html
에 여러 JavaScript 파일을 순서대로 로드하여 다른 파일의 함수를 사용했습니다. 하지만 이 방식에는 전역 스코프 오염이라는 심각한 문제가 있습니다.
calculator.js
에서 정의한 multiply
함수는 해당 파일 안에서만 유효한 것이 아니라, 애플리케이션 전역에서 접근할 수 있습니다.
브라우저 개발자 도구 콘솔에서 확인해보겠습니다.
> multiply
ƒ multiply(a, b) { return a * b; }
> window.multiply
ƒ multiply(a, b) { return a * b; }
전역 스코프 window
객체에 함수가 등록된 것을 확인할 수 있습니다.
> multiply(10, 4)
40
함수를 바로 호출할 수도 있습니다. 하지만 JavaScript는 동적 타입 언어이기 때문에 함수 변수에 다른 값을 할당할 수 있습니다.
> multiply = 2
2
> multiply(10, 4)
Uncaught TypeError: multiply is not a function
이렇게 전역 스코프가 오염되면 애플리케이션이 예측 불가능해지고, 결국 런타임 에러가 발생하게 됩니다.
IIFE 방식의 모듈
IIFE(Immediately Invoked Function Expression)
는 즉시 실행 함수 표현식입니다. 함수를 정의하자마자 실행하는 패턴으로, 함수 내부에 독립적인 스코프를 생성합니다. 이를 통해 내부에서 정의한 변수나 함수가 외부에서 접근할 수 없도록 하여 전역 스코프 오염을 예방할 수 있습니다.
앞서 만들었던 calculator
모듈을 IIFE 방식으로 변경해보겠습니다.
// calculator.js
// 전역 네임스페이스 calculator 생성
// 이미 존재하면 기존 것을 사용하고, 없으면 빈 객체 할당
var calculator = calculator || {};
// 즉시 실행 함수
(function () {
// 이 안에서 정의된 함수는 외부에서 접근 불가
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
return a / b;
}
// 전역 네임스페이스에 필요한 함수만 노출
calculator.multiply = multiply;
calculator.divide = divide;
})();
js
이 패턴의 동작 원리를 단계별로 살펴보겠습니다.
1단계: 전역 네임스페이스 생성
var calculator = calculator || {};
js
calculator
라는 전역 네임스페이스를 만들고, 이미 존재하면 기존 것을 사용하고 없으면 빈 객체를 할당합니다.
2단계: IIFE로 독립적인 스코프 생성
(function () {
// 외부로부터 독립적인 스코프
})();
js
함수를 즉시 정의하고 실행하면, 이 함수 내부의 스코프는 외부로부터 완전히 독립됩니다.
3단계: 필요한 함수만 네임스페이스에 노출
함수 내부에서 multiply
와 divide
를 정의하고, 이 함수들을 외부에서 사용할 수 있도록 전역 네임스페이스 calculator
에 할당합니다.
이제 app.js
에서는 다음과 같이 사용해야 합니다.
// 잘못된 사용 - multiply는 전역 스코프에 없음
// console.log(multiply(5, 3)); // ReferenceError
// 올바른 사용 - calculator 네임스페이스를 통해 접근
console.log(calculator.multiply(5, 3)); // 15
console.log(calculator.divide(10, 2)); // 5
js
브라우저 콘솔에서 확인하면 multiply
함수는 전역에 직접 노출되지 않고, calculator
네임스페이스를 통해서만 접근할 수 있습니다.
> multiply
Uncaught ReferenceError: multiply is not defined
> calculator.multiply
ƒ multiply(a, b) { return a * b; }
> calculator.multiply(10, 4)
40
다양한 모듈 스펙
JavaScript 모듈을 구현하는 대표적인 명세가 AMD
와 CommonJS
입니다.
CommonJS
CommonJS
는 JavaScript를 사용하는 모든 환경에서 모듈을 사용하는 것을 목표로 합니다. exports
키워드로 모듈을 만들고, require()
함수로 모듈을 불러옵니다.
// calculator.js
exports.multiply = function (a, b) {
return a * b;
};
exports.divide = function (a, b) {
return a / b;
};
// app.js
const calculator = require('./calculator.js');
console.log(calculator.multiply(5, 3)); // 15
console.log(calculator.divide(10, 2)); // 5
js
대표적으로 서버 사이드 플랫폼인 Node.js
에서 CommonJS
를 사용합니다. calculator
라는 모듈을 만들 때 exports
키워드로 함수를 내보내고, require()
함수로 모듈의 경로를 전달하면 해당 모듈을 가져올 수 있습니다.
AMD와 UMD
AMD(Asynchronous Module Definition)
는 비동기로 로딩되는 환경에서 모듈을 사용하는 것을 목표로 합니다. 주로 브라우저 환경에서 사용됩니다. 브라우저처럼 외부에서 JavaScript를 로딩해야 하는 비동기 환경에서는 AMD
스펙을 사용합니다.
CommonJS
와 AMD
두 가지를 모두 지원하는 것이 UMD
스펙입니다.
ES2015 표준 모듈
이후 ES2015
에서 표준 모듈 시스템이 만들어졌습니다. 현재는 Babel
이나 Webpack
을 이용해서 표준 모듈 시스템을 사용하는 것이 일반적입니다.
// calculator.js
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
return a / b;
}
// app.js
// 모든 export를 가져오기
import * as calculator from './calculator.js';
// 필요한 것만 가져오기
// import { multiply, divide } from './calculator.js';
console.log(calculator.multiply(5, 3)); // 15
console.log(calculator.divide(10, 2)); // 5
js
export
구문으로 모듈을 만들고, import
구문으로 가져올 수 있습니다.
브라우저의 모듈 지원
모든 브라우저가 ES 모듈을 지원하는 것은 아닙니다. Internet Explorer
를 포함한 몇몇 모바일 브라우저는 모듈 시스템을 지원하지 않습니다. 최신 브라우저(Chrome, Firefox, Safari 등)에서는 모듈 시스템을 어떻게 사용하는지 살펴보겠습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ES Module Example</title>
</head>
<body>
<!-- type="module"을 지정하면 ES 모듈 사용 가능 -->
<script type="module" src="src/app.js"></script>
</body>
</html>
html
스크립트 태그를 로드할 때 type="text/javascript"
대신 type="module"
을 사용하면 app.js
에서 모듈을 사용할 수 있습니다.
그러나 브라우저에 무관하게 모듈을 사용하고 싶다면 바로 여기서 Webpack이 필요합니다.
로컬 파일 실행 시 CORS 문제
위 코드를 그대로 실행하면 CORS
에러가 발생할 수 있습니다. 브라우저가 파일을 직접 읽지 않고 HTTP 서버를 통해 로드해야 하기 때문입니다.
이를 해결하기 위해 간단한 개발 서버를 실행해보겠습니다.
npx light-server
bash
서버가 실행되면 다음과 같이 출력됩니다.
Light server is listening at http://localhost:4000
serving static dir: ./
when file changes, reload the page
이제 http://localhost:4000
에 접속하여 모듈이 제대로 동작하는지 확인할 수 있습니다.
Webpack 설치 및 기본 사용
설치 및 CLI 사용법
모듈로 개발하게 되면 모듈 간의 의존 관계가 생깁니다. 하나의 JavaScript 모듈에서 다른 모듈을 import
하는 방식입니다.
예를 들어 다음과 같은 의존 관계를 생각해볼 수 있습니다.
app.js (엔트리 포인트)
├─ calculator.js
├─ formatter.js
└─ utils.js
└─ helpers.js
이렇게 만든 구조가 우리가 만든 app.js
와 calculator.js
의 관계와 유사합니다.
Webpack은 모듈로 연결된 여러 개의 JavaScript 파일을 하나로 합쳐주는 역할을 합니다.
이렇게 하나로 합쳐진 파일을 번들(Bundle)
이라고 하고, 이 작업을 하는 도구를 번들러(Bundler)
라고 합니다. Webpack이 바로 이런 번들링 작업을 수행하는 대표적인 번들러입니다.
번들러 작업을 하는 webpack
패키지와, Webpack을 터미널 명령어로 사용할 수 있게 해주는 webpack-cli
를 설치해보겠습니다.
npm install -D webpack webpack-cli
bash
이 글에서는 다음 버전을 기준으로 진행합니다.
{
"devDependencies": {
"webpack": "^5.102.0",
"webpack-cli": "^6.0.1"
}
}
json
먼저 --help
옵션으로 Webpack을 어떻게 사용하는지 알아보겠습니다.
node_modules/.bin/webpack --help
bash
Webpack을 실행할 때 필수적인 옵션은 3가지입니다.
1. mode 옵션
개발 환경(development
) 또는 운영 환경(production
)을 지정합니다.
2. entry 옵션
모듈의 시작점(엔트리 포인트)을 지정합니다. Webpack은 이 파일부터 시작해서 연결된 모든 모듈을 찾아갑니다.
3. output 옵션
엔트리 포인트를 통해 웹팩이 모든 모듈을 합친 결과물을 저장하는 경로를 설정합니다.
Webpack 4 vs 5 차이점
Webpack 4에서는 --output
뒤에 경로와 파일명을 함께 지정할 수 있었습니다.
# Webpack 4
node_modules/.bin/webpack --mode development --entry ./src/app.js --output dist/main.js
bash
Webpack 5부터는 --output
이 디렉토리 경로만 받을 수 있고, 파일명은 별도 옵션으로 지정해야 합니다.
--output-path
: 빌드 산출물이 저장될 폴더 경로--output-filename
: 출력될 파일 이름
# Webpack 5
node_modules/.bin/webpack --mode development --entry ./src/app.js --output-path dist --output-filename main.js
bash
빌드가 성공하면 다음과 같은 메시지가 출력됩니다.
asset main.js 4.5 KiB [emitted] (name: main)
./src/app.js 150 bytes [built] [code generated]
./src/calculator.js 120 bytes [built] [code generated]
webpack 5.102.0 compiled successfully in 150 ms
빌드 결과 사용하기
이제 index.html
에서 빌드된 파일을 로드해보겠습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Webpack Example</title>
</head>
<body>
<!-- 빌드된 파일을 로드 -->
<script src="dist/main.js"></script>
</body>
</html>
html
이제 모든 브라우저에서 모듈 시스템을 사용할 수 있습니다. 콘솔에 다음과 같이 정상적으로 출력되는 것을 확인할 수 있습니다.
콘솔 출력:
15
5
Webpack은 이렇게 여러 개의 모듈을 하나의 파일로 만들어주는 역할을 합니다.
설정 파일 만들기
Webpack 설정을 할 때 매번 터미널에 긴 옵션을 입력하는 것은 비효율적입니다. 설정 파일을 만들어서 관리하는 것이 좋습니다.
--help
문서를 보면 --config
옵션이 있는데, 이를 통해 Webpack 설정 파일을 지정할 수 있습니다. 기본 파일명은 webpack.config.js
입니다.
프로젝트 루트에 webpack.config.js
파일을 생성하겠습니다.
// webpack.config.js
const path = require('path');
// Node의 모듈 시스템 사용
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
// 엔트리가 여러 개일 경우
// sub: './src/sub.js',
},
output: {
// path는 절대 경로로 지정
path: path.resolve('./dist'),
// 엔트리 키 값으로 동적 파일명 생성
// main.js, sub.js 형태로 생성됨
filename: '[name].js',
},
};
js
설정을 자세히 살펴보겠습니다.
mode: development
로 설정
entry: 시작점을 지정하는데, 객체 형태로 지정합니다. entry
포인트의 경로뿐만 아니라 main
이라는 키도 설정했습니다. 이 키는 다음에 나올 output
설정에서 사용됩니다.
output: path
와 filename
을 설정합니다.
path
: 절대 경로를 지정합니다.path.resolve()
를 사용하여 현재 디렉토리 기준으로dist
폴더의 절대 경로를 생성합니다.filename
:[name].js
로 지정했습니다.[name]
부분이 엔트리에서 설정한 키 값(main
)으로 치환됩니다.
이렇게 설정한 이유는 엔트리가 여러 개일 수 있기 때문입니다. 엔트리가 여러 개면 그에 맞는 output도 여러 개여야 하는데, 파일명을 동적으로 생성할 수 있습니다.
예를 들어 엔트리가 다음과 같다면:
entry: {
main: './src/app.js',
sub: './src/sub.js',
},
js
main.js
와 sub.js
가 각각 output으로 생성됩니다.
npm 스크립트 등록
npm
은 프로젝트를 관리하는 도구로, 스크립트를 자동화하는 기능이 있습니다. Webpack으로 코드를 번들링하는 과정을 npm 스크립트에 등록해보겠습니다.
package.json
에서 scripts
부분을 다음과 같이 수정합니다.
{
"scripts": {
"build": "webpack"
}
}
json
이제 다음 명령어로 빌드할 수 있습니다.
npm run build
bash
로더(Loader)
Webpack은 모든 파일을 모듈로 바라봅니다.
JavaScript로 만든 모듈뿐만 아니라 CSS, 이미지, 폰트까지 전부 모듈로 처리합니다. 따라서 ES6
의 import
키워드를 사용하면 이런 파일들을 JavaScript 코드 안으로 가져와서 사용할 수 있습니다.
이것이 가능한 이유는 Webpack의 로더(Loader)
때문입니다. Webpack의 로더는 모든 파일을 JavaScript 모듈처럼 만들어줍니다. 예를 들어 CSS 파일을 JavaScript에서 직접 로딩해서 사용할 수 있게 해줍니다.
또한 이미지 파일을 Data URL 형식의 문자열로 변환한 다음, JavaScript에서 이미지 파일을 사용할 수 있게 해주는 것이 로더의 역할입니다.
로더를 사용하기 전에 직접 로더를 만들어보면서 동작 원리를 살펴보겠습니다.
커스텀 로더 만들기
프로젝트 루트에 console-to-alert-loader.js
파일을 생성합니다.
// console-to-alert-loader.js
module.exports = function consoleToAlertLoader(content) {
console.log('console-to-alert 로더가 동작합니다');
// console.log를 alert로 변경
return content.replace('console.log(', 'alert(');
};
js
로더는 함수 형태로 작성합니다. 로더가 파일을 읽으면 파일의 내용이 content
인자로 전달됩니다. 여기서는 로더가 동작했는지 확인하기 위해 console.log
만 출력하고, console.log
를 alert
로 변경하는 간단한 작업을 수행합니다.
이제 만든 로더를 Webpack 설정에 연결해야 합니다.
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
// 로더가 처리해야 하는 파일들의 패턴 (정규표현식)
// .js로 끝나는 모든 파일 처리
test: /\.js$/,
use: [path.resolve('./console-to-alert-loader.js')],
},
],
},
};
js
module.rules
배열에 로더 규칙을 추가했습니다.
test
: 로더가 처리할 파일의 패턴을 정규표현식으로 지정합니다./\.js$/
는.js
로 끝나는 모든 파일을 의미합니다.use
: 사용할 로더의 경로를 지정합니다.
JavaScript 파일이 아닌 파일들은 이 로더가 처리하지 않습니다. 빌드하면 console.log
대신 alert
가 실행되는 것을 확인할 수 있습니다.
터미널 출력:
console-to-alert 로더가 동작합니다
asset main.js 4.8 KiB [emitted] (name: main)
브라우저에서 실행하면 alert
창이 뜹니다.
주요 로더 사용법
이제 실무에서 자주 사용되는 대표적인 로더 몇 가지를 살펴보겠습니다.
css-loader와 style-loader
CSS 파일에 로더를 설정하면 JavaScript 파일에서 CSS 파일을 모듈로 불러올 수 있습니다.
// app.js
import './app.css';
js
일반적으로는 이 코드가 동작하지 않습니다. Webpack의 로더를 사용하면 ES6
의 import
구문으로 CSS 코드를 JavaScript 코드로 가져와서 사용할 수 있습니다.
CSS 파일이 모듈이 되려면 Webpack의 로더가 CSS 파일을 JavaScript 모듈로 변환해주어야 합니다.
스타일 코드를 작성해보겠습니다.
/* app.css */
body {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
h1 {
color: #333;
text-align: center;
}
css
app.js
에서 이 CSS 파일을 import합니다.
// app.js
import './app.css';
import * as calculator from './calculator.js';
console.log(calculator.multiply(5, 3));
console.log(calculator.divide(10, 2));
js
빌드를 하면 오류가 발생합니다.
ERROR in ./src/app.css 1:4
Module parse failed: Unexpected token (1:4)
You may need an appropriate loader to handle this file type,
currently no loaders are configured to process this file.
Webpack이 CSS 파일을 읽다가 문법을 이해하지 못해서 에러를 발생시킨 것입니다.
Webpack이 CSS 파일을 JavaScript에서 모듈로 가져올 수 있도록 CSS 파일을 처리해주는 것이 css-loader
입니다.
로더를 사용하려면 먼저 설치해야 합니다.
npm install -D css-loader
bash
Webpack 설정에 로더를 추가합니다.
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
// CSS 파일을 처리하는 로더
test: /\.css$/,
use: ['css-loader'],
},
],
},
};
js
Webpack은 엔트리 포인트부터 시작해서 연결된 모든 모듈을 검색합니다. 그러다가 CSS 파일을 만나면 이 패턴(/\.css$/
)에 의해 css-loader
가 동작합니다.
빌드가 성공합니다.
asset main.js 7.2 KiB [emitted] (name: main)
./src/app.js 180 bytes [built] [code generated]
./src/calculator.js 140 bytes [built] [code generated]
./src/app.css 250 bytes [built] [code generated]
webpack 5.102.0 compiled successfully in 280 ms
dist
폴더를 보면 CSS 파일이 별도로 있는 것이 아니라 main.js
에 포함되어 있습니다. JavaScript 문자열로 CSS 코드가 변환된 것을 확인할 수 있습니다.
하지만 브라우저에서 확인해보면 스타일이 적용되지 않습니다.
HTML 코드가 DOM으로 변환되어야 브라우저에 문서가 보이듯이, CSS 코드도 CSSOM(CSS Object Model)으로 변환되어야 브라우저에서 스타일이 적용됩니다.
그러려면 HTML 파일에서 CSS 코드를 직접 불러오거나, 인라인 스타일로 넣어줘야 합니다. 아직 그런 처리를 하지 않고 JavaScript 파일에만 CSS 코드가 있어서 브라우저에 적용되지 않는 것입니다.
style-loader
는 JavaScript로 변경된 스타일 코드를 HTML에 삽입해주는 로더입니다.
CSS 코드를 모듈로 사용하고 Webpack으로 번들링하려면 css-loader
와 style-loader
두 가지를 함께 사용해야 합니다.
먼저 style-loader
를 설치합니다.
npm install -D style-loader
bash
Webpack 설정을 수정합니다.
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
// CSS 파일을 처리하는 로더
test: /\.css$/,
// 로더는 오른쪽에서 왼쪽으로 실행됨
// css-loader로 CSS를 JavaScript로 변환 → style-loader로 HTML에 삽입
use: ['style-loader', 'css-loader'],
},
],
},
};
js
use
배열에 여러 로더를 지정할 때는 오른쪽에서 왼쪽으로 실행됩니다. 먼저 css-loader
가 CSS 파일을 JavaScript 모듈로 변환하고, 그 다음 style-loader
가 변환된 스타일을 HTML에 삽입합니다.
다시 빌드하고 브라우저에서 확인하면 스타일이 제대로 적용된 것을 볼 수 있습니다.
개발자 도구에서 확인하면 <head>
태그 안에 <style>
태그가 자동으로 삽입된 것을 확인할 수 있습니다.
<head>
...
<style>
body {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
}
h1 {
color: #333;
text-align: center;
}
</style>
</head>
html
file-loader와 Asset Modules
로더는 CSS뿐만 아니라 이미지 파일도 처리할 수 있습니다.
import
구문으로 이미지 파일을 JavaScript로 가져와서 사용할 수 있습니다. CSS 파일도 모듈로 처리하기 때문에 CSS 파일에서 이미지 파일을 가져올 수 있습니다.
실습을 위해 이미지 파일을 프로젝트에 추가하겠습니다. src
폴더에 background.jpg
이미지를 추가합니다.
Webpack 4에서는 file-loader
를 사용했습니다.
npm install -D file-loader
bash
// webpack.config.js (Webpack 4)
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
// 이미지 파일을 처리하는 로더
test: /\.(png|jpe?g|gif)$/,
loader: 'file-loader',
options: {
// 파일을 모듈로 사용했을 때 경로 앞에 추가되는 문자열
// output 경로가 dist이므로 같은 값 지정
publicPath: './dist/',
// 파일이 output에 복사될 때 사용하는 파일 이름
// 원본 파일명.확장자?해시값 형태
name: '[name].[ext]?[hash]',
},
},
],
},
};
js
CSS 파일에서 이미지를 배경으로 사용해보겠습니다.
/* app.css */
body {
background-image: url('./background.jpg');
background-size: cover;
background-position: center;
}
css
빌드하면 dist
폴더에 이미지 파일이 복사되고, CSS에서 올바른 경로로 참조됩니다.
Webpack 5부터는 file-loader
대신 내장 Asset Modules를 사용합니다.
// webpack.config.js (Webpack 5)
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
// 이미지 파일을 처리하는 Asset Module
test: /\.(png|jpe?g|gif)$/,
type: 'asset/resource',
},
],
},
};
js
type: 'asset/resource'
로 지정하면 파일을 별도로 복사하고 URL을 반환합니다.
빌드하면 dist
폴더에 해시값이 포함된 파일명으로 이미지가 저장됩니다.
📦 dist/
┣ 📜 main.js
┗ 📜 a1b2c3d4e5f6g7h8i9j0.jpg
파일명이 해시값으로 변경되는 이유는 캐시 갱신을 위해서입니다. Webpack은 빌드할 때마다 유니크한 해시값을 생성합니다.
정적 파일의 경우 브라우저에서 캐싱하는 경우가 많습니다. JavaScript, CSS, 이미지, 폰트 등을 성능을 위해 캐싱합니다. 파일 내용이 변경되어도 이름이 같으면 브라우저가 이전 캐시를 사용할 수 있습니다. 이를 방지하는 방법 중 하나가 파일 이름을 변경하는 것입니다.
실제로 실행시켜보면 이미지가 배경으로 잘 표시됩니다.
Webpack 5에서도 파일명과 경로를 커스터마이즈할 수 있습니다.
// webpack.config.js (Webpack 5 - 상세 설정)
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset/resource',
generator: {
// 브라우저에서 접근할 때 사용할 경로
publicPath: './dist/',
// 파일 이름 형식
// [name]: 원본 파일명
// [contenthash]: 파일 내용 기반 해시
// [ext]: 확장자
filename: '[name].[contenthash][ext]',
},
},
],
},
};
js
url-loader와 Asset Modules (inline)
사용하는 이미지 개수가 많아지면 네트워크 요청 수가 늘어나 사이트 성능에 영향을 줄 수 있습니다.
만약 한 페이지 안에서 작은 이미지를 여러 개 사용한다면 Data URL
스키마를 이용하는 방법이 더 효율적입니다.
Data URL은 파일 경로 대신 파일 데이터를 직접 Base64로 인코딩한 문자열을 사용하는 방식입니다.
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
alt="1x1 red pixel"
/>
html
이렇게 하면 별도의 HTTP 요청 없이 HTML에 이미지를 직접 삽입할 수 있습니다. 작은 파일들은 이렇게 인라인으로 포함하는 것이 더 효율적입니다.
실습을 위해 작은 이미지를 추가하겠습니다. src
폴더에 icon.png
(5KB 미만)를 추가합니다.
// app.js
import './app.css';
import * as calculator from './calculator.js';
import icon from './icon.png';
document.addEventListener('DOMContentLoaded', () => {
document.body.innerHTML = `
<h1>Webpack Example</h1>
<img src="${icon}" alt="Icon" />
`;
});
console.log(calculator.multiply(5, 3));
console.log(calculator.divide(10, 2));
js
Webpack 4에서는 url-loader
를 사용했습니다.
npm install -D url-loader
bash
// webpack.config.js (Webpack 4)
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif)$/,
loader: 'url-loader',
options: {
publicPath: './dist/',
name: '[name].[ext]?[hash]',
// 20KB 이하는 Data URL로 변환
// 그 이상은 파일로 복사
limit: 20000,
},
},
],
},
};
js
Webpack 5에서는 type: 'asset'
을 사용하면 파일 크기에 따라 자동으로 인라인 또는 파일로 처리합니다.
// webpack.config.js (Webpack 5)
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset', // 자동으로 inline/resource 선택
parser: {
dataUrlCondition: {
maxSize: 20000, // 20KB 이하는 inline
},
},
generator: {
filename: '[name].[contenthash][ext]',
},
},
],
},
};
js
빌드하면 다음과 같은 결과를 얻을 수 있습니다.
📦 dist/
┣ 📜 main.js (icon.png가 Base64로 포함됨)
┗ 📜 background.a1b2c3d4.jpg (20KB 이상이므로 파일로 복사)
main.js
내부를 확인하면 작은 이미지는 Data URL로 변환되어 있습니다.
// main.js 일부
const icon = 'data:image/png;base64,iVBORw0KGgoAAAANS...';
js
브라우저에서 Network 탭을 확인하면 작은 이미지는 별도의 HTTP 요청이 발생하지 않는 것을 볼 수 있습니다.
플러그인(Plugin)
로더가 각 파일 단위로 처리했던 것에 반해, 플러그인은 번들된 결과물 전체를 처리합니다. JavaScript 코드를 난독화한다거나 특정 텍스트를 추출하는 용도로 플러그인이 사용됩니다.
로더의 원리를 이해하기 위해 커스텀 로더를 만들어봤듯이, 플러그인도 직접 만들어보면서 동작 원리를 이해해보겠습니다.
커스텀 플러그인 만들기
Webpack 문서의 “Writing a Plugin”을 참고하여 플러그인을 만들어보겠습니다. 플러그인은 로더가 함수로 정의된 것과 다르게 클래스로 정의합니다.
banner-add-plugin.js
파일을 생성합니다.
// banner-add-plugin.js (Webpack 5)
class BannerAddPlugin {
// apply 메서드를 구현하면 Webpack이 compiler 객체를 전달함
apply(compiler) {
// emit 훅은 파일이 생성되기 직전에 실행됨
compiler.hooks.emit.tapAsync('BannerAddPlugin', (compilation, callback) => {
// compilation 객체를 통해 번들된 결과물에 접근 가능
if (compilation.assets['main.js']) {
// 기존 소스 코드 가져오기
const source = compilation.assets['main.js'].source();
// 파일 상단에 추가할 배너
const banner = [
'/**',
' * 이것은 BannerAddPlugin이 처리한 결과입니다.',
' * Build Date: ' + new Date().toLocaleDateString(),
' */',
].join('\n');
const newContent = banner + '\n\n' + source;
// 기존 파일을 새 내용으로 교체
compilation.assets['main.js'] = {
source: () => newContent,
size: () => newContent.length,
};
}
// 비동기 작업 완료를 Webpack에 알림
callback();
});
}
}
module.exports = BannerAddPlugin;
js
Webpack 4 이하에서는 compiler.plugin()
메서드를 사용했지만, Webpack 4 이상에서는 Tapable Hooks API로 변경되었습니다.
// Webpack 3 이하 (더 이상 사용하지 않음)
compiler.plugin('emit', (compilation, callback) => {
// ...
});
// Webpack 4 이상 (권장)
compiler.hooks.emit.tapAsync('PluginName', (compilation, callback) => {
// ...
});
js
만든 플러그인을 Webpack 설정에 추가합니다.
// webpack.config.js
const path = require('path');
const BannerAddPlugin = require('./banner-add-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 20000,
},
},
generator: {
filename: '[name].[contenthash][ext]',
},
},
],
},
// 플러그인 배열에 인스턴스 추가
plugins: [new BannerAddPlugin()],
};
js
빌드하고 dist/main.js
파일을 열어보면 상단에 배너가 추가된 것을 확인할 수 있습니다.
/**
* 이것은 BannerAddPlugin이 처리한 결과입니다.
* Build Date: 2025/10/11
*/
/******/ (() => { // webpackBootstrap
...
js
로더가 여러 파일 각각에 대해 실행되는 것과 달리, 플러그인은 번들된 결과물에 대해 한 번만 실행되는 것을 확인할 수 있습니다.
주요 플러그인 사용법
커스텀으로 만들었던 플러그인과 유사한 것이 실제로 존재합니다. 대표적인 플러그인들을 살펴보겠습니다.
BannerPlugin
빌드한 결과물에 빌드 정보나 커밋 버전 같은 것을 추가할 수 있는 플러그인입니다. BannerPlugin
은 Webpack이 기본으로 제공하는 내장 플러그인입니다.
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 20000,
},
},
generator: {
filename: '[name].[contenthash][ext]',
},
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: '이것은 BannerPlugin이 처리한 결과입니다.',
}),
],
};
js
빌드하고 dist/main.js
를 확인하면 상단에 배너가 추가된 것을 볼 수 있습니다.
/*! 이것은 BannerPlugin이 처리한 결과입니다. */
/******/ (() => { // webpackBootstrap
...
js
배너에 더 자세한 정보를 추가해보겠습니다.
// webpack.config.js
const childProcess = require('child_process');
plugins: [
new webpack.BannerPlugin({
banner: `
Build Date: ${new Date().toLocaleDateString()}
Commit Version: ${childProcess.execSync('git rev-parse --short HEAD').toString().trim()}
Author: ${childProcess.execSync('git config user.name').toString().trim()}
`,
}),
],
js
Node의 child_process
모듈을 사용하여 터미널 명령어를 실행할 수 있습니다.
git rev-parse --short HEAD
: 현재 커밋의 짧은 해시값git config user.name
: Git 사용자 이름
빌드하면 다음과 같은 배너가 생성됩니다.
/*!
Build Date: 2025/10/11
Commit Version: a1b2c3d
Author: John Doe
*/
js
빌드된 결과물에 이런 배너 정보를 추가하는 이유는, 배포 후 실제 정적 파일이 제대로 배포되었는지, 또는 캐시로 인해 갱신되지 않았는지 확인할 때 유용하기 때문입니다.
DefinePlugin
프론트엔드 소스 코드는 개발 환경과 운영 환경으로 나눠서 운영합니다.
환경에 따라 API 주소가 다를 수 있습니다. 배포할 때마다 코드를 수정하면 휴먼 에러가 발생하기 쉽고 장애가 날 위험이 있습니다.
API 주소처럼 환경 의존적인 정보를 소스가 아닌 다른 곳에서 관리하는 것이 좋습니다. 이런 환경 변수를 애플리케이션에 제공하기 위해 DefinePlugin
을 사용합니다.
DefinePlugin
도 Webpack의 기본 내장 플러그인입니다.
// webpack.config.js
plugins: [new webpack.DefinePlugin({})];
js
이렇게 빈 객체로 설정해도 DefinePlugin
이 기본적으로 주입하는 환경 변수가 있습니다. 바로 Node의 환경 변수인 process.env.NODE_ENV
입니다.
Webpack이 Node 환경에서 실행되기 때문에, 빌드될 때 이 NODE_ENV
환경 변수를 번들에 포함시킵니다.
// app.js
import './app.css';
import * as calculator from './calculator.js';
console.log(calculator.multiply(5, 3));
console.log(calculator.divide(10, 2));
console.log('Environment:', process.env.NODE_ENV);
js
빌드하고 실행하면 다음과 같이 출력됩니다.
콘솔 출력:
15
5
Environment: development
development
가 출력되는 이유는 webpack.config.js
에서 mode: 'development'
로 설정했기 때문입니다.
직접 환경 변수를 추가할 수도 있습니다.
// webpack.config.js
plugins: [
new webpack.DefinePlugin({
TWO: '1+1',
}),
],
js
애플리케이션에서는 TWO
라는 전역 변수로 접근할 수 있습니다.
console.log(TWO); // 2
js
'1+1'
은 문자열이 아니라 JavaScript 코드로 평가되어 2
가 됩니다.
만약 문자열 값을 넣고 싶다면 JSON.stringify()
를 사용합니다.
plugins: [
new webpack.DefinePlugin({
TWO: JSON.stringify('1+1'),
'api.domain': JSON.stringify('http://dev.api.domain.com'),
}),
],
js
console.log(TWO); // "1+1" (문자열)
console.log(api.domain); // "http://dev.api.domain.com"
js
객체 형식으로 정의하면 api.domain
처럼 점 표기법으로 접근할 수 있습니다.
HtmlWebpackPlugin
HtmlWebpackPlugin
은 Webpack의 기본 플러그인은 아니지만, 널리 사용되는 서드파티 플러그인입니다. HTML 파일을 빌드 과정에 포함시켜 처리합니다.
지금까지는 HTML 파일을 프로젝트 루트에 직접 만들고, 빌드한 결과물의 경로를 수동으로 입력했습니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 수동으로 입력 -->
<script src="dist/main.js"></script>
</body>
</html>
html
HTML도 빌드 과정에 포함시키고 싶다면 이 플러그인을 사용합니다.
먼저 패키지를 설치합니다.
npm install -D html-webpack-plugin
bash
index.html
을 src/index.html
로 이동하고, JavaScript 로딩 코드는 제거합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 스크립트 태그 제거 - 플러그인이 자동으로 추가함 -->
</body>
</html>
html
Webpack 설정에 플러그인을 추가합니다.
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
js
빌드하면 dist
폴더에 index.html
이 생성됩니다.
📦 dist/
┣ 📜 main.js
┗ 📜 index.html
생성된 index.html
을 확인하면 스크립트 태그가 자동으로 추가되어 있습니다.
Webpack 4에서는 <body>
태그 하단에 <script>
태그가 추가되었지만, Webpack 5에서는 <head>
태그에 <script defer>
태그가 추가됩니다.
<!-- Webpack 5 결과 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script defer src="main.js"></script>
</head>
<body></body>
</html>
html
이제 http://127.0.0.1:5500/dist/index.html
로 접속하면 됩니다.
HtmlWebpackPlugin
을 사용하면 동적으로 HTML을 생성할 수 있습니다. EJS 템플릿 문법을 사용하여 환경에 따라 다른 값을 넣을 수 있습니다.
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- EJS 템플릿 문법 사용 -->
<title>Document <%= env %></title>
</head>
<body></body>
</html>
html
Webpack 설정에서 templateParameters
로 값을 전달합니다.
// webpack.config.js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
templateParameters: {
env: process.env.NODE_ENV === 'development' ? '(개발)' : '(운영)',
},
}),
],
js
환경 변수를 설정하여 빌드합니다.
NODE_ENV=development npm run build # 개발 환경
NODE_ENV=production npm run build # 운영 환경
bash
개발 환경에서는 <title>Document (개발)</title>
, 운영 환경에서는 <title>Document (운영)</title>
로 생성됩니다.
개발 환경에서 타이틀에 “(개발)” 표시를 하면 휴먼 에러를 줄일 수 있습니다.
운영 환경에서는 HTML을 압축하고 주석을 제거하는 것이 좋습니다.
// webpack.config.js
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
templateParameters: {
env: process.env.NODE_ENV === 'development' ? '(개발)' : '(운영)',
},
minify:
process.env.NODE_ENV === 'production'
? {
collapseWhitespace: true, // 빈칸 제거
removeComments: true, // 주석 제거
}
: false,
}),
],
js
운영 환경에서 빌드하면 HTML이 한 줄로 압축됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Document (운영)</title>
<script defer src="main.js"></script>
</head>
<body></body>
</html>
html
CleanWebpackPlugin
CleanWebpackPlugin
은 output 폴더를 자동으로 삭제해주는 플러그인입니다.
지금까지는 필요할 때마다 dist
폴더를 수동으로 삭제했는데, 이 플러그인을 추가하면 빌드할 때마다 dist
폴더를 자동으로 정리하고 새로운 파일을 생성합니다.
npm install -D clean-webpack-plugin
bash
// webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
new CleanWebpackPlugin(),
// ... 다른 플러그인들
],
js
주의할 점은 default export
가 아니라 named export
이므로 중괄호로 import해야 합니다.
빌드하면 기존 dist
폴더가 삭제되고 새로운 빌드 결과물이 생성됩니다.
MiniCssExtractPlugin
스타일시트가 많아지면 JavaScript 파일 하나에 모두 포함시키는 것이 부담스러울 수 있습니다. 브라우저에서 큰 파일 하나를 로드하는 것보다 여러 작은 파일을 동시에 로드하는 것이 성능에 유리합니다.
번들 결과에서 스타일시트 코드만 따로 뽑아서 CSS 파일을 별도로 만들어 역할에 따라 파일을 분리하는 것이 좋습니다.
최종 결과물이 JavaScript 하나, CSS 파일 하나로 분리되면 각각의 용량이 줄어들고, 브라우저가 병렬로 다운로드할 수 있어 페이지 로딩 성능이 향상됩니다.
이 플러그인도 서드파티 패키지이므로 설치가 필요합니다.
npm install -D mini-css-extract-plugin
bash
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [
// 운영 환경에서는 MiniCssExtractPlugin.loader 사용
// 개발 환경에서는 style-loader 사용
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader',
],
},
// ... 다른 rules
],
},
plugins: [
// 운영 환경에서만 플러그인 활성화
...(process.env.NODE_ENV === 'production'
? [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
]
: []),
// ... 다른 플러그인들
],
};
js
이 플러그인은 로더와 플러그인 설정을 모두 추가해야 합니다.
- 로더 설정:
style-loader
대신MiniCssExtractPlugin.loader
사용 - 플러그인 설정: CSS 파일명 지정
개발 환경에서는 빠른 빌드를 위해 JavaScript 파일 하나로 처리하고, 운영 환경에서만 CSS를 분리하는 것이 일반적입니다.
운영 환경으로 빌드하면:
📦 dist/
┣ 📜 main.js
┣ 📜 main.a1b2c3d4.css
┗ 📜 index.html
index.html
을 확인하면 CSS도 자동으로 로드됩니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Document (운영)</title>
<link href="main.a1b2c3d4.css" rel="stylesheet" />
<script defer src="main.js"></script>
</head>
<body></body>
</html>
html
개발 환경 구성
지금까지 실습한 환경에서는 HTML, JavaScript, CSS, 이미지 파일들을 직접 브라우저에 올려서 결과를 확인했습니다.
실제 웹 서비스가 제공되려면 인터넷 어딘가에 게시되어야 합니다. 인터넷에 연결된 컴퓨터에서 서버 프로그램이 정적 파일을 제공하는 형식이어야 합니다.
개발 환경에서도 이와 유사한 환경을 갖추는 것이 좋습니다. 운영 환경과 동일하게 맞춤으로써 운영 환경에 배포했을 때 발생할 수 있는 문제를 개발 단계에서 미리 확인할 수 있습니다.
또한 AJAX 방식의 API 연동을 하게 되면 CORS(Cross-Origin Resource Sharing)
브라우저 보안 정책 때문에 개발 환경에서 API를 사용하는 것이 불가능할 수 있습니다. 이런 이유로 개발 환경에서도 서버가 반드시 필요합니다.
Webpack은 이런 개발 서버를 제공하는 webpack-dev-server
를 제공합니다.
webpack-dev-server 설정
npm install -D webpack-dev-server
bash
package.json
에 스크립트를 추가합니다.
{
"scripts": {
"build": "webpack",
"start": "webpack-dev-server"
}
}
json
개발 서버를 실행합니다.
npm start
bash
서버가 실행되면 다음과 같은 메시지가 출력됩니다.
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:8080/
<i> [webpack-dev-server] On Your Network (IPv4): http://192.168.0.10:8080/
<i> [webpack-dev-server] Content not from webpack is served from 'public' directory
이제 http://localhost:8080
에 접속하여 결과물을 확인할 수 있습니다.
Webpack 개발 서버는 파일 변화를 감지하여 자동으로 브라우저를 새로고침해주는 기능도 제공합니다.
예를 들어 app.css
를 수정하면:
/* app.css */
body {
background-color: #e0f7fa; /* 색상 변경 */
}
css
저장하면 Webpack 개발 서버가 변경을 감지하고 자동으로 브라우저에 반영합니다. 브라우저를 수동으로 새로고침할 필요가 없습니다.
이렇게 Webpack 개발 서버를 사용하면 개발 서버를 제공할 뿐만 아니라, 코드 변화를 감지해서 결과물을 실시간으로 브라우저에 보여주는 매우 편리한 기능을 제공합니다.
devServer 상세 설정
Webpack 개발 서버의 설정을 살펴보겠습니다.
// webpack.config.js (Webpack 4)
const path = require('path');
module.exports = {
// ...
devServer: {
// 정적 파일을 제공할 경로 (기본값: output 경로)
contentBase: path.join(__dirname, 'dist'),
// 브라우저를 통해 접근하는 경로
publicPath: '/',
// 개발 환경 도메인 설정
// 쿠키 기반 인증은 인증 서버와 동일한 도메인이어야 함
host: 'dev.domain.com',
// 빌드 시 에러나 경고를 브라우저 화면에 표시
overlay: true,
// 개발 서버 포트
port: 8081,
// 메시지 수준 설정
stats: 'errors-only', // 'none', 'errors-only', 'minimal', 'normal', 'verbose'
// History API를 사용하는 SPA 개발 시 설정
// 404 발생 시 index.html로 리다이렉트
historyApiFallback: true,
},
};
js
Webpack 5에서는 일부 옵션이 변경되었습니다.
// webpack.config.js (Webpack 5)
const path = require('path');
module.exports = {
// ...
devServer: {
// 정적 파일 제공 경로
static: {
directory: path.join(__dirname, 'dist'),
publicPath: '/',
},
// 개발 도메인 설정
host: 'dev.domain.com',
// 개발 포트
port: 8081,
// SPA의 History API 지원
historyApiFallback: true,
// 브라우저 오버레이 설정
client: {
overlay: {
errors: true,
warnings: false,
},
logging: 'error',
},
// HMR 활성화 (기본값 true)
hot: true,
// 터미널 메시지 레벨 조정
infrastructureLogging: {
level: 'error',
},
// 서버 구동 시 자동으로 브라우저 열기
open: true,
},
};
js
API 서버 연동 및 CORS 해결
API 없이 개발하게 되면 프론트엔드 개발할 때 난감할 때가 있습니다. 목업(Mock) 데이터를 제공하는 목업 API를 만들어서 화면을 개발하면 더 수월합니다.
Webpack 개발 서버는 목업 API를 제공하는 기능도 있습니다.
// webpack.config.js (Webpack 4)
module.exports = {
devServer: {
overlay: true,
stats: 'errors-only',
// before 함수로 Express 앱에 접근
before: (app) => {
// app은 Express 서버 인스턴스
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
]);
});
},
},
};
js
Webpack 5부터는 setupMiddlewares
를 사용합니다.
// webpack.config.js (Webpack 5)
module.exports = {
devServer: {
client: {
overlay: true,
logging: 'error',
},
setupMiddlewares: (middlewares, devServer) => {
if (!devServer) {
throw new Error('webpack-dev-server is not defined');
}
// Express 인스턴스 접근
const app = devServer.app;
// 목업 API 엔드포인트 등록
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
]);
});
// middlewares 반환 필수
return middlewares;
},
},
};
js
app.get
, app.post
, app.use
등 Express의 모든 메서드를 사용할 수 있습니다.
이제 Webpack 개발 서버로 API 요청을 날려보겠습니다.
npm install axios
bash
// app.js
import axios from 'axios';
import './app.css';
import * as calculator from './calculator.js';
document.addEventListener('DOMContentLoaded', async () => {
try {
const res = await axios.get('/api/users');
console.log('API 응답:', res.data);
} catch (error) {
console.error('API 오류:', error);
}
console.log(calculator.multiply(5, 3));
console.log(calculator.divide(10, 2));
});
js
개발 서버를 실행하고 브라우저 콘솔을 확인하면:
콘솔 출력:
API 응답: [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}, {id: 3, name: "Charlie"}]
15
5
데이터를 성공적으로 받아온 것을 확인할 수 있습니다.
실제 프로젝트에서는 connect-api-mocker
같은 전용 패키지를 사용하는 것이 더 편리합니다. API 응답값을 JSON 파일로 만들어 놓고 개발 서버에 연동시킬 수 있습니다.
npm install -D connect-api-mocker
bash
프로젝트에 mocks/api/users/GET.json
파일을 생성합니다.
mocks/
└─ api/
└─ users/
└─ GET.json
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" },
{ "id": 3, "name": "Charlie" }
]
json
Webpack 5 설정:
// webpack.config.js (Webpack 5)
const ConnectApiMocker = require('connect-api-mocker');
module.exports = {
devServer: {
setupMiddlewares: (middlewares, devServer) => {
if (!devServer) {
throw new Error('webpack-dev-server is not defined');
}
// /api 요청을 mocks/api 폴더의 파일로 매핑
devServer.app.use(ConnectApiMocker('/api', 'mocks/api'));
return middlewares;
},
},
};
js
이제 /api/users
요청은 자동으로 mocks/api/users/GET.json
파일을 반환합니다.
CORS 문제 해결하기
실제 API 서버가 만들어졌다고 가정해보겠습니다.
현재 Webpack 개발 서버는 http://localhost:8080
에서 실행되고 있고, API 서버가 http://localhost:8081
에 있다면 어떻게 될까요?
이 경우 Ajax 요청을 하면 CORS
에러가 발생합니다. 브라우저의 동일 출처 정책(Same-Origin Policy)
때문입니다.
서버에서 Access-Control-Allow-Origin
헤더를 추가하는 방법이 있습니다.
// server/index.js
app.get('/api/keywords', (req, res) => {
res.header('Access-Control-Allow-Origin', '*'); // 모든 도메인 허용
res.json(keywords);
});
js
서버 응답 헤더를 수정할 필요 없이, Webpack 개발 서버에서 API 서버로 프록시하는 방법도 있습니다. 이 방법이 개발 환경에서 더 편리합니다.
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': 'http://localhost:8081',
},
},
};
js
이 설정은 개발 서버에 들어온 모든 HTTP 요청 중 /api
로 시작하는 요청을 http://localhost:8081
로 프록시합니다.
클라이언트 코드에서는 상대 경로만 사용하면 됩니다.
// API 호출 코드
import axios from 'axios';
// 전체 URL 대신 상대 경로 사용
const { data } = await axios.get('/api/keywords');
js
이렇게 설정하면 CORS 에러 없이 정상적으로 API를 호출할 수 있습니다.
참고: Next.js에서는
rewrites
속성으로, Webpack에서는proxy
속성으로 동일한 기능을 제공합니다.
Hot Module Replacement (HMR)
Hot Module Replacement(HMR)
는 애플리케이션이 실행되는 동안 변경된 모듈만 교체하는 기능입니다.
예를 들어 폼 데이터에 무언가 입력한 상태에서 관련 없는 다른 모듈을 수정했을 때, 폼에 입력한 데이터가 날아가지 않도록 해줍니다.
Webpack 5에서는 hot
옵션이 기본적으로 true
로 설정되어 있습니다.
// webpack.config.js (Webpack 5)
module.exports = {
devServer: {
hot: true, // 기본값이 true이므로 생략 가능
},
};
js
HMR을 제대로 사용하려면 모듈이 HMR 인터페이스를 구현해야 합니다.
// app.js
if (module.hot) {
console.log('HMR이 활성화되었습니다');
// calculator 모듈 변경 감지
module.hot.accept('./calculator', () => {
console.log('calculator 모듈이 변경되었습니다');
// 모듈 다시 로드
});
}
js
이렇게 HMR을 사용하면 코드가 변경될 때 전체 화면을 새로고침하지 않고 변경된 모듈만 교체됩니다. 개발 속도를 크게 향상시킬 수 있습니다.
많이 사용되는 로더들은 HMR을 기본적으로 지원합니다.
대표적인 예로 style-loader
가 있습니다. CSS 파일을 수정하면 페이지 새로고침 없이 스타일만 즉시 반영됩니다.
style-loader
내부적으로는 다음과 같은 HMR 코드가 구현되어 있습니다.
// style-loader 내부 구현 예시
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => {
// 스타일 업데이트 로직
});
}
js
최적화
Webpack에서 번들 결과물을 최적화하는 방법을 알아보겠습니다.
코드가 많아지면 번들된 결과물인 JavaScript 파일이 메가바이트 단위까지 커질 수 있습니다. 브라우저에서 큰 파일을 다운로드하는 것은 성능에 큰 영향을 줍니다.
Webpack으로 번들 결과를 압축하고, 상황에 따라 작은 파일 여러 개로 분리하는 방법을 살펴보겠습니다.
번들 크기 최적화
mode 설정
가장 간단한 방법은 mode
값을 설정하는 것입니다.
// webpack.config.js
module.exports = {
mode: 'production', // 운영 환경에서는 production 사용
};
js
개발 환경에서는 디버깅 편의를 위해 다음 플러그인들을 사용합니다.
NamedChunksPlugin
NamedModulesPlugin
또한 DefinePlugin
을 통해 process.env.NODE_ENV
값이 "development"
로 설정되어 애플리케이션 전역 변수로 주입됩니다.
mode
를 "production"
으로 설정하면 JavaScript 결과물을 최소화하기 위해 다음 플러그인들을 자동으로 사용합니다.
FlagDependencyUsagePlugin
FlagIncludedChunksPlugin
ModuleConcatenationPlugin
NoEmitOnErrorsPlugin
OccurrenceOrderPlugin
SideEffectsFlagPlugin
TerserPlugin
process.env.NODE_ENV
값이 "production"
으로 설정됩니다.
환경 변수 NODE_ENV
값에 따라 자동으로 모드를 설정하도록 구성할 수 있습니다.
// webpack.config.js
const mode = process.env.NODE_ENV || 'development';
module.exports = {
mode,
};
js
package.json
의 스크립트를 다음과 같이 설정합니다.
{
"scripts": {
"build": "NODE_ENV=production webpack --progress",
"start": "webpack-dev-server --progress"
}
}
json
이제 npm run build
를 실행하면 production 모드로 빌드됩니다. 빌드된 파일을 확인하면 코드가 난독화되고 압축된 것을 볼 수 있습니다.
optimization 속성
optimization
속성을 사용하면 빌드 과정을 더욱 세밀하게 커스터마이징할 수 있습니다.
HtmlWebpackPlugin
이 HTML 파일을 압축하는 것처럼, CSS 파일도 공백을 제거하여 압축할 수 있습니다.
먼저 플러그인을 설치합니다.
npm install -D css-minimizer-webpack-plugin
bash
Webpack 설정에 플러그인을 추가합니다.
// webpack.config.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const mode = process.env.NODE_ENV || 'development';
module.exports = {
mode,
// ...
optimization: {
minimizer: mode === 'production' ? [new CssMinimizerPlugin()] : [],
},
};
js
optimization.minimizer
는 Webpack이 결과물을 압축할 때 사용할 플러그인 배열입니다. 위 설정을 통해 빌드 결과물 중 CSS 파일이 압축됩니다.
mode=production
일 때 자동으로 사용되는 TerserWebpackPlugin
은 JavaScript 코드를 난독화하고 debugger
구문을 제거합니다.
기본 설정 외에도 콘솔 로그를 제거하는 옵션을 추가할 수 있습니다. 배포 버전에서는 보안을 위해 콘솔 로그를 제거하는 것이 좋습니다.
먼저 플러그인을 설치합니다.
npm install -D terser-webpack-plugin
bash
optimization.minimizer
배열에 플러그인을 추가합니다.
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer:
mode === 'production'
? [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 콘솔 로그 제거
},
},
}),
new CssMinimizerPlugin(),
]
: [],
},
};
js
참고: 개발 환경에서는 디버깅을 위해 콘솔 로그가 필요하지만, 운영 환경에서는 민감한 정보 노출을 방지하기 위해 제거하는 것이 좋습니다.
코드 스플리팅 (Code Splitting)
코드를 압축하는 것 외에도, 결과물을 여러 개로 분리하면 브라우저 다운로드 속도를 높일 수 있습니다. 큰 파일 하나를 다운로드하는 것보다 작은 파일 여러 개를 동시에 다운로드하는 것이 더 빠르기 때문입니다.
가장 단순한 방법은 엔트리를 여러 개로 분리하는 것입니다.
// webpack.config.js
module.exports = {
entry: {
main: './src/app.js',
sub: './src/sub.js',
},
};
js
빌드하면 dist
폴더에 main.js
와 sub.js
두 개의 파일이 생성됩니다. HtmlWebpackPlugin
이 HTML 파일에 두 스크립트를 자동으로 포함시킵니다.
그런데 main.js
와 sub.js
모두 axios
라이브러리를 사용한다면, 같은 코드가 두 파일에 중복으로 포함됩니다.
이럴 때 splitChunks
를 사용하면 중복 코드를 별도 파일로 분리할 수 있습니다.
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
js
빌드하면 다음과 같은 파일들이 생성됩니다.
📦 dist/
┣ 📜 main.js # main 엔트리 고유 코드
┣ 📜 sub.js # sub 엔트리 고유 코드
┗ 📜 vendors-main-sub.js # 공통으로 사용하는 코드 (axios 등)
전체 용량이 줄어든 것을 확인할 수 있습니다.
ls -lh dist
bash
엔트리 포인트를 수동으로 분리하는 것은 관리가 어렵습니다. Dynamic Import를 사용하면 Webpack이 자동으로 코드를 분리해줍니다.
기존의 정적 import 대신 동적 import를 사용합니다.
변경 전 (정적 import):
import calculator from './calculator';
js
변경 후 (동적 import):
document.addEventListener('DOMContentLoaded', async () => {
// 동적으로 모듈 불러오기
const module = await import(
/* webpackChunkName: "calculator" */ './calculator.js'
);
const calculator = module.default || module;
console.log(calculator.multiply(5, 3));
console.log(calculator.divide(10, 2));
});
js
/* webpackChunkName: "calculator" */
주석은 Webpack이 생성할 청크 파일의 이름을 지정하는 매직 코멘트입니다. 이를 통해 생성되는 파일명을 제어할 수 있습니다.
Dynamic Import를 사용하면 멀티 엔트리 포인트 설정을 제거할 수 있습니다.
// webpack.config.js
module.exports = {
entry: {
main: './src/app.js',
// calculator 엔트리는 더 이상 필요하지 않음
},
// splitChunks 설정도 제거 가능
};
js
빌드하면 다음과 같은 파일들이 생성됩니다.
📦 dist/
┣ 📜 main.js # 메인 번들
┣ 📜 calculator.js # Dynamic Import로 분리된 청크
┗ 📜 vendors-calculator.js # calculator에서 사용하는 외부 라이브러리
Dynamic Import를 사용하면 엔트리 포인트를 수동으로 관리하지 않아도 Webpack이 자동으로 코드를 분리해줍니다.
externals로 번들 최적화
번들 크기를 줄이는 마지막 방법은 애초에 번들에 포함하지 말아야 할 라이브러리를 빌드 범위에서 제외하는 것입니다.
axios
와 같은 외부 라이브러리는 npm에서 다운로드한 이미 빌드된 파일입니다. 이런 라이브러리를 Webpack으로 다시 빌드할 필요가 없습니다.
externals
설정을 사용하면 특정 라이브러리를 번들에서 제외하고 전역 변수로 사용하도록 설정할 수 있습니다.
// webpack.config.js
module.exports = {
externals: {
axios: 'axios', // axios 모듈을 전역 변수 axios로 대체
},
};
js
이제 Webpack은 코드에서 axios
를 발견해도 번들에 포함시키지 않고, 전역 변수 axios
를 사용하도록 빌드합니다.
externals
로 제외한 라이브러리는 직접 HTML에서 로드해야 하므로, 빌드된 파일을 dist
폴더로 복사해야 합니다.
copy-webpack-plugin
을 설치합니다.
npm install -D copy-webpack-plugin
bash
// webpack.config.js (Webpack 5)
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
externals: {
axios: 'axios',
},
plugins: [
new CopyPlugin({
patterns: [
{
from: './node_modules/axios/dist/axios.min.js',
to: './axios.min.js',
},
],
}),
],
};
js
src/index.html
에 axios를 로드하는 스크립트 태그를 추가합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- externals로 제외한 라이브러리는 직접 로드 -->
<script src="axios.min.js"></script>
</body>
</html>
html
빌드하면 dist
폴더에 axios.min.js
가 복사되고, index.html
에서 이를 로드하는 것을 확인할 수 있습니다.
npm run build
ls dist # axios.min.js 파일 확인
bash
externals의 장점은 다음과 같습니다.
- 번들 크기 감소: 이미 빌드된 라이브러리를 제외하여 번들 크기가 줄어듭니다.
- 빌드 속도 향상: 외부 라이브러리를 빌드하지 않아 빌드 시간이 단축됩니다.
- 개발/운영 환경 모두 개선: 개발 서버 시작 시간과 운영 빌드 시간 모두 단축됩니다.
이처럼 이미 빌드된 외부 라이브러리는 externals
로 빼서 관리하는 것이 효율적입니다.
핵심 정리
Webpack 학습 핵심 포인트
-
모듈 시스템의 필요성: 전역 스코프 오염을 방지하고 코드를 체계적으로 관리하기 위해 모듈 시스템이 필요합니다.
-
Webpack의 역할: 여러 모듈을 하나의 번들 파일로 합쳐주는 번들러입니다. 모든 파일(JS, CSS, 이미지)을 모듈로 처리합니다.
-
로더: 각 파일을 JavaScript 모듈로 변환합니다. (
css-loader
,style-loader
, Asset Modules 등) -
플러그인: 번들된 결과물 전체를 후처리합니다. (BannerPlugin, DefinePlugin, HtmlWebpackPlugin 등)
-
개발 서버:
webpack-dev-server
로 개발 환경을 운영 환경과 유사하게 구성하고, HMR로 개발 속도를 향상시킬 수 있습니다. -
최적화:
mode
설정으로 자동 최적화- CSS/JS 압축
- 코드 스플리팅으로 파일 분리
externals
로 외부 라이브러리 제외
-
Webpack 4 vs 5: Output 옵션, Asset Modules, devServer 설정 등에서 차이가 있으므로 버전에 맞는 설정을 사용해야 합니다.
마치며: 현대 번들러 생태계
이제 Webpack의 핵심 개념과 활용 방법을 모두 살펴보았습니다. 하지만 말씀드리고 싶은 부분이 있습니다. 요즘은 Webpack뿐만 아니라 다양한 번들러가 등장했고, 프로젝트마다 사용하는 도구가 달라지고 있습니다.
Vite가 최근에는 거의 표준처럼 자리잡았다고 볼 수 있습니다. 대부분의 신규 프로젝트에서 Vite를 사용하고 있는데요, 개발 서버 속도가 매우 빠르고 개발 환경에서는 esbuild
를, 프로덕션 빌드에서는 Rollup
을 사용해 안정성도 유지하는 구조 덕분이라고 생각합니다.
Rollup은 UI 컴포넌트 라이브러리를 제작할 때 가장 많이 사용됩니다. 디자인 시스템이나 내부 공용 컴포넌트를 개발할 때 Rollup의 깔끔한 번들링과 Tree-shaking이 큰 장점으로 작용합니다.
Webpack은 오래된 프로젝트에서 여전히 많이 사용되고 있습니다. 다양한 플러그인 생태계의 이점도 있고, 수년간 유지해온 설정을 쉽게 바꿀 수 없다는 현실적인 이유도 있습니다.
이 글의 내용이 최신 트렌드와 다소 차이가 있는 것은 사실입니다. 하지만 “번들러의 동작 원리”를 이해하는 것이 가장 중요합니다. Webpack을 통해 로더와 플러그인의 개념, 모듈 시스템의 필요성, 최적화 방법을 익히면, 이 지식을 Vite, Rollup, esbuild 등 최신 도구에도 그대로 적용할 수 있습니다.
도구는 계속 변하지만, 번들러가 해결하려는 근본적인 문제는 동일합니다. 이러한 원리를 이해하고 직접 최신 도구에 적용하는 과정에서 크게 성장할 수 있다고 생각합니다. 이것이 바로 실무 감각을 키우는 방법이기도 합니다.
실제 프로젝트에서 번들러를 선택하고 설정할 때, 이 글에서 배운 개념들을 토대로 각 도구의 특성을 비교하고 활용해보시기 바랍니다.