프론트엔드 개발환경의 이해와 실습 Section 2: Webpack(basic)
Section 2. 프론트엔드 개발환경의 이해: Webpack
REF: 김정환님의 블로그
1. 배경
문법 수준에서 모듈을 지원하기 시작한 것은 ES2015부터다.
import/export 구문이 없었던 모듈 이전의 개발 방식은 index.html에 필요한 js파일을 script 태그로 모두 추가했다.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="src/math.js"></script>
<script src="src/app.js"></script>
</body>
</html>
// src/math.js
function sum(a, b) {
return a + b;
}
// src/app.js
console.log(sum(1, 2)); // 3
문제는 전역 스코프가 오염된다는 것
1.1 IIFE 방식의 모듈
IIFE( Immediately Invoked Function Expression )은 정의되자마자 즉시 실행되는 Javascript Function을 말한다.
(function () {
statement;
})();
IIFE로 변경해보자. 전역 스코프가 오염되지 않는다.
// src/math.js
var math = math || {};
(function () {
function sum(a, b) {
return a + b;
}
math.sum = sum;
})();
// src/app.js
console.log(math.sum(1, 2)); // 3
1.2 다양한 모듈 스펙
AMD와 CommonJS가 모듈을 구현하는 대표적인 명세이다.
CommonJS 대표적으로 Node.js에서 사용한다. exports 키워드로 모듈을 만들고 require() 함수로 불러들이는 방식이다.
// src/math.js
exports function sum(a,b){return a+b}
// src/app.js
const sum = require("./math.js");
console.log(sum(1, 2)); // 3
AMD(Asynchronous Module Definition)는 비동기로 로딩되는 환경에서 모듈을 사용하는 것이 목표이며 주로 브라우저 환경이다.
UMD(Universial Module Definition)는 AMD기반으로 CommonJS 방식까지 지원하는 통합 형태이다.
ES2015에서 표준 모듈 시스템을 내 놓았다. 지금은 바벨과 웹팩을 이용해 사용하는 것이 일반적이다.
// src/math.js
export function sum(a, b) {
return a + b;
}
// src/app.js
import * as math from "./math.js";
console.log(math.sum(1, 2)); // 3
1.3 브라우저의 모듈 지원
크롬 브라우저에서 모듈을 사용하는 법
<script type="module" src="src/app.js"></script>
CORS 오류가 난다면 lite-server 패키지를 설치해 실행해보자
크롬에서는 작동하지만 모든 브라우저에서 모듈 시스템을 지원하지 않는다. 웹팩이 필요하다.
2. 엔트리/아웃풋
웹팩은 여러개 파일을 하나로 합쳐주는 번들러(bundler)다. 하나의 시작점(entry point)으로부터 의존적인 모듈을 전부 찾아내서 하나의 결과물로 만든다. app.js -> math.js -> 1 file
webpack 설치하고 번들링 작업을 해보자
npm view webpack versions
npm install -D webpack@4.46.0 webpack-cli
아래의 버전이 설치되었다.
"devDependencies": {
"webpack": "^4.46.0",
"webpack-cli": "^4.9.0"
}
help option 사용하기
$ node_modules/.bin/webpack --help
Usage: webpack [entries...] [options]
Alternative usage to run commands: webpack [command] [options]
The build tool for modern web applications.
Options:
-c, --config <value...> Provide path to a webpack configuration file e.g. ./webpack.config.js.
--config-name <value...> Name of the configuration to use.
-m, --merge Merge two or more configurations using 'webpack-merge'.
--env <value...> Environment passed to the configuration when it is a function.
--node-env <value> Sets process.env.NODE_ENV to the specified value.
--progress [value] Print compilation progress during build.
-j, --json [value] Prints result as JSON or store it in a file.
--entry <value...> The entry point(s) of your application e.g. ./src/main.js.
-o, --output-path <value> Output location of the file generated by webpack e.g. ./dist/.
-t, --target <value> Sets the build target e.g. node.
-d, --devtool <value> Determine source maps to use.
--no-devtool Do not generate source maps.
--mode <value> Defines the mode to pass to webpack.
--name <value> Name of the configuration. Used when loading multiple configurations.
--stats [value] It instructs webpack on how to treat the stats e.g. verbose.
--no-stats Disable stats output.
-w, --watch Watch for files changes.
--no-watch Do not watch for file changes.
--watch-options-stdin Stop watching when stdin stream has ended.
--no-watch-options-stdin Do not stop watching when stdin stream has ended.
Global options:
--color Enable colors on console.
--no-color Disable colors on console.
-v, --version Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.
-h, --help [verbose] Display help for commands and options.
Commands:
build|bundle|b [entries...] [options] Run webpack (default command, can be omitted).
configtest|t [config-path] Validate a webpack configuration.
help|h [command] [option] Display help for commands and options.
info|i [options] Outputs information about your system.
serve|server|s [entries...] Run the webpack dev server. To see all available options you need to install 'webpack',
'webpack-dev-server'.
version|v [commands...] Output the version number of 'webpack', 'webpack-cli' and 'webpack-dev-server' and commands.
watch|w [entries...] [options] Run webpack and watch for files changes.
To see list of all supported commands and options run 'webpack --help=verbose'.
Webpack documentation: https://webpack.js.org/.
CLI documentation: https://webpack.js.org/api/cli/.
Made with ♥ by the webpack team.
--mode
옵션 [‘development’, ‘production’, ‘none’]
--entry
옵션 - 시작점
--output
옵션 - 결과를 저장
$ node_modules/.bin/webpack --mode development --entry ./src/app.js --output-path dist
Hash: 9d440e075e5780b9c8d7
Version: webpack 4.46.0
Time: 86ms
Built at: 2021. 10. 15. 오후 9:04:09
Asset Size Chunks Chunk Names
main.js 4.81 KiB main [emitted] main
Entrypoint main = main.js
[0] multi ./src/app.js 28 bytes {main} [built]
[./src/app.js] 69 bytes {main} [built]
[./src/math.js] 38 bytes {main} [built]
dist 폴더가 생성되었고 그 안에 main.js가 생성됨
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
// ...
index.html에 적용하고 브라우저에 띄워보자
<script src="dist/main.js"></script>
매번 옵션을 터미널에 입력할 수 없으니 설정 문서를 만들자
--config
기본 옵션으로 file명은 webpack.config.js 이다.
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: {
main: "./src/app.js",
},
output: {
path: path.resolve("./dist"),
filename: "[name].js", // entry가 여러개일 경우 동적으로 생성될 수 있도록 한다.
},
};
// package.json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
$ npm run build
> sample@1.0.0 build
> webpack
Hash: 68e9e3eabbdf3e53fba5
Version: webpack 4.46.0
Time: 130ms
Built at: 2021. 10. 15. 오후 9:13:57
Asset Size Chunks Chunk Names
main.js 4.5 KiB main [emitted] main
Entrypoint main = main.js
[./src/app.js] 69 bytes {main} [built]
[./src/math.js] 38 bytes {main} [built]
3. 로더
3.1 로더의 역할
모든 것을 자바스크립트의 모듈로 만들어 사용할 수 있게 한다.
타입스크립트를 자바스크립트 문법으로 변환해 주거나 이미지를 data URL 형식의 문자열로 변환하고, CSS파일을 자바스크립트에서 직접 로딩할 수 있도록 한다.
3.2 커스텀 로더 만들기
// my-webpack-loader.js
module.exports = function myWebpackLoader(content) {
// console.log('myWebpackLoader')
return content.replace("console.log(", "alert(");
};
// webpack.config.js
{
module: {
rules: [
{
test: /\.js$/,
use: [path.resolve("./my-webpack-loader.js")],
},
];
}
}
console.log를 alert으로 변환하는 로더를 만들었다.
4. 자주 사용하는 로더
4.1 css-loader
css파일을 모듈로 변환하여 자바스크립트에서 불러와 사용할 수 있게 한다.
npm install -D css-loader@5.2.7
// app.js
import "./app.css";
/* app.css */
body {
background-color: green;
}
// webpack.config.js
module: {
rules: [
{
test: /\.css$/,
use: ["css-loader"],
},
];
}
npm run build
dist/main.js 파일에 app.css 내용이 포함되었지만 실제 브라우저에는 배경 색이 바뀌지 않았다.
적용이 되려면 css를 html에서 직접 로드하거나 인라인 코드로 변환해야한다.
4.2 style-loader
자바스크립트로 변경된 스타일 코드를 html에 넣어주는 일을 하는 style-loader
가 필요하다.
style-loader는 자바스크립트로 변경된 스타일을 동적으로 DOM에 추가하는 로더이다.
css 번들링을 위해서 이 두 로더를 함께 사용한다.
npm install -D style-loader@2.0.0
// webpack.config.js
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
];
}
npm run build
4.3 file-loader
bg.png 파일을 다운로드 받아 css에서 사용해보자
body {
background-image: url(bg.png);
}
그리고 빌드를 하면 파싱 오류가 뜬다.
npm run build
이제 file-loader가 필요하다.
npm install -D file-loader@5.1.0
// webpack.config.js
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.png/,
use: ["file-loader"],
},
];
}
npm run build
dist 폴더에 이미지 파일이 생성되었다. 파일명은 해시값이다. 웹팩이 캐시를 사용할 때 생기는 문제를 예방하기 위해 해시값으로 파일명을 변경한다.
이미지의 경로는 index.html을 기준으로 설정해준다.
publicPath는 파일로더가 처리하는 파일을 모듈로 사용할 때 경로 앞에 추가되는 문자열이다.
name은 로더가 파일을 아웃풋에 복사할 때 사용하는 파일 이름이다. 기본적으로 설정된 해쉬값을 쿼리스트링으로 옮겨서 ‘bg.png?5af0af42f49426cc73b0e7b3d7d2eb14’형식으로 파일을 요청하도록 변경했다. 매번 해시값이 달라져서 캐시 사용할 때 발생하는 문제를 예방할 수 있다.
{
test: /\.png/,
loader: 'file-loader',
options: {
publicPath: './dist/',
name: '[name].[ext]?[hash]'
}
}
npm run build
4.4 url-loader
Data URI Scheme을 사용하는 것은 이미지를 여러 개 사용할 때 네트워크 리소스 부담을 줄이고 사이트 성능에 도움을 주는 방법이다.
Data URI Scheme을 사용하기 위해 url-loader가 필요하다.
비교적 작은 크기의 파일을 추가해보자
nyancat.jpg 파일을 다운로드하고 app.js에 추가한다.
// app.js
import "./app.css";
import nyancat from "./nyancat.jpg";
document.addEventListener("DOMContentLoaded", () => {
document.body.innerHTML = `
<img src="${nyancat}" />
`;
});
그리고 파일로더가 jpg 외의 파일 확장자를 읽을 수 있도록 설정한다.
// webpack.config.js
{
test: /\.(png|jpg|gif|svg)/,
loader: 'file-loader',
options: {
publicPath: './dist/',
name: '[name].[ext]?[hash]'
}
}
npm run build
dist 폴더에 nyancat.jpg 파일이 추가됐다.
파일의 크기를 보면 nyancat.jpg는 비교적 작으므로 url-loader를 이용해 base64 인코딩해서 올린다.
$ ll src
total 1223
-rw-r--r-- 1 maphnew 197121 45 10월 16 18:46 app.css
-rw-r--r-- 1 maphnew 197121 190 10월 16 19:05 app.js
-rw-r--r-- 1 maphnew 197121 1227424 10월 16 18:45 bg.png
-rw-r--r-- 1 maphnew 197121 38 10월 15 20:46 math.js
-rw-r--r-- 1 maphnew 197121 18869 10월 16 19:04 nyancat.jpg
url-loader 설치한다.
npm install -D url-loader@3.0.0
아래와 같이 설정하면 20kb 미만일 경우 base64로 인코딩, 이상일 경우 file-loader가 실행된다.
// webpack.config.js
{
test: /\.(png|jpg|gif|svg)/,
loader: 'url-loader',
options: {
publicPath: './dist/',
name: '[name].[ext]?[hash]',
limit: 20000, // 20kb
}
}
npm run build
bg.png는 dist파일에 저장되고 nyancat.jpg는 url-loader가 처리하여 base64 인코딩되어 main.js에 들어간다.
5. 플러그인
5.1 플러그인의 역할
로더가 파일 단위로 처리하는 반면 플러그인은 번들된 결과물을 처리한다. 번들된 자바스크립트를 난독화 한다거나 특정 텍스트를 추출하는 용도로 사용한다.
5.2 커스텀 플러그인 만들기
// my-webpack-plugin.js
class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.done.tap("My Plugin", (stats) => {
console.log("MyPlugin: done");
});
}
}
module.exports = MyWebpackPlugin;
// webpack.config.js
plugins: [new MyWebpackPlugin()];
npm run build
파일은 여러 개인데 로그가 찍힌걸 보면 한 번만 찍혔다.
로더가 파일 하나 혹은 여러 개에 대해 동작하는 반면 플러그인은 하나로 번들링된 결과물을 대상으로 동작한다.
예제에서 결과물이 main.js 하나이기 때문에 플러그인이 한 번만 동작한 것이라 추측된다.
웹팩 내장 플러그인 BannerPlugin 코드를 참고하자.
// my-webpack-plugin.js
class MyWebpackPlugin {
apply(compiler) {
// compiler.hooks.done.tap('My Plugin', stats => {
// console.log('MyPlugin: done')
// })
compiler.plugin("emit", (compilation, callback) => {
const source = compilation.assets["main.js"].source();
console.log(source);
// compilation.assets['main.js'].source = () => {
// const banner = [
// '/**',
// ' * 이것은 BannerPlugin이 처리한 결과입니다.',
// ' * Build Date: 2021-10-16',
// ' */'
// ].join('\n');
// return banner + '\n\n' + source;
// }
callback();
});
}
}
module.exports = MyWebpackPlugin;
npm run build
번들링된 main.js의 소스 내용이 출력되는 것을 볼 수 있다.
커스텀 플러그인을 수정해 번들된 결과에 후처리를 해보자
// my-webpack-plugin.js
class MyWebpackPlugin {
apply(compiler) {
// compiler.hooks.done.tap('My Plugin', stats => {
// console.log('MyPlugin: done')
// })
compiler.plugin("emit", (compilation, callback) => {
const source = compilation.assets["main.js"].source();
compilation.assets["main.js"].source = () => {
const banner = [
"/**",
" * 이것은 BannerPlugin이 처리한 결과입니다.",
" * Build Date: 2021-10-16",
" */",
].join("\n");
return banner + "\n\n" + source;
};
callback();
});
}
}
module.exports = MyWebpackPlugin;
npm run build
// main.js
/**
* 이것은 BannerPlugin이 처리한 결과입니다.
* Build Date: 2021-10-16
*/
/******/ (function(modules) { // webpackBootstrap
// ...
6. 자주 사용하는 플러그인
6.1 BannerPlugin
// webpack.config.js
const webpack = require("webpack");
plugins: [
new webpack.BannerPlugin({
banner: "이거슨 배너입니다.",
}),
];
npm run build
// dist/main.js
/*! 이거슨 배너입니다. */
/******/ (function(modules) { // webpackBootstrap
// ...
좀 더 자세한 정보를 넣어보자.
// webpack.config.js
const childProcess = require("child_process");
plugins: [
new webpack.BannerPlugin({
banner: `
Build Date: ${new Date().toLocaleString()}
Commit Version: ${childProcess.execSync(
"git rev-parse --short HEAD"
)}
Author: ${childProcess.execSync("git config user.name")}
`,
}),
];
npm run build
// main.js
/*!
*
* Build Date: 2021. 10. 17. 오후 3:28:49
* Commit Version: 7bda1c0
*
* Author: maphnew
*
*
*/
/******/ (function(modules) { // webpackBootstrap
빌드하고 배포했을 때 정적파일들이 잘 배포됐는지 혹은 캐시에 의해 갱신되지 않는지 확인하기 쉽게 설정됐다.
6.2 DefinePlugin
개발환경과 운영환경으로 나눠 운영할 때 환경변수가 다르다.
환경변수 정보를 소스가 아닌 곳에서 관리하기 위해 DefinePlugin을 제공한다.
테스트 해보자.
// webpack.config.js
plugins: [
new webpack.BannerPlugin({
banner: `
Build Date: ${new Date().toLocaleString()}
Commit Version: ${childProcess.execSync(
"git rev-parse --short HEAD"
)}
Author: ${childProcess.execSync("git config user.name")}
`,
}),
new webpack.DefinePlugin({}),
];
// app.js
console.log(process.env.NODE_ENV); // "development"
process.env.NODE_ENV라는 노드 환경정보에는 웹팩 설정의 mode에 설정한 값이 여기에 들어간다.
이 외에도 직접 환경변수를 넣고 싶으면 아래와 같이 설정할 수 있다.
// webpack.config.js
new webpack.DefinePlugin({
TWO: '1+1'
}),
// app.js
console.log(TWO); // 2
// webpack.config.js
new webpack.DefinePlugin({
TWO: JSON.stringify('1+1')
}),
// app.js
console.log(TWO); // "1+1"
// webpack.config.js
new webpack.DefinePlugin({
TWO: JSON.stringify('1+1'),
'api.domain': JSON.stringify('http://dev.api.domain.com')
}),
// app.js
console.log(api.domain); // "http://dev.api.domain.com"
코드가 아닌 값을 넘길 경우 위와 같이 문자열화 한 뒤 넘긴다.
6.3 HtmlTemplatePlugin
HTML 파일을 처리하는데 사용된다.
html 파일을 웹팩 빌드 과정에 넣고 싶을 때 사용하면 된다.
빌드 타임의 값을 넣거나 코드를 압축할 수 있다.
npm install -D html-webpack-plugin@4.5.2
index.html을 src폴더로 옮겨서 소스로 관리하고 아래와 같이 main.js를 불러오는 script태그를 삭제하고 html-webpack-plugin 설정을 한다.
<!-- src/index.html -->
<!-- <script src="dist/main.js"></script> -->
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
//...
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
];
npm run build
dist 폴더에 index.html 파일이 생성 되었다.
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="main.js"></script>
</body>
</html>
html 파일을 빌드과정에 포함시킴으로써 의존적이지 않은 html코드를 생성할 수 있다.
브라우저를 확인해보면 백그라운드이미지를 불러오지 못하는데, 로더 설정에서 publicPath를 확인해보자.
이전에는 index.html파일 기준으로 경로를 설정했지만 지금은 설정할 필요가 없어졌다.
// webpack.config.js
{
test: /\.(png|jpg|gif|svg)/,
loader: 'url-loader',
options: {
// publicPath: './dist/',
name: '[name].[ext]?[hash]',
limit: 20000, // 20kb
}
}
npm run build
html파일에 변수를 주입시키거나 white space와 주석을 제거 할 수 있다.
// webpack.config.js
new HtmlWebpackPlugin({
template: "./src/index.html",
templateParameters: {
env: process.env.NODE_ENV === "development" ? "(개발용)" : "",
},
minify:
process.env.NODE_ENV === "production"
? {
collapseWhitespace: true,
removeComments: true,
}
: false,
});
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document<%= env %></title>
<!-- 이것은 주석입니다. -->
</head>
<body></body>
</html>
NODE_ENV=development npm run build
브라우저에서 타이틀이 변경된 것을 확인할 수 있다.
NODE_ENV=production npm run build
dist폴더의 index.html파일에 white space와 주석이 제거되었다.
정적파일을 배포하면 즉각 브라우저에 반영되지 않는 경우가 있는데 브라우저 캐시가 원인일 때가 있다. 예방하기 위해 다음의 옵션을 설정한다.
// webpack.config.js
new HtmlWebpackPlugin({
hash: true, // 정적 파일을 불러올 때 쿼리문자열에 웹팩 해시값을 추가한다.
});
<!-- dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Document</title>
</head>
<body>
<script src="main.js?d5650e6c37936aebdebb"></script>
</body>
</html>
hash:ture 옵션으로 빌드할 때 생성하는 해시값을 정적파일 로딩 주소의 쿼리 문자열로 붙여서 HTML을 생성한다.
6.4 CleanWebpackPlugin
output 폴더를 삭제해주는 플러그인이다.
npm install -D clean-webpack-plugin@3.0.0
//webpack.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
plugins: [
// ...
new CleanWebpackPlugin(),
];
default로 export 되어 있지 않으므로 위와 같이 import 한다.
dist 폴더에 임의의 파일을 생성하고 삭제되는지 확인해보자.
npm run build
6.5 MiniCssExtractPlugin
하나의 자바스크립트 파일이 비대해지면 성능에 있어 부담이다.
스타일시트가 많아지는 것이 원인일 경우가 있는데 이것을 분리해주는 플러그인이다.
여러 개의 작은 파일을 동시에 다운로드하는 것이 큰 파일 하나를 받는 것보다 더 빠르다.
개발 환경에서는 css를 하나의 모듈로 처리해도 되지만 프로덕션 환경에서는 분리하는 것이 효과적이다.
npm install -D mini-css-extract-plugin@1.6.2
// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
plugins: [
// ...
...(process.env.NODE_ENV === "production"
? [new MiniCssExtractPlugin({ filename: "[name].css" })]
: []),
];
이 플러그인은 css 관련 로더 설정도 필요하다.
// webpack.config.js
{
test: /\.css$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader'
]
},
NODE_ENV=production npm run build
dist폴더에 main.css가 만들어졌다.
/* dist/main.css */
/*!
*
* Build Date: 2021. 10. 17. 오후 6:37:38
* Commit Version: 7bda1c0
*
* Author: maphnew
*
*
*/
body {
background-image: url(bg.png?5af0af42f49426cc73b0e7b3d7d2eb14);
}
dist/index.html에는 main.css를 로드한 것을 볼 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Document</title>
<link href="main.css?c741a338b4f577ad5fa9" rel="stylesheet" />
</head>
<body>
<script src="main.js?c741a338b4f577ad5fa9"></script>
</body>
</html>