자바스크립트 프레임워크로 React를 사용하든 Vue를 사용하든 Angular를 사용하든 항상 웹팩(Webpack)이라는 녀셕을 만나게 됩니다. 보통 구글링해서 겨우 필요한 설정만 바꾸면서 사용하고 했었는데 이 번에 시간을 내서 웹팩에서 대해 한 번 공부를 해보았습니다.
웹팩(Webpack)은 기본적으로 자바스크립트 모듈 번들러(JavaScript Module Bundler)이며 웹 개발을 도와주는 엄청나게 많은 부가 기능을 가지고 있습니다. 간단한 예제 프로젝트를 통해서 웹팩의 필요성을 느껴보고 기본적인 사용법에 대해서 알아보도록 하겠습니다.
먼저 webpack-example 디렉터리를 만들고 그 안에서 예제 프로젝트를 시작해보겠습니다.
$ mkdir webpack-example
$ cd webpack-example
참고로 예제 프로젝트가 끝나게 되면 다음과 같은 디렉터리 구조를 가지게 될 것입니다.
webpack-example
|- /dist
|- main.js
|- /src
|- time.js
|- index.js
|- index.html
|- package.json
|- /node_modules
HTML 파일 작성하기
예제 프로젝트 안에 다음과 같이 간단한 index.html 파일을 작성합니다.
index.html
Webpack Example위 index.html 파일을 크롬이나 파이어폭스와 같은 모던 브라우져에서 열어보면 현재 시간을 H:m:s 포멧으로 1초 마다 업데이트 하면서 화면에 보여주게 됩니다. 참고로 이 코드는 const, =>, ${exp}과 같은 ES6(ES2015) 키워드를 사용했기 때문에 익스플로러와 같이 ES6 구문 해석을 지원하지 않는 브라우저에서는 정상적으로 작동하지 않을 수 있습니다.
HTML 태그와 JavaScript 코드를 하나의 파일에 섞어 놓는 것은 좋은 관행이 아니므로, index.html 파일에서 JavaScript 코드만 뽑아 내어 index.js 파일을 만들고 이 둘을
index.html
Webpack Example index.js function currentTime() { const date = new Date(); const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); return `${hours}:${minutes}:${seconds}`; }const div = document.createElement("div");
document.body.appendChild(div);
setInterval(() => (div.innerText = currentTime()), 1000);
Moment.js라는 유명한 자바스크립트 라이브러리를 사용해서 위 코드를 다시 작성해보겠습니다. index.html 파일에는 Moment.js의 소스 파일에 대한 CDN 링크가 추가되었고, index.js 파일의 currentTime() 함수는 Moment.js 패키지의 format() 함수를 호출하는 1줄 짜리 코드로 바뀌었습니다.
index.html
Webpack Example index.js function currentTime() { return moment().format("H:m:s"); }const div = document.createElement("div");
document.body.appendChild(div);
setInterval(() => (div.innerText = currentTime()), 1000);
이 것이 전통적으로 웹사이트에서 외부 자바스크립트 라이브러리를 사용하던 전형적인 방법이었습니다. 하지만 여기에는 여러가지 단점이 있습니다.
먼저 JavaScript 파일이 HTML 파일로 부터 완전히 독립할 수 없다는 것입니다. 여전히 HTML 파일에서 외부 자바스크립트 라이브러리 대한 의존성이 관리되고 있기 때문에, 정작 JavaScript 파일(index.js)은 HTML 파일(index.html) 없이는 정상적으로 작동할 수 없는 모순이 일어납니다.
또한 외부 자바스크립트 라이브러리에 들어있는 모든 변수, 함수, 객체들이 사용 여부에 관계 없이 글로벌 네임스페이스를 오염시키게 됩니다. 게다가 외부 라이브러리를 추가적으로 사용할 때 마다 JavaScript 파일이 아닌 HTML 파일을 수정해줘야 된다는 것도 마음에 들지 않습니다.
다른 프로그래밍 언어들처럼 웹에서도 자바스크립트를 통한 자체적인 외부 모듈 임포트 기능이 절실해지는 시점입니다.
좀 더 체계적으로 예제 프로젝트의 외부 라이브러리를 관리하기 위해서 Node.js 패키지 매니저인 NPM을 도입하도록 하겠습니다.
$ npm init -y
Wrote to C:\workspace\webpack-example\package.json:
{
"name": "webpack-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
그리고 Moment.js 패키지를 설치하도록 하겠습니다.
$ npm i -S moment
ES6는 다른 프로그래밍 언어처럼 하나의 자바스크립트 모듈에서 다른 자바스크립트 모듈을 사용할 수 있도록 import나 export와 키워드를 제공합니다.
먼저 index.html 파일에서 Moment.js의 소스 파일에 대한 CDN 링크를 제거합니다.
index.html
Webpack Example 그리고 index.js 파일 맨 위에 Moment.js 패키지를 임포트 해줍니다.index.js
import moment from "moment";
function currentTime() {
return moment().format("H:m:s");
}
const div = document.createElement("div");
document.body.appendChild(div);
setInterval(() => (div.innerText = currentTime()), 1000);
자, 이제 부푼 마음으로 크롬 브라우저에서 index.html 파일을 다시 열어보면, 다음과 같이 index.js 파일의 import 라인에서 구문 에러가 발생한다는 것을 알 수 있습니다.
이를 통해 일반 브라우저는 ES6의 import 키워드를 해석할 수 없다는 것을 알게되었습니다. 이제부터 웹팩(Webpack)을 통해 이 문제를 해결해보도록 하겠습니다.
예제 프로젝트에 webpack과 webpack-cli 패키지를 설치합니다. webpack은 웹팩의 핵심 패키지이며 webpack-cli는 터미널에서 webpack 커맨드를 실행할 수 있게 해주는 커맨드라인 도구입니다. 두 개의 패키지 모두 개발할 때만 필요한 의존성이기 때문에 -D 옵션을 사용하였습니다.
$ npm i -D webpack
프로젝트 디렉터리 안에 소스 디렉터리(src)와 배포 디렉터리(dist)를 생성합니다. 그리고 index.js 파일을 소스 디렉터리로 이동시킵니다.
$ mkdir src dist
$ mv index.js src/
그런 다음, 다음과 같이 npx webpack 커맨드를 실행시키면, 웹팩이 소스 디렉터리 안의 index.js 파일과 그 안에 임포트 되어 있던 Moment.js 패키지까지 읽어들여 하나의 파일 번들로 묶은 후 배포 디렉터리 안에 main.js 파일로 떨궈 줍니다. 우리는 이 과정을 흔히 번들링이라고도 합니다.
$ npx webpack
npx: installed 1 in 3.328s
Path must be a string. Received undefined
C:\workspace\webpack-example\node_modules\webpack\bin\webpack.js
Hash: f7dd33b9774ee4ae7dbe
Version: webpack 4.16.3
Time: 5633ms
Built at: 2018-07-31 12:00:21
Asset Size Chunks Chunk Names
main.js 225 KiB 0 [emitted] main
Entrypoint main = main.js
[124] ./src/index.js 227 bytes {0} [built][125] (webpack)/buildin/module.js 497 bytes {0} [built][126] ./node_modules/moment/locale sync ^.\/.*$ 2.91 KiB {0} [optional][built]
+ 124 hidden modules
실제로 dist/main.js 파일을 열어보면 웹팩이 소스 코드를 최적화(uglify/minify) 해놓았음을 알 수 있습니다. 브라우저가 해석해야 코드의 크기를 줄여주기 때문에 웹 애플리케이션의 성능이 좋아지며, 사람이 읽기 어렵기 때문에 약간의 보안적인 이점도 있습니다.
이제 index.html 파일이 우리가 작성한 원래 소스 파일이 아닌 웹팩이 만들어준 번들 파일을 참조하도록 수정합니다.
index.html
Webpack Example 다시 브라우져에서 index.html을 실행해보면 제대로 현재 시간이 표시되는 것을 확인할 수 있습니다. 이번에는 크롬 뿐만 아니라 익스플로러도 IE8 이상이라면 정상적으로 작동하게 됩니다. 왜냐하면 웹팩이 Babel을 이용하여 ES6 문법으로 작성된 코드를 ES5 문법으로 변환(transpile)해주기 때문에 main.js 파일은 ES6 문법을 지원하지 않은 브라우저도 해석할 수 있습니다.이제 ES6 모듈 키워드(import, export)를 사용할 수 있으니 비단 외부 모듈을 사용할 때 뿐만 아니라 프로젝트를 모듈화할 때도 웹팩을 활용할 수도 있을 것입니다. 본 예제 프로젝트에는 불필요한 작업이겠지만 큰 규모의 프로젝트의 경우 자바스크립트 코드를 모듈화하면 재활용성, 테스트성, 유지보수성이 향상됩니다.
먼저 소스 디렉터리(src) 안에 time.js라는 자바스크립트 모듈 파일을 만들고 currentTime() 함수를 index.js로 부터 옮겨옵니다. currentTime() 함수에서 Moment.js를 사용하기 때문에 임포트하는 부분도 함께 옮겨와야 합니다. export 키워드를 사용해서 옮겨온 currentTime() 함수를 외부에 노출시킵니다.
time.js
import moment from "moment";
export function currentTime() {
return moment().format("H:m:s");
}
index.js 파일에는 제거된 코드를 대신해서 time.js 모듈을 임포트하는 코드를 추가합니다.
index.js
import { currentTime } from "./time";
const div = document.createElement("div");
document.body.appendChild(div);
setInterval(() => (div.innerText = currentTime()), 1000);
다시 npx webpack 커맨드를 실행해서 새로운 번들 파일을 만든 후 브라우저에서 index.html 파일을 실행해보면 동일하게 현재 시간이 표시되게 됩니다.
소드 코드의 모듈 간의 의존성을 정리해보면 다음과 같으며, Webpack은 이러한 모듈간의 의존 관계를 트리로 구성하여 하나의 번들 파일로 제공하게 됩니다. 따라서 HTML 파일은 이 최종 번든 파일만을 참조할 수 있으며 이 것이 바로 웹팩이 우리에게 가져다주는 힘입니다.
index.js (최상위 내부 모듈) -> time.js (내부 모듈) -> moment.js (외부 모듈)