직접 해보는 Webpack 설정

설치부터 주요 특징 및 설정까지 알고 쓰자

2019-02-09

Webpack

webpack은 모던 자바스크립트 애플리케이션을 위한 정적 모듈 번들러입니다.
모던 애플리케이션의 경우 모듈 단위로 개발을 하는 경우가 많은데 직접 작성한 모듈 설치한 모듈들의 의존성을 파악하기란 너무나 어려운 일입니다.

저같은 경우 gulp, grunt 같은 테스크러너에 merge, minify, uglify등의 테스크를 등록하여 번들링(?) 하였었는데 테스크러너는 기본적으로는 의존성 모듈을 관리해주지 않았기 때문에 명시적으로 번들링할 대상들을 관리해줘야 한다는 불편함이 있었습니다. 때문에 개발자가 코드를 잘 작성해도 문제가 생기는등 관리 포인트가 늘어나는 아쉬운 점들이 있었습니다.

하지만 webpack은 모듈간 의존성을 스스로 파악하여 번들링해 주기 때문에 번거롭게 신경쓰지 않아도 된다는 장점이 있습니다. 그 외에도 여러가지 장점이 있겠지만 이번 포스팅에서는 webpack을 좀 더 잘 쓰기위한 주요 설정과 개인적으로 유용하다고 생각되는 몇가지 팁에 대해서 작성해 보도록 하겠습니다.

Webpack install & init

webpack과 webpack-cli를 설치하고 기본 값 셋팅된 package.json 파일을 생성합니다.

1
2
npm i -g webpack webpack-cli && webpack webpack-cli -D// install webpack, webpack-cli
npm init -y // generate default package.json

webpack config 파일은 아래 웹팩 속성 확인 후 직접 작성해 줍니다.

Webpack Property

  • mode: 해당값에 따라 내부 최적화를 따릅니다.
  • entry: 라이브러리 및 모듈을 로딩을 시작할 포인트 설정
  • output: 산출물 파일명 및 파일 path 설정
  • loader: 로더는 웹팩 번들링 시점에 중간에 개입
  • plugins: 번들링 완료 후 마지막 output 시점에 개입
  • resolve: 모듈로딩 관련 옵션 설정, 모듈 해석방식 정의(alias등)
  • devtool: 디버깅을 위한 소스맵 제공
  • devServer: 빌드를 위한 개발 서버
1
2
3
4
5
6
7
8
9
10
module.exports = {
mode: '',
entry: {},
output: {},
module: {}, // loader
plugins: [] // {array},
resolve: {},
devtool: '',
devServer: {}
}

Webpack Command Line

  • webpack: 빌드 기본명령(개발용)
  • webpack -p: minification 기능이 들어간 빌드(주로 배포용)
  • webpack -watch (-w): 개발에서 빌드할 파일의 변화를 감지
  • webpack -d: sourcemap 포함 빌드
  • webpack --display-error-details: error 발생시 디버깅 정보를 상세히 출력
  • webpack --optimize-minimize --define process.env.NODE_ENV=“‘production’”: 배포용

Mode

번들링시 mode에 따른 built-in 최적화를 수행합니다. process.env.NODE_ENV 를 설정해도 mode를 자동으로 set 해주지는 않으며 기본값으로 ‘production’ 모드를 가집니다.

Example

1
2
3
module.exports = {
mode: 'production'
};
option description
development process.env.NODE_ENV 를 development 으로 설정합니다. NamedChunksPlugin,NamedModulesPlugin 활성화 합니다
production process.env.NODE_ENV 를 production 으로 설정합니다. FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 및 TerserPlugin을 사용합니다.
none 기본 최적화 옵션 사용

Entry

라이브러리 및 모듈을 로딩을 시작할 엔트리 포인트입니다.
아래와 같이 string, array, object 방식으로 선언이 가능하며 object 타입으로 선언한 경우 output 설정시 [name]이라는 속성으로 엔트리의 키값을 참조할 수 있습니다.

string

1
2
3
{
etnry : 'string.js'
}

array

1
2
3
{
entry : ['array.js']
}

object

1
2
3
4
5
6
7
8
9
10
11
12
13
{
entry : {
app: './src/app.js', // 잦은배포
vendors: './src/vendors.js' // 버전업 잘 없는 경우
}
}
{
entry : {
page1: './src/page1/index.js',
page2: './src/page2/index.js',
vendors: './src/vendors.js'
}
}

Output

빌드 산출물의 경로와 파일명을 설정합니다. name, hash, chunkhash 속성을 사용할 수 있습니다.

output option

  • name: 엔트리 명에 따른 output
  • hash: webpack build에 따른 output
  • chunkhash: chunk에 따른 output
1
2
3
4
5
6
7
{
output: {
path: '/home/cdn/assets/[hash]', // 빌드된 번들 파일이 위치할 파일의 절대 경로
publicPath: 'http://script.auction.co.kr/assets/[hash]/', // 브라우저가 참고할 번들링 결과 파일의 URL주소(CDN 호스트) 반드시 앞뒤 / 추가해 줘야합니다.
filename: '[name].js' // entry의 key name => name 으로 참조 됩니다.
}
}

Loader

번들링 시점에 특정 동작을 처리하기 위해 사용됩니다. 번들링 완료 후가 아닌 중간에 개입한다는 특성을 가지고 있습니다. (Loader는 module 이라는 속성명으로 사용됩니다.)

Array[String]

1
2
3
4
5
6
7
8
module: {
rules: [
{
test: /regExp/, // regExp
use: ['loader name'] // {string} loader name list
}
]
}

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.png$/,
use: ['url-loader?limit=1']
}
]
}

Array[string] (expose, imports loader)

1
2
3
4
5
6
7
{
test: /backbone/,
use: [
'expose-loader?Backbone',
'import-loader?_=underscore,jquery'
]
}

Array[Object]

1
2
3
4
5
6
7
8
9
10
11
12
13
module: {
rules: [
{
test: /regExp/, // regExp
use: [
{
loader: 'loader name', // {string} loader name
options: {} // {object} option object
}
]
}
]
}

Example

babel-loader 케이스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader', // {string} loader name
options: {
presets: [
[
'es2015',
'react',
{modules: false}
]
] // babel 플러그인 리스트
} // {object} option object
}
] // {object}
}
]
}

Plugin

파일별 커스텀 기능을 사용하기 위해서 사용하며 번들링 완료 후 마지막 output 시점에 개입한다는 특성이 있습니다.

Array

1
2
3
module.exports = {
plugins: [] // {array}
}

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new webpack.ProvidePlugin({
$: 'jquery'
}),
// 모든 모듈에서 사용할 수 있도록 해당 모듈을 변수로 변환한다.
// 즉 각 모듈(파일)에서 매번 import할 필요 없이 해당 변수를 통하여 참조가능
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify('5fa3b9'),
BROWSER_SUPPORTS_HTML5: true,
TWO: '1+1',
'typeof window': JSON.stringify('object')
}),
// 웹팩 번들링 시작하는 시점에 사용 가능한 상수값 정의 가능
// 개발계, 테스트계에 따라 다른 설정 적용시 유용하다.
new ManifestPlugin({
fileName: 'manifest.json',
basePath: './dist/'
}),
// 번들링시 생성되는 코드에 대한 정보들을 json 파일 안에 담아줘 라이브러리들간 의존성 파악 용의
]
}

Resolve

webpack을 좀 더 편리하게 사용할 수 있게 해주는 옵션이라고 보시면 됩니다. 다양한 옵션을 추가로 사용할 수 있지만 간략하게 alias, modules 두가지 옵션만 알아보도록 하겠습니다. 좀 더 자세한 옵션 목록과 사용법은 Resolve 가이드를 참고해 주세요.

alias

실제 모듈 Path가 아닌 alias에서 선언한 모듈 Path로 로딩이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
resolve:{
alias: {
Utilities: path.resolve(__dirname, 'src/path/utilities/')
}
}
}
// 기존 모듈 로딩
import Utility from '../../src/path/utilities/utility';

// resolve alias option 설정 시 모듈 로딩
import Utility from 'Utilities/utility';

modules

require, import 등 모듈 로딩시에 기준이되는 폴더를 설정할 수 있습니다.

1
2
3
4
5
module.exports = {
resolve:{
modules: ['node_modules'] // defaults
}
}
1
2
3
4
5
module.exports = {
resolve:{
modules: [path.resolve(__dirname, 'src'), 'node_modules'] // src/node_modules
}
}

Devtool

디버깅을 위한 소스맵을 제공하며 소스맵 스타일을 정할 수 있습니다. build, rebuild 속도에 영향을 줄 수 있는 옵션입니다. 자세한 소스맵 스타일 값은 devtool 가이드를 확인해 주세요.

1
2
3
module.exports = {
devtool: '#inline-source-map'
}

DevServer

webpack-dev-server

devServer를 사용하여 빌드를 위한 개발 서버를 구성할 수 있습니다. webpack 자체에서 제공하는 개발 서버로 빠른 리로딩 기능을 제공하며 번들된 파일은 in memory에 올라가기 때문에 가시적으로 볼 수 없습니다. 프로토타이핑이 끝나고 배포할 때 실제 bundle 파일이 생성됩니다.

1
npm i webpack-dev-server -D

Example

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
//...
devServer: {
publicPath: '/assets', // 절대경로로 지정하고 항상 /를 앞뒤에 붙여야 한다.
contentBase: path.join(__dirname, 'dist'), // 서버가 로딩할 static 파일 경로
// contentBase: false, // 비활성화
compress: true, // gzip 압축 방식을 이용하여 웹 자원의 사이즈를 줄인다.
// 파일을 줄이는게 아니라 서버와 클라이언트간 압축방식 정의
port: 9000
}
};

Run server with cli

1
webpack-dev-server --open

Run server with script

1
"scripts": { "start": "webpack-dev-server"} 
1
npm start

Webpack 사용시 알아두면 좋은 TIP

Webpack 빌드를 위한 개발 서버 구성

앞서 언급된 devServer를 이용하는 방법도 있지만 이미 서버가 구성된 경우에는 webpack을 미들웨어로 구성하여 서버와 연결할 수 있습니다.

webpack-dev-middleware

기존에 구성한 서버에 weback 에서 컴파일한 파일을 전달하는 middleware wrapper로 webpack에 설정한 파일을 변경시, 파일에 직접 변경 내역을 저장하지 않고 메모리 공간을 활용합니다. (변경된 파일내역을 파일 디렉토리 구조안에서는 확인이 불가능합니다.)

Install

1
npm i express webpack-dev-middleware -D

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express');
const webapck = require('webpack');
const webpackDevMiddleware = requrie('webpack-dev-middleware');
const webpackConfig = require('./webpack.config');

const app = express();
const compiler = webpack(webpackConfig);

app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath, // 일반적으 output에 설정한 publicPath 사용
stats: {colors: true}, // 번들링 시 webpack 로그 컬러 하이라이팅
lazy: true // entry point에 네트워크 요청이 있을 때만 컴파일 다시함
}));

app.listen(3000, function() {
console.log('listening on port 3000');
})

Gulp 연동

webpack 에서도 많은 기능을 제공하지만 테스크러너와 연동이 필요한 경우도 있을 수 있습니다. 아래와같이 pipeline 중간에 웹팩 설정을 넣어주면 됩니다.

1
2
3
4
5
6
7
8
9
10
const gulp = require('gulp');
const webpack = require('webpack-stream');
const webpackConfig = require('./webpack.config.js');

gulp.task('default', function(){
return gulp
.src('src/entry.js')
.pipe(webpack(webpackConfig)) // 걸프 테스크 중간에 웹팩 추가
.pipe(gulp.dest('dist/'));
})

모듈내 라이브러리 중복 로딩되지 않도록 처리하기

아래 코드경우 개별 모듈에서 moment, lodash 를 import 하면 main.js와 vendor.js 번들파일에 중복으로 말리게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
entry: {
main: './app/index.js',
vendor: [
'moment',
'lodash'
]
},
output: {
filename: '[name].js', // main.js, vendor.js
path: path.resolve(__dirname, 'dist')
}
}

ProvidePlugin 으로 moment, lodash 모듈을 전역으로 빼고 import하지 않는 방법도 있지만 CommonsChunkPlugin 플러그인을 사용하여 공통 모듈이 중복 로딩되지 않도록 처리할 수 도 있습니다.

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
module.exports = {
entry: {
main: './app/index.js',
vendor: [
'moment',
'lodash'
]
},
output: {
filename: '[name].js', // main.js, vendor.js
path: path.resolve(__dirname, 'dist')
},
module: {},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' // output bundle file name
// or
names: ['vendor', 'manifest'] // 매 번들시 번들파일에 웹팩 초기화 하는 부트스트랩 코드가 들어가는데 manifest.js 그 부분까지 분리해 낼 수 있다. 매번 번들되는 코드의 양을 좀 더 줄일 수 있다.
})

// and 번들된 파일 관리 목록 json
// npm i webpack-manifest-plugin --save-dev
new ManifestPlugin({
fileName: 'manifest.json',
basePath: './dist/'
})
]
}

join, resolve 차이점

webpack config 작성이나 nodejs 작업시 join, resolve를 많이 접할 수 있습니다. 개인적으로 헷갈렸던 두 메소드의 차이점에 대해서 확인해 보겠습니다.

path.join()

OS의 파일 구분자를 이용하여 파일 위치를 조합합니다. OS의 파일 구분자를 이요하기 때문에 결과값이 OS에 따라 달라짐에 유의해야합니다.

1
2
path.join('/foo', 'bar', 'baz/asdf');
// '/foo/bar/baz/asdf'

path.resolve()

join()의 경우 문자열을 합치기만 하지만 resolve()는 오른족에서 왼쪽으로 파일 위치를 구성해가며 유효한 위치를 찾습니다. 만약 결과 값이 유효하지 않으면 현재 디렉토리가 사용되며 반환되는 위치 값은 항상 absolute URL 이고 absolute URL이 만들어 지면 종료됩니다.
파라미터에 들어온 유효하지 않은 path 까지 알아서 제거해 주기 때문에 항상 유효한 path만 받을 수 있다는 장점이 있습니다.

1
2
3
4
5
6
7
8
9
path.resolve('/foo/bar', './baz');
// '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/');
// '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 현재 폴더 위치가 /home/myself/node 라면
// '/home/myself/node/wwwroot/static_files/gif/image.gif'

정리

webpack 주요 설정 속성과 몇가지 팁에 대해서 알아봤습니다. zero configuration 이 트렌드가 되는 요즘 configpack 이라는 놀림을 받고 있는 webpack 이지만 점점 더 좋아질 것으로 생각합니다. 지금도 react나 vue를 위한 설정은 cli로 아무런 설정없이 바로 사용이 가능하며 custom을 위해 eject를 사용하는 방법도 있습니다.

설정 없이 자동으로 된다 해도 아직까지는 설정에 대한 학습이 필수라고 생각하기 때문에. 가장 정리가 잘 되어있고 친절한 webpack.js.org를 자주 열어보고 학습하는 것을 추천드립니다.