webpack 실전 가이드


Note: 이 글은 webpack1을 다루는 글입니다. webpack1은 deprecated 되었으므로 가급적 webpack2를 사용하시는 것이 좋습니다. webpack2를 다루는 글은 이 링크를 참조하세요.


webpack: module bundler

webpack은 모듈 번들러로, 의존성을 가진 모듈들을 다루고, 그 모듈로부터 정적인 asset을 생성한다.

webpack 페이지 공식 설명

모듈 로더, 모듈 번들러

Node.js는 CommonJS 표준이 구현되어 있으므로 별도의 라이브러리 없이도 쉽게 코드를 모듈화 할 수 있다. 그러나 브라우저에서는 이러한 CommonJS API가 지원되지 않았으므로 모듈화를 구현하기 어렵다.

브라우저에서도 모듈화를 지원하기 위해 CommonJS 혹은 AMD(Asynchronous Module Definition) 같은 스펙을 구현한 많은 라이브러리들이 있다. 그 중 대표적으로 널리 쓰이는 것으로 RequireJS를 꼽을 수 있을 것이다. RequireJS는 모듈 로더로, <script> 태그 없이도 AMD 스타일의 모듈을 구현한 스크립트를 동적으로 로드할 수 있도록 도우는 라이브러리다.

모듈 번들러도 이와 같이 모듈 형태로 작성된 스크립트를 브라우저에서 사용할 수 있도록 해주지만, 동적으로 불러오는 모듈 로더와는 달리, 사용하고 있는 의존성 모듈들을 빌드 과정을 통해 하나의 스크립트에 포함시키게 된다. 이렇게 의존성 모듈이 포함된 스크립트를 번들이라고 부른다.

혹시나 CommonJS나 AMD에 대해 처음 들어보았다면 이 링크를 참조.

webpack

이러한 기능을 하는 모듈 번들러 중 널리 사용되는 것으로는 Browserifywebpack이 있다. 두 도구 모두 CommonJS, AMD를 지원하지만 철학이 상이하다. 이 글은 두 도구의 차이점에 대해 다루는 글은 아니므로, 어떤 부분이 다른지 구체적으로 알고 싶다면 Browserify와 Webpack, Browserify VS Webpack - JS Drama 같은 글을 참고하면 도움이 될 것 같다.

webpack은 단순한 모듈 번들러가 아니라, npm과 조합하면 사용하기에 따라서 gulp나 grunt 같은 task runner까지 대체가능하다는 점에서 매우 다재다능한 툴이다. 물론 gulp나 grunt도 webpack용 플러그인을 제공하기 때문에 조합해서 사용하는 것 역시 가능하다.

이 글에서는 webpack만을 다루며, webpack의 설치부터 기본적인 CommonJS 모듈과 Stylesheet를 포함한 빌드와 babel을 이용한 ES2015 사용 + ESLint를 이용한 Linting까지 step by step으로 소개할 것이다.

Hello, world!

먼저 webpack을 가지고 놀기 위한 놀이터 디렉토리를 만들자.

$ mkdir webpack-playground && cd webpack-playground

webpack을 사용하기 위해서는 당연히 설치가 필요하다. npm으로 간단히 설치할 수 있다.

$ npm install -g webpack

다음과 같이 빌드된 코드를 로드할 html 코드를 작성한다.

<!-- index.html -->
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>

그리고 사용할 모듈을 구현한다.

// hello.js
module.exports = 'Hello';
// world.js
module.exports = 'world';

다음으로 해당 모듈을 사용하는 컨트롤러를 작성한다.

// entry.js
var hello = require('./hello');
var world = require('./world');
document.write(hello + ', ' + world + '!');

마지막으로 webpack을 통해 해당 코드를 빌드한다.

$ webpack entry.js bundle.js

그러면 현재 디렉토리 내에 bundle.js 파일이 생성된다. 그 상태로 index.html 파일을 브라우저에서 보면 Hello, world!가 출력된 화면을 만날 수 있다.

Style

webpack은 Stylesheet도 포함해 빌드 할 수 있다. 다만 사용하려면 style-loader, css-loader 같은 별도의 loader 플러그인을 설치해야 한다.

$ npm install --save style-loader css-loader

그리고 간단한 스타일을 작성한다.

/* style.css */
body {
font-size: 24px;
color: blue;
}

그 다음으로는 아까 만들었던 entry.js 파일에 지금 작성한 스타일을 포함시킨다.

// entry.js
require('!style!css!./style.css');
var hello = require('./hello');
var world = require('./world');
document.write(hello + ', ' + world + '!');

require() 안에 들어가는 문자열이 다소 복잡한데, 파일을 로드할 로더를 설정해줘야 하기 때문이다. 아무 설정도 하지 않으면 오류가 발생한다. 덧붙여 자바스크립트 같은 경우는 확장자를 생략할 수 있지만 그 외의 파일은 확장자를 생략하면 찾지 못한다.

마지막으로 다시 빌드하고 나서 브라우저에서 다시 index.html을 열어보면 색깔과 크기가 바뀐 걸 확인할 수 있다.

여기서 끝내면 살짝 아쉬우므로 서드파티 플러그인 sass-loader까지 사용해보겠다.

$ npm install --save node-sass sass-loader

css 파일을 sass로 컨버팅한다.

// style.sass
body
font-size: 24px
color: blue

entry.js 파일을 알맞게 수정한다.

// entry.js
require('!style!css!sass!./style.sass');
var hello = require('./hello');
var world = require('./world');
document.write(hello + ', ' + world + '!');

빌드가 정상적으로 되었다면 다시 index.html을 열어 확인해보자.

Config

그런데 매번 새로운 스타일시트가 추가될 때마다 require() 함수를 저렇게 복잡하게 사용하는 건 솔직히 좀 불편하다. 따라서 webpack.config.js라는 설정파일을 사용할 수 있다.

// webpack.config.js
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{ test: /\.sass$/, loader: 'style!css!sass' }
]
}
};

설명하지 않아도 직관적으로 이해하기 쉬운 설정파일이지만 간단하게 설명하면, entry 프로퍼티로 빌드되기 전의 파일 경로를 넣어주면 되고, output에는 bundle로 만들어질 파일의 정보를 명시해주면 된다.

별도의 로더를 사용하면 module.loaders 프로퍼티에 나열해주면 된다. 각 로더는 testloader 프로퍼티를 가지는데 test는 대상이 되는 파일들이 매치될 수 있는 정규식을 넣어주고, loader에는 사용할 로더들의 이름을 명시해주면 된다.

이렇게 설정파일을 만들어주면 require()를 간단하게 할 수 있다.

// entry.js
require('./style.sass');
var hello = require('./hello');
var world = require('./world');
document.write(hello + ', ' + world + '!');

게다가 이제 커맨드라인 명령어도 간단하게 사용할 수 있다.

$ webpack

ES2015 with babel

최신 ECMAScript 스펙은 구현되지 않은 브라우저가 많기 때문에 프론트엔드에서 사용하기는 어려움이 따르는데 babel은 이렇게 최신 스펙은 사용하고 싶으면서도 구형 브라우저 지원도 포기하기 싫은 사람들을 위한 컴파일러이다. ES2015 이상의 문법을 사용해서 구현된 JavaScript 파일을 구형 브라우저도 지원하는 형태로 컴파일해준다. 특히 다른 컴파일러나 언어에 비해서 ES2015의 스펙을 충실히 구현하고 있다.

babel은 webpack용 플러그인 babel-loader를 제공하므로 webpack에서도 쉽게 사용할 수 있다.

먼저 npm을 통해 babel 관련 모듈을 설치한다.

$ npm install --save babel-loader babel-core babel-preset-es2015

설치가 되었으면 이 babel을 빌드과정에 포함시키기 위해 webpack.config.js를 다음과 같이 수정한다.

// webpack.config.js
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
loaders: [
{
test: /\.sass$/,
loader: 'style!css!sass'
}, {
test: /\.js$/,
loader: 'babel',
exclude: /(node_modules|bower_components)/,
query: {
presets: ['es2015']
}
}
]
}
};

이제 빌드과정에 babel을 통한 컴파일 과정이 포함되게 되었다.
다음으로 기존의 코드를 ES2015 스타일로 수정하자.

// hello.js
export default 'Hello';
// world.js
export default 'world';
// entry.js
import './style.sass';
import hello from './hello';
import world from './world';
document.write(`${hello}, ${world}!`);

코드를 수정하고 다시 빌드해보면 빌드시간이 다소 길어졌지만 ES2015로 작성한 코드가 정상적으로 동작하는 걸 확인할 수 있을 것이다.

Lint

기왕 ES2015 사용하는 거, Lint까지 제대로 해보자. Linter로는 ESLint를 사용할 것이다.

먼저 Lint 관련 모듈을 설치한다.

$ npm install --save eslint eslint-loader

설치가 완료되었으면 Lint를 위해서 .eslintrc.js 파일을 수동으로 생성한다. eslint 커맨드라인 도구를 이용해서 쉽게 생성할 수도 있지만 여기서 사용하는 코드와 다소 안 맞는 부분이 있어 그냥 새로 만드는 것을 추천한다.

// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
},
"parserOptions": {
"sourceType": "module"
}
};

이해하기가 어렵진 않지만 ESLint 설정 파일에 대한 설명을 짤막하게 하자면, 기본적으로 ESLint에서 추천하는 환경설정을 확장한 설정이며, 브라우저에서 동작하므로 browser 프로퍼티를 true로 주었다. 또한 모듈을 사용하고 있기 때문에 parserOptions.sourceType'module'로 지정했다. indent 프로퍼티의 경우 사용하는 에디터나 환경에 따라 값을 달리 주어야 할 것이다.

그 다음에는 webpack.config.js를 수정해서 Lint를 빌드하기 전에 수행하도록 한다.

// webpack.config.js
module.exports = {
entry: './entry.js',
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
preLoaders: [
{
test: /\.js$/,
loader: 'eslint',
exclude: /(node_modules|bower_components)/
}
],
loaders: [
{
test: /\.sass$/,
loader: 'style!css!sass'
}, {
test: /\.js$/,
loader: 'babel',
exclude: /(node_modules|bower_components)/,
query: {
presets: ['es2015']
}
}
]
}
};

전에 못 보던 preLoaders라는 프로퍼티에 eslint 로더가 추가된 것을 볼 수 있는데, preLoadersloaders 전에 실행되어야 하는 로더들을 선언하는 부분이다. babel-loader는 ES2015 코드를 컴파일 할 것이기 때문에 컴파일 하기 전에 Lint를 하기 위해서 preLoaders에 eslint를 추가한다.

이제 Lint가 제대로 되고 있는지 확인하기 위해서 규칙에 어긋나는 코드를 추가한다.

// entry.js
import './style.sass';
import hello from './hello';
import world from './world';
var foo = bar; // 에러가 발생할 것이다.
document.write(`${hello}, ${world}!`);

그리고 다시 빌드했을 때 다음과 같이 에러가 뜬다면 Linter가 잘 동작하고 있다는 것이다.

error 'foo' is defined but never used no-unused-vars
error 'bar' is not defined no-undef

외부 모듈 사용하기

webpack을 사용하면서 가지게 되는 장점 중 하나는 더 이상 bower를 통해 패키지 관리를 할 필요가 없다는 것이다. npm에 등록된 Node.js 모듈은 모두 webpack을 통해서 bundle에 포함할 수 있다.

여기서 사용할 외부 모듈은 underscore.js다. npm을 통해 설치하면 된다.

$ npm install --save underscore

모듈 import는 매우 간단하다. 원래 require()를 사용하던 것처럼 import ... from ... 구문을 사용하면 된다. import 한 뒤에는 잘 동작하는지 확인하기 위해서 다음과 같이 random() 메소드를 사용해보자.

// entry.js
import './style.sass';
import hello from './hello';
import world from './world';
import _ from 'underscore';
document.write(`${hello}, ${world}! `);
document.write(`Random: ${_.random(0, 100)}`);

다시 빌드한 뒤, 브라우저에서 실행시켜보면 새로고침 할 때마다 랜덤한 정수가 출력되는 것을 확인할 수 있다.

마치며

사실 webpack은 State of the Art JavaScript in 2016에서도 Build tool 부문에 선정된 바 있는 아주 핫한 도구다. 나의 경우엔 Redux를 공부하려고 했는데 Redux에서는 튜토리얼부터 모듈화를 적극적으로 활용하고 있기 때문에 webpack부터 공부하게 되었다. 직접 공부해보니 의존성 관리를 효과적으로 해결해 줄 수 있는 툴이라는 생각이 들었다. 최근에 회사에서 개발하고 있는 것도 그런 부분에서 어려움을 겪은 적이 있어서 더욱 와닿았다. 거기에 Stylesheet도 import가 가능하다니..! 다만 minimal한 Browserify와는 달리 많은 기능이 있으므로 러닝 커브가 다소 있다는 점이 아쉽지만, 그만큼 강력한 도구인 것은 분명하고 일단 배워두면 써먹을 수 있는 부분이 많을 것이라고 생각한다.

여기에 사용된 코드는 GitHub 레포지토리 webpack-guide에 올라가 있으니 참고하시기 바란다.

참고링크