이번 포스팅에서는 현재 프론트엔드 어플리케이션 배포에서 가장 많이 사용되는 번들러인 Webpack에 대해서 정리하고자 합니다.
번들링이란 "사용자에게 웹 애플리케이션을 제공하기 위한 파일 묶음을 만드는 것"을 의미합니다. 모던 웹으로 발전하면서 JavaScript 코드의 양이 증가하고 세분화된 모듈 파일들이 증가했습니다. 그에 따라, JavaScript 언어의 특성에 따라 발생하기 쉬운 각 변수들의 스코프 문제와 동시에 네크워크 코스트 문제를 해결해야 했습니다.
이러한 문제를 해결하고자 하나의 시작점에서 의존성을 가지는 모듈을 추적하여 하나의 묶음으로 반드는 번들러가 등장하게 되었습니다. 번들링을 통해 네트워크 상에서 하나의 묶음으로 데이터를 받아올 수 있으며, 번들링 과정에서 배포에 최적화된 파일을 만들어 줍니다.
Webpack이란 여러 개의 파일을 하나의 파일로 합쳐주는 모듈 번들러입니다. HTML, CSS, JavaScript 등의 자원을 전부 각각의 모듈로 보고 이를 조합해 하나의 묶음으로 빌드하는 도구입니다.
Webpack에서는 HTML, CSS 파일 뿐만아니라 .jpg, .png 확장자를 가지는 이미지 파일도 관리합니다. 다양한 로더를 사용하면 다양한 파일들을 번들링 할 수 있습니다.
빌드는 개발이 완료된 앱을 배포하기 위해서 그 앱을 구성하는 파일들을 하나의 폴더로 만드는 과정을 의미합니다. 각각의 파일들에 존재하는 import, export 키워드들을 바탕으로 의존적 관계를 파악해 그룹화 됩니다.
실제 프로젝트에 사용하지 않기 때문에 devDependency 옵션을 설정하고 설치합니다. webpack과 함게 webpack-cli를 설치해야 합니다. webpack-cli는 webpack을 더 쉽게 사용할 수 있도록 build, serve, loader와 같은 다양한 명령을 제공합니다.
npm install -D webpack webpack-cli
Webpack의 기본 설정을 위한 JS 파일을 생성합니다. 아래의 코드는 번들링이 시작되는 파일과 번들링이 완료된 파일이 생성의 이름과 경로를 지정해주는 코드 입니다.
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'), // './dist'의 절대 경로를 리턴합니다.
filename: 'app.bundle.js',
},
};
번들링을 하기 위해서는 아래와 같은 명령어를 입력해야 합니다. 해당 명령어를 입력하고 번들링 결과를 확인합니다.
npx webpack
아래의 코드는 webpack의 config 파일 예시입니다. target 프로퍼티는 컴파일을 위한 환경을 의미합니다. 기본값은 "web"이며, 배열안에 "es5"를 입력해주면 브라우저와 동일한 환경에서 사용하기 위하여 컴파일할 것이며, 작성된 코드를 es5 버전으로 컴파일 하겠다고 지정하는 것입니다.
module.exports = {
target: ["web", "es5"],
entry: "./src/script.js",
output: {
path: path.resolve(__dirname, "docs"),
filename: "app.bundle.js",
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html"),
}),
new MiniCssExtractPlugin(),
],
optimization: {
minimizer: [
new CssMinimizerPlugin(),
]
}
};
Entry는 작성한 코드의 시작점으로 Entry point로 부터 의존성이 있는 다른 모듈과 라이브러리를 찾아내 디펜던시 그래프를 생성하게 됩니다. 기본 값은 ./src/index.js이지만 다른 entry point를 지정할 수 있습니다.
// 기본값
module.exports = {
...
entry: "./src/index.js", // index.js 파일을 시작으로 의존성 그래프를 생성합니다.
};
// 지정 값
module.exports = {
...
entry: "./src/script.js",
};
Output은 생성된 번들이 출력되는 부분으로 번들 파일의 이름과 위치를 설정할 수 있습니다. 기본 출력 파일의 경우에는 ./dist/main.js로 , 생성된 기타 파일의 경우에는 ./dist 폴더로 설정됩니다. output.clean 옵션을 사용하면 빌드 후 오래된 파일 없이 새롭게 생성된 파일만 output 경로에 남아있게 됩니다.
onst path = require('path');
module.exports = {
...
output: {
path: path.resolve(__dirname, "dist"), // 절대 경로로 설정을 해야 합니다.
filename: "app.bundle.js", // root 경로의 dist 폴더 내, app.bundle.js 파일로 번들링됩니다.
clean: true
},
};
Webpack은 기본적으로 JavaScript와 JSON 파일만 이해하지만, Loader를 사용하면 다양한 유형의 파일을 처리할 수 있습니다. 그리고 그 것들을 모듈로 변환해 앱에서 사용하거나 디펜던시 그래프에 추가할 수 있습니다.
module.exports = {
...
module: {
rules: [
{
test: /\.css$/, // 파일명이 .css로 끝나는 모든 파일에 적용시킵니다.
// 배열 마지막 요소부터 오른쪽에서 왼쪽 순으로 적용됩니다.
// 먼저 css-loader가 적용되고, 그 다음으로 styled-loader가 적용되는 순서입니다.
use: [MiniCssExtractPlugin.loader, "style-loader", "css-loader"],
// loader가 node_modules 폴더 내부에 있는 파일을 처리 하지 않도록 제외해야 합니다
exclude: /node_modules/,
},
],
},
};
Plugins를 사용하면 번들을 최적화하거나 에셋을 관리, 환경변수 죽입 등 다양한 작업을 수행할 수 있습니다. 우선 Plugin을 설치합니다.
npm i -D html-webpack-plugin mini-css-extract-plugin
설치된 Plugin을 Plugins 프로퍼티의 배열에 추가합니다.
const webpack = require('webpack');
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
...
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"], // mini-css-extract-plugin의 loader 와 style-loader가 충돌할 수 있으니 주의해야합니다.
},
],
},
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
minimize: true, // 개발 모드에서도 CSS 최적화를 사용합니다.
},
plugins: [
new HtmlWebpackPlugin({ // 루트 경로의 src 폴더의 index.html 파일을 번들링 하여 Output 경로에 번들링된 html 파일 생성
template: path.resolve(__dirname, "src", "index.html"),
}),
new MiniCssExtractPlugin(), // CSS를 별도의 파일로 추출해 CSS를 포함하는 JS 파일 당 CSS 파일을 작성하는 플러그인
new CssMinimizerPlugin(), // CSS를 최적화하고 축소합니다.
],
};
Webpack 4버전 부터 선택한 항복에 따라 최적화를 실행합니다. 최적화의 다양한 옵션 중에 가장 많이 사용되는 것이 , minimize, minimizer이 있습니다.
module.exports = {
...
optimization: {
minimizer: [
new CssMinimizerPlugin(),
]
}
};
mode 파라미터를 development, production 또는 none으로 설정하면 webpack에 내장된 환경별 최적화를 활성화 할 수 있습니다. 기본값은 production 입니다. webpack.config.js 파일에서 각각의 모드에 따른 설정을 지정할 수 있습니다.
// webpack.config.js
module.exports = {
mode: 'production',
...
};
module.exports = {
mode: 'development',
...
};
development : DefinePlugin의 process.env.NODE_ENV를 development로 설정합니다. 모듈과 청크에 유용한 이름을 사용할 수 있습니다.
production : DefinePlugin의 process.env.NODE_ENV를 production으로 설정합니다. 모듈과 청크, FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, TerserPlugin 등에 대해 결정적 망글이름(mangled name)을 사용할 수 있습니다.
none
none : 기본 최적화 옵션에서 제외합니다.
HTML, JS, CSS 파일을 docs 폴더로 번들링하는 webpack.config.js 파일입니다. HtmlWebpackPlugin, MiniCssExtractPlugin를 통해서 HTML, CSS 파일을 별도의 .html, .css 확장자 파일로 번들링하고 MinimizerPlugin을 통해서 CSS를 최적화 합니다.
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
target: ["web", "es5"],
mode: "development",
entry: "./src/script.js",
output: {
path: path.resolve(__dirname, "docs"),
filename: "app.bundle.js",
clean: true,
},
plugins: [ // 플러그인을 추가 합니다.
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "src", "index.html"),
}),
new CssMinimizerPlugin(),
new MiniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.css$/, // 파일명이 .css로 끝나는 모든 파일에 적용합니다.
use: [MiniCssExtractPlugin.loader, "css-loader"],
exclude: /node_modules/,
},
],
},
optimization: {
minimizer: [new CssMinimizerPlugin()],
minimize: true, // 개발 모드에서도 CSS 최적화를 사용합니다.
},
};
리액트를 사용하는 경우에는 jsx 파일을 파싱하기 위한 babel loader가 필요합니다. 우선, babel을 사용하기 위한 패키지들을 설치해 줍니다.
npm i -D @babel/core @babel/preset-react babel-loader s @babel/preset-env
// package.json
"devDependencies": {
...
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^9.1.2",
}
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
mode: "development",
entry: ".//src/app.js",
output: {
path: path.resolve(__dirname, "docs"),
filename: "app.bundle.js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(
__dirname,
"src",
"index.html"
),
}),
new CssMinimizerPlugin(),
new MiniCssExtractPlugin(),
],
module: {
rules: [
// css loader 설정
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
exclude: /node_modules/,
},
// jsx loader 설정
{
test: /\.jsx?/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
],
},
optimization: {
minimizer: [new CssMinimizerPlugin()],
minimize: true,
},
devServer: {
static: "./docs",
},
};
webpack-dev-server는 간단한 웹 서버와 실시간 다시 로딩 기능을 제공합니다. 아래의 코드는 dist 디렉터리의 파일을 localhost:8080에서 제공합니다.
npm install webpack-dev-server
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
devServer: {
static: './dist',
}
};
npx webpack serve --open
require 문법은 Node.js의 CommonJS 문법으로 브라우저에서 지원하지 않기 떄문에 떄문에 Browserify, RequireJS 및 SystemJS와 같은 번들러와 도구를 만들어 브라우저에서 실행되는 CommonJS 모듈을 작성을 가능하게 합니다.
그리고 Webpack은 ES 모듈의 import, export 키워드와 CommonJS의 require 키워드를 섞어서 사용하는 것이 가능하도록 합니다.
지금까지 리액트를 사용하면서 CRA에 기본적으로 설치되어 있는 Webpack 설정만을 사용해 왔습니다. 이번에 직접 Webpack 설정을 해보면서 정말 많은 플러그인이 존재하는 것을 알 수 있었고 자원을 최소한으로 사용하기 위해 프로젝트 환경에 맞는 커스터마이징이 필요하겠다고 느꼈습니다. 앞으로 사이드 프로젝트에 더 다양한 플러그인을 적용해볼 계획입니다.