최적화
코드가 많아지면 번들링된 결과물도 커지기 마련이다.
거의 메가바이트 단위로 커질수도 있는데 이는 브라우저 성능에 영향을 줄 수 있다.
다운로드 시간이 오래걸릴 것이기 때문이다.
파일을 다운로드하는데 시간이 많이 걸리기 때문이다.
이번 섹션에서는 번들링한 결과물을 어떻게 최적화할 수 있는지 몇 가지 방법에 대해 알아보겠다.
어떻게 웹팩으로 번들 결과를 압축하고 또 상황에 따라서는 작은파일 여러개로 분리할 수 있는지 알아보도록 하겠다.
우선 가장 쉬운 방법인 webpack.config.js
파일의 mode
값을 사용하는 방법이다.
production 모드
웹팩에 내장되어 있는 최적화 방법 중 mode
값을 설정하는 방식이 가장 기본이다.
세 가지 값이 올 수 있는데 지금까지 설정한 “development”는 디버깅 편의를 위해 아래 두 개 플러그인을 사용한다.
- NamedChunksPlugin
- NamedModulesPlugin
DefinePlugin을 사용한다면 process.env.NODE_ENV
값이 “development”로 설정되어 어플리케이션에 전역변수로 주입된다.
반면 mode
를 “production”으로 설정하면 자바스크립트 결과물을 최소화하기 위해 다음 일곱개 플러그인을 사용한다.
- FlagDependencyUsagePlugin
- FlagIncludedChunksPlugin
- ModuleConcatenationPlugin
- NoEmitOnErrorsPlugin
- OccurrenceOrderPlugin
- SideEffectsFlagPlugin
- TerserPlugin
DefinePlugin을 사용한다면 process.env.NODE_ENV
값이 “production”으로 설정되어 어플리케이션 전역변수로 주입된다.
그동안 우리는 공부하면서
mode
값을development
로 두고 실습했다.// webpack.config.js module.exports = { mode: "development" }
그런데 development가 아닌 production으로 설정해야 운영환경에 적합한 번들결과를 만들어낼 수 있다.
그럼 노드 환경변수 NODE_ENV 값에 따라 모드를 설정하도록 웹팩 설정 코드를 다음과 같이 추가할 수 있다.
// webpack.config.js
const mode = process.env.NODE_ENV || 'development'; // 기본값을 development로 설정
module.exports = {
mode,
}
process.env.NODE_ENV
가 없으면 mode
는 기본값인 development
로 설정된다.
위 코드는 키와 값의 변수명이 같기 때문에 mode라고만 적어주면된다.
빌드시에 이를 운영 모드로 설정하여 실행하도록 package.json
에 scripts
부분에 npm 스크립트를 아래와 같이 추가한다.
package.json
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "NODE_ENV=production webpack --progress",
"lint": "eslint src --fix",
"start": "webpack-dev-server --progress"
}
}
“build” 부분에 NODE_ENV=production
을 추가했다.
npm start
로 실행될 때는 노드 환경변수 값을 전달하지 않았다.
이 경우엔 mode
는 기본값인 development
로 실행될 것이다.
위와 같은 오류가 나는 경우가 있다.
이런 경우엔
npm i -g win-node-env
명령어로 win-node-env
를 설치해주면 해결된다.
npm run build
파일이 난독화되었다.
이것은 production
모드에서 사용하는 플러그인들에 의해서 이렇게 난독화된 것이다.
CSS는 그대로인 것을 볼 수 있다.
그럼 이전과 비교하기 위해서 이번엔 development
로 두고 빌드해보자.
package.json
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "NODE_ENV=development webpack --progress",
"lint": "eslint src --fix",
"start": "webpack-dev-server --progress"
}
}
그럼 이렇게 코드를 읽기 쉽게 해서 번들링한다.
mode
의 production
은 1차적으로 코드를 최적화할 수 있는 방법이다.
optimization 속성으로 최적화
빌드 과정을 커스터마이징할 수 있는 여지를 제공하는데 그것이 바로 optimization
속성이다.
HtmlWebpackPlugin이 html 파일을 압축한 것 처럼
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
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,
}),
]
}
css 파일도 빈칸을 없애는 압축을 하려면 어떻게 해야할까?
optimize-css-assets-webpack-plugin
이 바로 그것이다.
npm i -D optimize-css-assets-webpack-plugin
이 플러그인은 일반 플러그인과 설정방법이 조금 다르다.
일반 플러그인은 plugins
키에 설정을 했는데, 이것은 optimization
키에서 설정한다.
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: mode === 'production' ? [
new OptimizeCSSAssetsPlugin(),
] : [],
}
}
optimization
의 minimizer
부분엔 여러개 플러그인을 설정할 수 있기 때문에 [] 배열형태로 넣는다.
npm run build
위와 같이 CSS 파일도 압축된 것을 확인할 수 있다.
이것 뿐만아니라 mode=production
일 경우 사용되는 7가지 플러그인 중 TerserWebpackPlugin은 자바스크립트 코드를 난독화하고 debugger 구문을 제거한다.
기본 설정 외에도 콘솔 로그를 제거하는 옵션도 있는데 배포 버전에는 로그를 감추는 것이 좋을 수도 있기 때문이다.
운영에 나가는 결과물에 console.log
가 찍히고 그러는 것은 보기에 안 좋을 수 있다. 정보노출 위험도 있고.
npm i -D terser-webpack-plugin
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: mode === 'production' ? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 콘솔 로그를 제거한다.
}
}
})
] : [],
}
}
코드 스플리팅 (코드 분할)
코드를 압축하는 것 외에도 아예 결과물을 여러개로 쪼개면 좀 더 브라우저 다운로드 속도를 높일 수 있다.
큰 파일 하나를 다운로드하는 것 보다 작은 파일 여러개를 동시에 다운로드하는 것이 더 빠르기 때문이다.
가장 단순한 것은 엔트리를 여러개로 분리하는 것이다.
// webpack.config.js
module.exports = {
entry: {
main: "./src/app.js",
result: "./src/result.js",
},
}
npm run build
위와 같이 main.js
, result.js
두가지 결과물이 나온다.
그리고 번들된 dist/index.html
을 보면 두 결과물을 모두 로딩하는 것을 볼 수 있다.
이것은 html-webpack-plugin
이 하는 역할이다.
그런데 문제는
빌드 전 파일에 위와 같이 import
되어있기 때문에 결과물을 확인해보면
위와 같이 동일한 코드가 각각의 결과물에 들어간 것을 볼 수 있다.
중복되는 코드가 각각 entry 파일에 들어가는 것이다.
그래서 이와 같은 중복을 제거하는 옵션도 켜줘야된다.
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: mode === 'production' ? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 콘솔 로그를 제거한다.
}
}
})
] : [],
splitChunks: {
chunks: "all"
},
}
}
위와 같이 splitChunks
와 chunks
를 설정해주면 중복되는 코드를 전부 제거하고 entry 파일을 다시 빌드한다.
아까와 다르게 vendors~main~result.js
란 파일이 하나 더 생겼다.
보면 아까와 다르게 axios
검색 결과가 없다.
vendors~main~result.js
파일엔 axios
가 검색된다.
main.js
와 result.js
에서 중복된 코드는 다 vendors~main~result.js
파일로 빠지게 되어있다.
용량도 한번 살펴보도록 하자.
용량이 이전과 비교했을 때 줄어들었을 것이다.
그런데 entry
포인트로 분리하는 것은 개발자가 일일이 분리해야되기 때문에 손이 많이가는 방법이다.
그래서 뭔가 자동화하는 방법이 필요한데, 그걸 웹팩에선 다이나믹 임포트(dynamic import), 동적 임포트라고 한다.
다이나믹(동적) 임포트 사용방법도 알아보도록 하겠다.
다이나믹(동적) 임포트
import
로 파일 연동할 때
// app.js
import form from "./form";
import result from "./result";
import "./app.css";
let resultEl;
let formEl;
document.addEventListener("DOMContentLoaded", async () => {
formEl = document.createElement("div");
formEl.innerHTML = form.render();
document.body.appendChild(formEl);
resultEl = document.createElement("div");
resultEl.innerHTML = await result.render();
document.body.appendChild(resultEl);
});
이런식이 아니라 다음과 같이 가져올 수 있다.
// app.js
import form from "./form";
// import result from "./result";
import "./app.css";
let result;
let resultEl;
let formEl;
document.addEventListener("DOMContentLoaded", async () => {
formEl = document.createElement("div");
formEl.innerHTML = form.render();
document.body.appendChild(formEl);
import(/* webpackChunkName: "result" */"./result.js").then(async m => {
result = m.default;
resultEl = document.createElement("div");
resultEl.innerHTML = await result.render();
document.body.appendChild(resultEl);
})
});
위와 같이 설정하면 웹팩이 위 코드를 보고 알아서 result.js
모듈은 따로 번들한다.
// webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: {
main: "./src/app.js",
// result: "./src/result.js",
},
optimization: {
minimizer: mode === 'production' ? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 콘솔 로그를 제거한다.
}
}
})
] : [],
// splitChunks: {
// chunks: "all"
// },
}
}
대신 위와 같이 entry 포인트와 splitChunks 부분은 지워줘야된다.
npm run build
이렇게 다이나믹 임포트로 지정하면 주석으로 지정한 webpackChunkName: "result"
의 result
이름으로 result.js
와 vendors~result.js
파일을 만들어준다.
사실 이런식으로 코드 스플리팅하는 것은 개발 초기단계에는 불필요하다.
코드가 많아지고 번들했는데 파일이 1Mb 이상으로 커져버리게되면 그때가서 코드를 분리해도 늦지 않는다.
externals
마지막 방법으로는 애초에 번들하지 말아야할 대상들은 빌드범위 대상에서 빼버리는 것이다.
그것이 바로 externals라는 것이다.
조금만 더 생각해보면 최적회해 볼 수 있는 부분이 있다.
바로axios
같은 써드파티 라이브러리다.
패키지로 제공될 때 이미 빌드과정을 거쳤기 때문에 빌드 프로세스에서 제외하는 것이 좋다.
웹팩 설정 중externals
가 바로 이러한 기능을 제공한다.
// webpack.config.js
module.exports = {
externals: {
axios: "axios",
},
}
externals
에 추가하면 웹팩은 코드에서 axios
를 사용하더라도 번들에 포함하지 않고 빌드한다.
대신 이를 전역 변수로 접근하도록하는데 키로 설정한 axios
가 그 이름이다.
즉, 웹팩으로 빌드할 때
axios
를 모듈을 사용하는 부분이 있으면, 전역변수axios
를 사용하는 것으로 간주하라. 라는 의미이다.
// result.js
import axios from "axios";
const result = {
async render() {
const res = await axios.get("/api/users");
return (res.data || [])
.map((user) => {
return `<div>${user.id}: ${user.name}</div>`;
})
.join("");
},
};
export default result;
우리 실습 코드 중에 result.js
파일에서 axios
모듈을 사용하는데, 위와 같은 코드가 있다면 axios
를 저 부분에 import
하지 말고 그냥 전역변수 axios가 있는 것처럼 해서 웹팩이 빌드한다.
대신에 axios
모듈을 전역변수 axios에 담아쓰려면 가져와야겠지?
axios
모듈은 node_modules
폴더 안에 있다.
axios.min.js
파일을 html 파일에 로딩하면 된다.
로딩하기 위해선 위 파일도 우리 결과물 폴더인 dist
폴더에 있는 것이 좋겠지?
그래서 웹팩이 실행될 때 파일을 복사해야되는데, 복사하는 라이브러리가 copy-webpack-plugin
이다.
npm i -D copy-webpack-plugin
그리고 webpack.config.js
의 plugins
에 해당 플러그인을 정의해주면 된다.
// webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: "./node_modules/axios/dist/axios.min.js",
to: "./axios.min.js",
}
]
}),
],
};
html 파일에도 axios 파일을 로딩하는 코드를 넣어주자.
<!-- ./src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Title<%= env %></title>
<!-- 이것은 주석입니다. -->
</head>
<body>
<script src="axios.min.js"></script>
</body>
</html>
npm run build
이런식으로 웹팩이 빌드할 필요가 없는 외부 라이브러리는 externals
로 빼놓는 것이 좋다.
운영환경 뿐만아니라 개발 환경에서도 이런 외부 라이브러리는 따로 빌드하지 않기 때문에, 이렇게 빼놓으면 웹팩 빌드시간을 단축시킬 수 있다.
이 부분들은 개발 초기에는 잘 안 쓰는 부분이다.
개발 후반부로가서 배포하기 전에 빌드용량이 커지면 그때가서 설정하는데 이러한 기본적인 방법을 숙지하고 있으면 좋을 거 같다.