인프런의 프론트앤드 개발환경의 이해와 실습을 듣고 일부 내용을 정리해본다.
어제에 이어 webpack의 기능을 좀 더 정리해보자.
yarn add -D webpack-dev-server
webpack-dev-server를 설치한 후 package.json의 script에 추가한다.
"scripts": {
"build": "webpack",
"start": "webpack-dev-server"
},
yarn start로 실행하면 http://localhost:8080/으로 웹 서버가 실행된다.
터미널을 살펴보면 webpack이 실행된 것을 알 수 있다.
관련 파일을 수정해면 브라우저가 바로 갱신된다. css 파일만 수정해도 브라우저에 바로 반영된다.
webpack.config.js 파일에서 webpack-dev-server의 옵션을 설정할 수 있다.
const path = require('path');
const webpack = require('webpack');
const child_process = require('child_process');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
path: path.resolve('./dist'),
filename: '[name].js',
},
devServer: {
overlay: true,
stats: 'errors-only',
},
module: {
rules: [
{
test: /\.css$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader',
],
},
{
test: /\.(jpg|png|gif|svg)$/,
loader: 'url-loader',
options: {
publicPath: './',
name: '[name].[ext]?[hash]',
limit: 20000, // 20k
},
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: `
Build Date: ${new Date().toLocaleDateString()}
`,
}),
new webpack.DefinePlugin({
VALUE: '1 + 1',
STRING: JSON.stringify('1 + 1'),
'api.domain': JSON.stringify('https://api.domain.com'),
}),
new HtmlWebpackPlugin({
template: './src/index.html',
templateParameters: {
env: process.env.NODE_ENV === 'development' ? '(DEV)' : '',
},
minify:
process.env.NODE_ENV === 'production'
? {
collapseWhitespace: true,
removeComments: true,
}
: false,
}),
new CleanWebpackPlugin(),
...(process.env.NODE_ENV === 'production'
? [new MiniCssExtractPlugin({ filename: '[name].css' })]
: []),
],
};
devServer: {
overlay: true,
stats: 'errors-only',
},
에러가 발생하면 브라우저에 overlay 된다.
자주 쓰이는 옵션은 따로 정리해보자.
api mock 기능을 추가할 수 있다.
const path = require('path');
const webpack = require('webpack');
const child_process = require('child_process');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/app.js',
},
output: {
path: path.resolve('./dist'),
filename: '[name].js',
},
devServer: {
overlay: true,
stats: 'errors-only',
before: (app) => {
app.get('/api/users', (req, res) => {
res.json([
{
id: 1,
name: 'Alice',
},
{
id: 2,
name: 'Bob',
},
{
id: 3,
name: 'Mike',
},
]);
});
},
},
module: {
rules: [
{
test: /\.css$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader',
],
},
{
test: /\.(jpg|png|gif|svg)$/,
loader: 'url-loader',
options: {
publicPath: './',
name: '[name].[ext]?[hash]',
limit: 20000, // 20k
},
},
],
},
plugins: [
new webpack.BannerPlugin({
banner: `
Build Date: ${new Date().toLocaleDateString()}
`,
}),
new webpack.DefinePlugin({
VALUE: '1 + 1',
STRING: JSON.stringify('1 + 1'),
'api.domain': JSON.stringify('https://api.domain.com'),
}),
new HtmlWebpackPlugin({
template: './src/index.html',
templateParameters: {
env: process.env.NODE_ENV === 'development' ? '(DEV)' : '',
},
minify:
process.env.NODE_ENV === 'production'
? {
collapseWhitespace: true,
removeComments: true,
}
: false,
}),
new CleanWebpackPlugin(),
...(process.env.NODE_ENV === 'production'
? [new MiniCssExtractPlugin({ filename: '[name].css' })]
: []),
],
};
devServer: {
overlay: true,
stats: 'errors-only',
before: (app) => {
app.get('/api/users', (req, res) => {
res.json([
{
id: 1,
name: 'Alice',
},
{
id: 2,
name: 'Bob',
},
{
id: 3,
name: 'Mike',
},
]);
});
},
},
http://localhost:8080/api/users로 접근하면 json을 반환한다.
axios를 설치하고 app.js 코드를 다음과 같이 수정해보자.
import './app.css';
import party from './party-8-240.png';
import axios from 'axios';
document.addEventListener('DOMContentLoaded', async () => {
document.body.innerHTML = `
<img src="${party}" />
`;
const res = await axios.get('/api/users');
console.log(res);
});
화면이 로드되자마자 api가 호출되고 결과가 console에 출력된다.
import './app.css';
import party from './party-8-240.png';
import axios from 'axios';
document.addEventListener('DOMContentLoaded', async () => {
document.body.innerHTML = `
<img src="${party}" />
`;
const res = await axios.get('/api/users');
console.log(res);
document.body.innerHTML += (res.data || []).map(
(user) => `<div>${user.id}: ${user.name}</div>`
).join('');
});
결과를 출력했다.
먼저 package를 설치한다.
yarn add -D connect-api-mocker
src와 같은 level에 mocks 폴더를 생성하고 하위 폴더와 파일을 추가한다.
mocks/api/users/GET.json
폴더 경로 api/users는 request 요청 url과 동일하게 설정한다.
그리고 GET 요청이기 때문에 GET.json 파일을 생성한다.
[
{
"id": 1,
"name": "Alice"
},
{
"id": 2,
"name": "Bob"
},
{
"id": 3,
"name": "Mike"
}
]
데이터는 devServer와 동일하다.
webpack.config.js 파일을 수정하자.
const apiMocker = require('connect-api-mocker');
devServer: {
overlay: true,
stats: 'errors-only',
before: (app) => {
app.use(apiMocker('/api', './mocks/api'));
},
},
apiMocker는 urlRoot와 pathRoot를 인자로 받는다. 각 항목을 입력하면 devServer에서 request에 응답하던 것과 동일한 역할을 수행할 수 있다.
개발 중 실제 백앤드 서버의 API를 테스트할 때 CORS 문제로 테스트에 어려움을 겪는 경우가 종종 있다.
localhost:xxxx를 백앤드에 등록하여 해결할 수 도 있지만 webpack devServer proxy를 사용하여 해결할 수 도 있다.
devServer: {
overlay: true,
stats: 'errors-only',
proxy: {
'/api': 'http://server-domain',
},
},
front 코드는 devServer에 api 응답을 요청하고, devServer는 백앤드 서버에 다시 응답을 요청한다.
이때 proxy가 CORS가 발생하지 않도록 처리한다.
다음 글도 참고하자. https://react.vlpt.us/redux-middleware/09-cors-and-proxy.html
DefinePlguin을 사용하고 proess.env.NODE_ENV를 설정하면 해당 값이 전역변수로 주입된다.
mode를 정의하여 process.env.NODE_ENV 값에따라 분기할 수 있도록 처리한다.
const mode = process.env.NODE_ENV || 'development';
module.exports = {
mode,
};
package.json의 scirpt에 production을 추가하여 실행하보자.
"scripts": {
"build": "NODE_ENV=production webpack",
"start": "NODE_ENV=production webpack-dev-server --progress"
},
yarn build를 실행하면 dist의 main.js가 난독화되어 생성된다.
package를 설치한다.
yarn add -D optimize-css-assets-webpack-plugin
webpack.config.js 파일에 추가한다.
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
devServer: {
overlay: true,
stats: 'errors-only',
before: (app) => {
app.use(apiMocker('/api', './mocks/api'));
},
hot: true,
},
optimization: {
minimizer: mode === 'production' ? [new OptimizeCSSAssetsPlugin()] : []
},
이렇게 설정한 후 NODE_ENV=production webpack
를 실행하면 dist 폴더의 css 파일의 공백이 모두 제거된 것을 확인할 수 있다.
js 파일을 압축하고, console 코드를 자동으로 삭제할 수 있다.
설치한다.
yarn add -D terser-webpack-plugin
webpack.config.js에 추가한다.
const TerserPlugin = require('terser-webpack-plugin');
optimization: {
minimizer:
mode === 'production'
? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
]
: [],
},
webpack을 production 모드로 실행하면 bundle 파일에 적용된다.
entry를 추가해보자.
module.exports = {
mode,
entry: {
main: './src/app.js',
result: './src/result.js',
},
}
result를 추가했다.
이 상태로 yarn build를 실행하면 dist 폴더에 result관련 파일들이 생성된다.
index.html을 살펴보면 app.js, result.js를 모두 로드하는 것을 볼 수 있다.
하지만 app.js와 result.js에 중복 코드가 많이 존재하게 된다.
이 중복 코드를 제거하기 위해 다음과 같이 설정한다.
optimization: {
minimizer:
mode === 'production'
? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
]
: [],
splitChunks: {
chunks: 'all',
},
},
설정 후 yarn build를 실행하자.
설정 전 후 결과를 비교해보자.
// 중복 제거 전
Asset Size Chunks Chunk Names
cat01.jpg?e9138897b09dd1181948c510691fecd7 721 KiB [emitted] [big]
index.html 279 bytes [emitted]
main.css 153 bytes 0, 1 [emitted] main
main.js 16.2 KiB 0, 1 [emitted] main
main.js.LICENSE.txt 55 bytes [emitted]
result.js 15.7 KiB 1 [emitted] result
result.js.LICENSE.txt 55 bytes [emitted]
// 중복 제거 후
Asset Size Chunks Chunk Names
cat01.jpg?e9138897b09dd1181948c510691fecd7 721 KiB [emitted] [big]
index.html 325 bytes [emitted]
main.css 153 bytes 1, 2 [emitted] main
main.js 2.21 KiB 1, 2 [emitted] main
main.js.LICENSE.txt 55 bytes [emitted]
result.js 1.71 KiB 2 [emitted] result
result.js.LICENSE.txt 55 bytes [emitted]
vendors~main~result.js 14.7 KiB 0 [emitted] vendors~main~result
vendors~main~result.js.LICENSE.txt 55 bytes [emitted]
main.js, result.js 파일의 용량이 줄어들었다. 그리고 venders… 라는 파일이 추가되었다.
중복코드는 vendors… 파일에 포함된다.
bundle할 필요가 없는 대상은 제외하는 것이 좋다.
bundle할 필요가 없는 대상이란 보통 package로 설치하는 라이브러리들이다.
axios를 처리해보자.
webpack.config.js 파일을 수정한다.
optimization: {
minimizer:
mode === 'production'
? [
new OptimizeCSSAssetsPlugin(),
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
]
: [],
splitChunks: {
chunks: 'all',
},
},
externals: {
axios: 'axios',
},
externals의 의미는 axios를 전역변수를 사용하는 것으로 간주한다는 의미다.
전역변수 처럼 사용하려면 node_modules/axios/dist/axios.min.js 파일을 html에서 로드해두면 된다.
build 할 때 이 파일을 dist 폴더에 복사한다.
복사를 위해 copy-webpack-plugin을 사용한다.
yarn add -D copy-webpack-plugin
const CopyPlugin = require('copy-webpack-plugin');
...
module.exports = {
...
plugins: [
...
new CopyPlugin([{
from: './node_modules/axios/dist/axios.min.js',
to: './axios.min.js',
}])
],
};
plugins에 CopyPlugin 설정을 한다.
from, to를 설정하는데 to는 dist 경로를 생략해야 한다. dist를 추가하면 dist/dist/axios.min.js 가 된다.
src/index.html 파일에서 ./dist/axios 파일을 로드할 수 있도록 script를 추가한다.
<!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="axios.min.js"></script>
</body>
</html>
여기서도 경로에 주의하자.
dist 폴더의 파일들의 용량을 확인해보면 axios 파일을 제외하고 나머지 파일들의 용량이 크게 줄었다.
axios 파일의 용량이 크게 차지하고 있었음을 알 수 있다.