webpack을 주제로 선정하게 된 이유는 프론트 개발 환경에서 가장 중추적인 역할을 하고 있기 때문입니다.
그러다보니 프론트엔드 개발자라면 한 번쯤은 깊게 학습해볼 필요가 있는 부분이라 생각하고 스스로 학습 내용을 정리할겸 주제로 선정하게 되었습니다.
1. 웹팩이란 무엇이고 왜 사용하는가?
웹팩에 대해 학습해본 사람이라면 한 번쯤 위와 같은 이미지를 본 적이 있을 것입니다.
웹팩에 대해 한 문장으로 표현을 한다면 자바스크립트 모듈 번들러
라고 표현할 수 있습니다. 즉, 여러 개의 모듈을 하나로 번들링(묶어주는) 역할을 합니다. 이때 여러 개의 모듈이라하면 javascript object 뿐만 아닌, css파일, image파일 등 모든 resource들이 될 수 있습니다.
es6(자바스크립트 버전)에서 새롭게 도입된 import
, export
키워드를 통해 모듈을 가져오고 내보내는 코드는 일반적으로 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { ChangeEventHandler, useCallback } from 'react';
interface Props {
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export const TextInput: React.FC<Props> = ({value, onChange}) => {
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e);
};
return (
<>
<div>TextInput</div>
<input value={value} onChange={handleValueChange}/>
</>
);
};
여기서 보내면 import 로 리액트 라이브러리를 가져오고 TextInput을 만들어 내보내고 있습니다. 이는 또한 아래와 같이 App.js에서 불러져와서 브라우저에 그려지게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useCallback, useState } from 'react';
import {TextInput} from 'construction-map-widget'
interface Props {
}
const App: React.FC<Props> = (props) => {
return (
<div>
<TextInput value={text} onChange={handleTextChange}/>
</div>
);
};
export default App;
여기서 웹팩의 역할을 import로 가져오는 모듈들을 합쳐 하나의 번들을 만들어주는 역할을 하여 하나의 javascript 파일에 모든 코드가 다 들어가게 됩니다. 이 역할이 웹팩의 가장 중요한 역할이며 모듈 번들러라 불리는 이유입니다.
2. 웹팩의 또 다른 역할
위에서 언급했던 것처럼 웹팩은 이뿐만 아니라 여러 가지 중요한 핵심 역할을 수행합니다. 먼저 예시 웹팩 설정 파일은 아래와 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const childProcess = require("child_process");
const CopyPlugin = require('copy-webpack-plugin');
const info = require('./package');
module.exports = (env = {}) => {
const prod = env.mode !== "production";
return {
mode: prod ? "production" : "development",
devtool: prod ? "hidden-source-map" : "inline-source-map", // 모드에 따라 SourceMap 확인 여부
entry: {
main: path.resolve(__dirname, './src/main.ts'),
},
output: {
path: path.resolve("./lib/"),
filename: "[name].js",
library: {
name: info.name,
type: 'umd'
},
globalObject: 'this',
},
resolve: {
alias: {
Util: path.resolve(__dirname, 'src/util'),
},
extensions: [".js", ".jsx", ".ts", ".tsx"], // 배열안 확장자에 따라서 처리
},
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
loader: "url-loader", // fallback 기본값이 file-loader이기 때문에 20kb 이상은 file-loader 가 처리한다.
options: {
// publicPath: './dist/', // 파일 로더가 처리하는 파일을 모듈로 사용했을때 경로 앞에 추가되는 문자열이다, 파일을 호출하는 측에선 dist 를 붙이고 파일을 호출할 것이다.
name: "[name].[ext]?[hash]", // 파일 로더가 output에 복사할때 사용하는 파일 이름, [원본 파일명].[확장자]?해쉬값
limit: 20000 // 20kb 미만의 파일은 url-loader 로 해서 base64로 변환한다.(파일을 javascript 문자열로 변환) 만약 2kb 이상일 경우 file-loader가 실행하도록 한다.
}
},
{
test: /\.(ts|tsx)$/,
use: ["babel-loader", "ts-loader"],
exclude: /node_modules/,
},
{
test: /\.(less)$/,
use: [
{loader: prod ? MiniCssExtractPlugin.loader : 'style-loader'},
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
{loader: 'less-loader'}
]
}
]
},
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({
}),
new CleanWebpackPlugin(),
...(prod ? [new MiniCssExtractPlugin({ filename: "[name].css" })] // javascript 에서 css 파일을 뽑아내는 과정이기에 굳이 개발환경에서는 필요없다(Javascript 파일 하나로 빌드하는 것이 더 빠르다)
: []),
// new CopyPlugin({
// patterns: [
// { from: "./node_modules/axios/dist/axios.min.js", to: "./axios.min.js" },
// ],
// }),
],
devServer: {
host: 'localhost', // host 설정
port: 3000,// port 설정
open: true, // 서버를 실행했을 때, 브라우저를 열어주는 여부
compress: true,
hot: true,
historyApiFallback: true,
},
}
}
프론트엔드 개발 환경 설정 파일 중 아마 가장 긴 설정 파일이지 않을까 싶은데요..!!!
여기서 주요한 부분들을 몇 가지 살펴보도록 하겠습니다.
1) entry
entry는 웹팩이 번들링을 시작하는 시작 경로입니다. 즉, 어떤한 모듈부터 차례대로 번들링을 시작할 것인지 설정을 하는 것입니다. 아래 예시에서는 main이라는 key로 ./src/main.ts
모듈부터 시작할 것이다라는 것을 명시하고 있습니다.
1
2
3
entry: {
main: path.resolve(__dirname, './src/main.ts'),
},
2) output
output의 경우에는 실제 번들링 결과물을 위치 시킬 경로를 지정하는 옵션입니다. 아래 예시에서는 ./lib/
경로 밑에 [name].js
파일로 번들링한다고 지정되어 있는데요 실제 위 entry에서 지정한 key값인 main이 들어가서 main.js 파일로 번들 결과물이 생성됩니다.
1
2
3
4
output: {
path: path.resolve("./lib/"),
filename: "[name].js",
},
3) module
위에서 웹팩은 모듈을 번들링해주는데 그러면 어떻게 스타일 시트
, 이미지
, 폰트
까지도 전부 모듈로 볼 수 있을까요? 이는 로더가 모든 것들을 모듈로 변환해주기 때문인데요 또한 여러 가지 자바스크립트 버전의 문법들을 모든 브라우저에서 호환 가능하여 동작하도록 javascript 버전을 낮춰 주는 역할을 합니다.
예를 들어, arrow function의 경우엔 es6에서 제공해주는 문법인데 이는 IE에서 지원을 하지 않기에 IE에서 지원하는 ES5문법으로 변환을 해주어야합니다. 이러한 역할도 수행한다고 볼 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
module: {
rules: [
{
test: /\.(png|jpg|gif|svg)$/,
loader: "url-loader", // fallback 기본값이 file-loader이기 때문에 20kb 이상은 file-loader 가 처리한다.
options: {
// publicPath: './dist/', // 파일 로더가 처리하는 파일을 모듈로 사용했을때 경로 앞에 추가되는 문자열이다, 파일을 호출하는 측에선 dist 를 붙이고 파일을 호출할 것이다.
name: "[name].[ext]?[hash]", // 파일 로더가 output에 복사할때 사용하는 파일 이름, [원본 파일명].[확장자]?해쉬값
limit: 20000 // 20kb 미만의 파일은 url-loader 로 해서 base64로 변환한다.(파일을 javascript 문자열로 변환) 만약 2kb 이상일 경우 file-loader가 실행하도록 한다.
}
},
{
test: /\.(ts|tsx)$/,
use: ["babel-loader", "ts-loader"],
exclude: /node_modules/,
},
{
test: /\.(less)$/,
use: [
{loader: prod ? MiniCssExtractPlugin.loader : 'style-loader'},
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
{loader: 'less-loader'}
]
}
]
},
위 설정 파일에는 rules 안에 object들을 설정해줬는데요 ts, tsx파일을 예시로 들어보겠습니다.
test
에 명시된 /\.(ts|tsx)$/,
의 경우엔 ts확장자 및 tsx확장의 경우엔 use
에 적힌 ["babel-loader", "ts-loader"]
로더들을 사용하여 모듈로 변환하라는 설정입니다. 여기서 로더의 순서는 배열의 뒤에 적힌 것 부터 먼저 실행되어 ts-loader로 타입스크립트 => 자바스크립트로 먼저 변환해주고, babel-loader로 javascript 문법을 es5로 변환해줍니다. (어느 버전으로 변환해줄지는 따로 설정이 가능합니다)
로더와 관련된 더 자세한 내용은 여기를 참고해주세요:)
4) plugins
로더는 번들되기전 파일에 대해 처리한다면, plugins는 번들 결과물에 대해 처리를 지원합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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({
}),
new CleanWebpackPlugin(),
...(prod ? [new MiniCssExtractPlugin({ filename: "[name].css" })] // javascript 에서 css 파일을 뽑아내는 과정이기에 굳이 개발환경에서는 필요없다(Javascript 파일 하나로 빌드하는 것이 더 빠르다)
: []),
// new CopyPlugin({
// patterns: [
// { from: "./node_modules/axios/dist/axios.min.js", to: "./axios.min.js" },
// ],
// }),
],
예시에서 명시된 플러그인을 설명드리면 다음과 같습니다.
- BannerPlugin: 번들 파일에 배너를 추가하는 플러그인, build 정보를 출력하기 위해 설정하였습니다.
- DefinePlugin: 환경 변수와 같은 정보 및 env와 같은 값들을 설정하기 위한 플러그인
- CleanWebpackPlugin: 번들링 시작 전에 기존 번들 결과물이 존재할 경우 전부 삭제시켜주는 플러그인
- MiniCssExtractPlugin: css 파일을 번들 결과물에서 별도로 분리해주는 플러그인
- CopyPlugin: 특정 라이브러리를 번들 디렉토리에 복사해주는 플러그인
자주 사용하는 웹팩 플러그인에 대해 더 알고 싶다면 여기를 참고해주세요.
5) devServer
웹팩은 자체적으로 내장 서버를 제공합니다.(spring boot의 내장 톰캣처럼) 그래서 devServer를 실행시킬 경우 express 서버가 내부에서 실행됩니다. 옵션에 대한 설명은 주석을 참고해주세요
1
2
3
4
5
6
7
devServer: {
host: 'localhost', // host 설정
port: 3000,// port 설정
open: true, // 서버를 실행했을 때, 브라우저를 열어주는 여부
compress: true, // 모든 항목에 대해 gzip압축을 사용할지에 대한 설정
hot: true,// Hot Module Replacement 설정
},
6) resolve
resolve의 alias 설정과 관련해서 진우님과 같이 삽질했던 기억이 남아서 남겨봤습니다.
보통 모듈을 import 로 가져올때 다음과 같이 상대경로로 해서 가져옵니다.
1
import parentOverrideTest from './store/overrideTest';
이때 alias 설정을 하면 절대 경로를 통해서 다음과 같이 가져올 수 있습니다.
1
import func1 from 'Util/func1';
더 자세한 사용법은 여기 참고해주세요.