실제 배포 환경을 구축하기 위한 유틸리티와 좋은 사례들에 대해 알아보자.
development 와 production의 빌드 목표는 매우 다르다. development 같은 경우에는 강력한 소스 매핑을 통한 디버깅, localhost 서버에서 라이브 리로딩이나 hot module replacement 기능 위주로 사용하게 된다. 그에 반면 production에서는 로드 시간을 줄기위 위해 번들 최소화, 가벼운 소스맵 및 에셋 최적화에 초점을 맞춘다. 논리적으로는 webpack 설정을 둘로 나눠서 작업하면 되지만 이렇게 되면 중복 환경이 중첩되게 된다.
중복을 제거하기 위해 '공통'설정을 따로 분리하고 이를 development 또는 production에 합치기 위해서 webpack-merge 유틸리티를 사용한다.
npm install --save-dev webpack-merge
webpack-demo
|- package.json
|- package-lock.json
|- webpack.common.js
|- webpack.dev.js
|- webpack.prod.js
|- /dist
|- /src
|- index.js
|- math.js
|- /node_modules
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Production',
}),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
};
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: './dist',
},
});
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});
위와 같이 설정을 하면 환경별로 merge를 통해서 원하는 환경설정을 가져와 쓸 수 있게 된다.
위에 정의한 설정 파일들을 사용하기 위해 npm scripts 파일을 수정해보자.
{
"name": "development",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
// dev 모드에서 사용
"start": "webpack serve --open --config webpack.dev.js",
// prod 모드에서 사용
"build": "webpack --config webpack.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"css-loader": "^0.28.4",
"csv-loader": "^2.1.1",
"express": "^4.15.3",
"file-loader": "^0.11.2",
"html-webpack-plugin": "^2.29.0",
"style-loader": "^0.18.2",
"webpack": "^4.30.0",
"webpack-dev-middleware": "^1.12.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0",
"xml-loader": "^1.2.1"
}
}
웹팩과 타입스크립트를 통합시키는 방법을 알아보자.
먼저 타입스크립트 컴파일러와 로더를 설치해야한다.
npm install --save-dev typescript ts-loader
webpack-demo
|- package.json
|- package-lock.json
|- tsconfig.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
|- index.js
|- index.ts
|- /node_modules
jsx를 지원하도록 간단하게 설정하고 Typescript를 ES5로 컴파일한다.
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
이렇게하면 웹팩이 ./index.ts를 통해 진입하고 , ts-loader를 통해 모든 .ts및 .tsx파일을 로드해서 bundle.js에 출력하게 된다.
ts-loader를 사용하면 다른 웹 애셋 import 같은 추가적인 웹팩 기능을 조금 더 쉽게 활성화 할 수 있지만 이미 babel-loader를 사용하여 코드를 트랜스파일하고 있는 경우라면 @babel/preset-typescript 를 사용하여 javascript와 typescript 파일을 모두 처리하도록 하게 하면 된다.
소스맵을 사용하려면 Typescript가 컴파일된 Javascript 파일로 인라인 소스맵을 출력하도록 설정해야한다. 아래 설정을 꼭 추가하자.
{
"compilerOptions": {
"outDir": "./dist/",
++ "sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
}
}
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.ts',
++ devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
타입스크립트에서 .svg 파일을 import 할려면 custom.d.ts파일을 통해 새로운 모듈 선언을 해줘야한다.
declare module '*.svg' {
const content: any;
export default content;
}
모든 에셋에 대한 기본 경로를 publicPath 설정을 통해 지정할 수 있다.
output.publicPath 설정을 통해 원하는 기본 에셋 경로를 설정할 수 있다.
import webpack from 'webpack';
// 환경 변수를 사용하고 존재하지 않는다면 루트를 사용하세요.
const ASSET_PATH = process.env.ASSET_PATH || '/';
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// 코드에서 환경 변수를 안전하게 사용할 수 있습니다.
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
앞에서 공부했듯이 애셋 모듈은 로더를 추가로 구성하지 않아도 애셋파일 사용할 수 있게 해주는 모듈이다.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
};
import mainImage from './images/main.png';
img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'
모든 .png 파일을 출력 디렉터리로 내보내고 해당 경로를 번들에 삽입한다. outputPath 및 publicPat를 사용자가 지정할 수 있다.
asset/resource는 기본적으로 [hash][ext][query] 파일명을 사용한다. webpack 설정을 통해 파일명을 수정할 수 있다.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
++ assetModuleFilename: 'images/[hash][ext][query]'
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
},
++ {
++ test: /\.html/,
++ type: 'asset/resource',
++ generator: {
++ filename: 'static/[hash][ext][query]'
++ }
}
]
},
};
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline'
},
]
}
};
import metroMap from './images/metro.svg';
block.style.background = `url(${metroMap})`; // url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDo...vc3ZnPgo=)
모든 .svg 파일은 data URI로 번들에 삽입된다. 기본적으로 Base64 알고리즘을 사용하여 인코딩하지만 커스텀 인코딩알고리즘도 설정을 통해 할 수 있다.
const path = require('path');
const svgToMiniDataURI = require('mini-svg-data-uri');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
return svgToMiniDataURI(content);
}
}
}
]
},
};
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
}
test: /\.txt/,
type: 'asset/source',
}
]
},
};
Hello World
import exampleText from './example.txt';
block.textContent = exampleText; // 'Hello world'
webpack은 조건에 따라 resource 와 inline중 자동 선택하는데 크기가 8kb 미만인 파일은 inline 모듈로 처리되고 그렇지 않으면 resource로 처리된다.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
}
]
},
};
Rule.parser.dataUrlCondition.maxSize 옵션을 통해 조건을 변경할 수 있다.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
]
},
};
Javascript의 스타일에 import를 사용하지 않는 애플리케이션에서 CSS 및 Javascript 파일을 별도의 번들로 얻기 위해서는 엔트리 값을 배열로 사용하여 다른 유형의 파일을 제공할 수 있다.
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: process.env.NODE_ENV,
entry: {
home: ['./home.js', './home.scss'],
account: ['./account.js', './account.scss'],
},
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.scss$/,
use: [
// 개발환경에서는 style-loader로 대체 합니다
process.env.NODE_ENV !== 'production'
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
};
위와 같이 설정을 하면 아래와 같은 결과물이 나온다.
이것으로 webpack의 getting started의 모든 내용을 다뤄보았다. 기존에 몰르고 썼던 것들을 이제 간략히나마 알고 쓸 수 있게 된 것 같다. 앞으로는 회사에서 사용하고 있는 module federation을 통한 micro-frontend 서비스를 직접 구현해보는 시간을 갖도록 하겠다.