이 포스트에서는 Node.js, npm, ES(ECMAScript), Babel 등등에 대해서는 설명하지 않는다. 해당 내용들은 구글링을 통해 직접 찾아보길 바란다.
이번 포스트에서는 모듈이 왜 필요한지, 무엇인지, 번들링이란 무엇인지 등등에 대해 다룬다. 또한 홈페이지나 SPA가 여러 개 있는 다중 페이지, IE8에서도 모듈 번들링을 하는 방법까지 다뤄보자.
모듈, 너는 누구니?
ES5(3)를 공부해본 사람이라면 자바스크립트의 스코프 관리는 지저분하다는 것을 알 수 있다. 이를 해결하고자 아래와 같은 방법들이 있다. private module을 구현하는 코드 (전역의 공간을 더럽히지 않는 코드) 대신 script 모듈에서 script2 모듈에 있는 데이터를 불러올 수 없다.
public module을 구현하는 코드 script 모듈에서 script2 모듈에 있는 데이터를 불러올 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// ES3+ (IE 8+), 전역 스코프에 변수가 추가된다는 단점이 존재한다. // 네임스페이스 패턴이라고 부른다. // script.js 'use strict'; var car = (function() { var namespace = {}; var color = 'blue'; // 외부에 노출시키지 않음, 캡슐화. // 아래 메소드는 네임스페이스의 메소드임. namespace.getColor = function() { return color; }; namespace.setColor = function(_color) { color = _color; }; // 메소드를 달고 있는 네임스페이스 객체 리턴. return namespace; }());
1 2 3 4 5 6
// ES3+ (IE 8+), 전역 스코프에 car라는 변수가 추가된다는 단점이 존재한다. // script2.js 'use strict'; console.log(car.getColor()); // 'blue'; car.setColor('red'); console.log(car.getColor()); // 'red';
1 2 3 4 5 6
// ES6 (현재 지원하는 브라우저 없음) // 이 방법이 베스트인데, 크롬마저도 미지원이다. // script.js let color = 'blue'; exportconstgetColor = () => color; exportconstsetColor = (_color) => color = _color;
1 2 3 4 5 6 7
// ES6 (현재 지원하는 브라우저 없음) // 이 방법이 베스트인데, 크롬마저도 미지원이다. // script2.js import {getColor, setColor} from'./script'; console.log(getColor()); setColor('red'); console.log(getColor());
이렇 듯 모듈은 변수의 스코프를 관리하는 기능을 한다. 물론 이게 모듈의 전부는 아니지만, 그건 이 포스트의 주제를 벗어나므로 설명하지 않겠다. 브라우저에서 ES6의 모듈이 지원되지는 않지만 모듈을 사용하는 두 가지 방법이 존재한다.
자바스크립트 파일/모듈 로더인 requireJS를 사용.
모듈 번들러인 Webpack이나 Browserify 등등을 사용.
이 중에서 나는 2번의 Webpack을 택했다. 참고로 번들러는 번들링하는 놈을 지칭하고, 번들링은 여러가지 파일을 모아서 하나로 만드는 것이라고 보면 된다. 여러 모듈을 하나로 합쳐서 http 리퀘스트 횟수를 줄여서 퍼포먼스를 향상시키는 등등의 효과를 불러일으킬 수 있다.
웹팩을 사용해보자
터미널 창에서 아래와 같이 입력해보자.
1 2
$ npm i -g webpack $ npm i -D webpack babel-core babel-preset-latest babel-loader
위 명령어들을 하나 하나 헤짚어 보자.
npm i -g webpack 터미널에서 webpack 명령어를 사용하기 위해 글로벌로 웹팩을 설치
npm i -D webpack 현재 프로젝트에서 웹팩을 사용하기 위해 설치
babel-core requireJS 문법을 이용해도 모듈 번들링을 할 수 있지만, ES6의 import, export를 사용해보기 위해 babel을 사용하였다.
babel-preset-latest babel에는 plugin이라는 게 존재한다. 이 plugin은 es6의 애로우 펑션을 지원하는 플러그인, 클래스를 지원하는 플러그인 등등이 있다. 그러한 플러그인을 모아놓은 걸 preset이라고 부른다. es2015 preset은 es6의 플러그인들을 모아놓은 것이고, latest preset은 ES2015~ES2017까지의 프리셋들을 모아놓은 것이다. 시간이 지나면 latest의 지원 프리셋 범위는 더 늘어날 수도 있다.
// module-a.js const a = '나는 a'; exportconst ab = a + ', a를 외부에 노출시키지 않고 변수 a를 활용!';
1 2
// module-b.js exportconst a = '모듈 a에 존재하는 변수 a와는 다른 스코프를 가짐';
1 2
// module-c.js exportconst b = '나도 써주랑!';
1 2 3 4 5 6
// entry-index.js import {ab} from'./module-a'; import {a} from'./module-b'; import {b} from'./module-c'; console.log(ab); // "나는 a, a를 외부에 노출시키지 않고 변수 a를 활용!" console.log(a); // "모듈 a에 존재하는 변수 a와는 다른 스코프를 가짐"
[Webpack 적용기 2 : 어떻게 사용하는가?](https://hjlog.me/post/118)에서는 다음과 같이 설명하고 있다.
> 번들링의 진입점에 해당하는 entry point에서부터 require으로 명시된 의존성들을 해석하며 의존성 트리(dependency tree)를 그린다.
output 결과물이 어느 폴더, 어떤 파일명으로 저장될지 정하는 옵션이다.
plugins js 난독화 플러그인, 번들 파일을 html에 자동으로 삽입해주는 플러그인 등등 종류가 많다.
이 때 require된 모듈들은 불러들어지는 과정에서 파이프라이닝 된 일련의 로더 들을 거치게 된다. 로더를 하나의 정해진 역할을 수행하는, 일종의 함수라고 생각할 수 있다. 로더는 직전 단계의 모듈을 입력으로 받아, 다양한 변형을 가한 뒤 다음 로더의 입력으로 넘겨준다. 마지막 로더는 최종적으로 적절하게 변형된 모듈을 번들 자바스크립트 파일에 넣어주게 된다.
그럼 실제로 번들링을 해보자.
1
$ webpack -w
위 명령어를 실행하면 모듈들이 번들링되며 엔트리 포인트와 엔트리 포인트에 관련된 모듈들이 변경될 때마다 다시 번들링되는 감시(watch)를 진행하게 된다. Ctrl+C 키를 누르면 빠져나올 수 있다.
그리고나서 다시 디렉토리를 보면 다음과 같은 파일이 생긴 것을 볼 수 있다.
bundle.js
bundle.js.map
한번 index.html 파일을 열어보고 콘솔창을 보자.
디버깅하여 모듈은 어떠한 스코프를 가지는지 알아보자.
6번 라인에 브레이크 포인트를 걸고 새로고침을 해보았다. module-c는 import 시켰지만 모듈의 변수를 사용하지 않았으므로 불필요하게 스코프를 생성하지 않았다.
만약 모듈화가 브라우저 자체 내에서 지원된다면 index.html은 아래와 같이 마크업해야하지 않을까 싶다.
웹팩의 모듈 번들링 방식은 bundle.js 파일 하나만 요청해서 리퀘스트 횟수가 1회였는데, 저렇게 의존하는 모듈이 많으면 많을 수록 리퀘스트 횟수가 증가하여 큰 비용을 지불하게 될 것이다. 위와 같이 웹팩을 사용하면 큰 효과를 얻어낼 수 있다.
웹팩에게 모듈이란…?
ES6의 관점에서 모듈과 웹팩의 관점에서 모듈은 다르다. ES6는 js 파일만 모듈이라고 한정하는데 반해 웹팩은 이미지, css 파일 등등도 모듈로 취급한다. js 모듈을 import 시키는 것은 따로 로더를 요구하지 않지만, 다른 모듈들은 로더를 필요로 한다. 다음 예제를 통해 css, scss 모듈을 import 시켜보자. 또한 번들링 된 모듈(bundle.js)을 자동으로 추가시켜보자. 위 디렉토리에서 아래 파일들을 추가하자.
<!doctype html> <htmllang="ko"> <head> <metacharset="utf-8" /> <title>Title</title> </head> <body> <h1>나는 문서의 제목이얌! 내가 짱이지!</h1> <p>IE11+에서 나는 드래그 안 될 걸?!</p> </body> </html>
이제 entry-index.js에 (s)css 모듈을 추가해보자.
1 2 3 4 5 6 7
import'../styles/style.css'; import'../styles/style.scss'; import {ab} from'./module-a'; import {a} from'./module-b'; import {b} from'./module-c'; console.log(ab); // "나는 a, a를 외부에 노출시키지 않고 변수 a를 활용!" console.log(a); // "모듈 a에 존재하는 변수 a와는 다른 스코프를 가짐"
html webpack plugin html에 번들링 한 파일을 자동으로 삽입해준다. tmplate에 ejs 등등의 html 템플릿 엔진으로 작성한 걸 넣어주고, filename에 그 템플릿을 토대로 새롭게 만들어질 html 파일을 지정해주면 된다. 자세한 내용은 플러그인 api를 참조하길 바란다.
css-loader, sass-loader test에는 정규표현식이 들어가고, 로더의 순서는 바뀌면 오류가 난다. 아마 오른쪽에서부터 왼쪽으로 적용이 되는 게 아닐까 싶다. 또한 소스맵 옵션을 제거하면 아래와 같이 난독화된 소스를 보게돼 디버깅하기가 쉽지 않다.
개발용? 배포용?
뭔가 이제 그럴듯 하게 보이긴 하지만 현재 프로젝트 디렉토리를 보자.
node_modules - 패키지들이 설치된 폴더로 개발할 때 수정할 일이 없는 파일.
.babelrc - 바벨의 설정파일로서 개발할 때 수정할 일이 없는 파일.
bundle.js - 개발할 때는 필요없는 배포용 파일
bundle.js.map - 실제 사용자가 디버깅 할 필요는 없으므로 이 파일도 개발용 파일.
entry-index.js - 개발용 파일
index.html - 템플릿이 되는 파일이므로 개발용 파일
index2.html - 템플릿을 토대로 만들어진 배포용 파일
module-a.js - bundle.js에 번들링된 내용이므로 실제 배포할 때 필요없는 개발용 파일
module-b.js - bundle.js에 번들링된 내용이므로 실제 배포할 때 필요없는 개발용 파일
module-c.js - bundle.js에 번들링된 내용이므로 실제 배포할 때 필요없는 개발용 파일
package.json - 패키지들의 의존성을 도와주는 파일로서 바벨의 설정파일로서 개발할 때 수정할 일이 없는 파일.
style.css - bundle.js에 번들링된 내용이므로 실제 배포할 때 필요없는 개발용 파일
styles.css - bundle.js에 번들링된 내용이므로 실제 배포할 때 필요없는 개발용 파일
webpack.config.js - 번들을 하기 위한 설정 파일이므로 개발할 때 수정할 일이 없는 파일.
개발용 파일과 배포용 파일이 너무 난잡하게 섞여있다. 이는 나중에 개발 & 배포를 할 때 상당한 혼란을 초래한다.
또한 스타일 시트를 bundle.js 안에 번들링하면 아래와 같은 현상이 발생한다.
위 현상은 스타일 시트의 규모가 커질 수록 스타일이 적용되지 않은 모습이 노출되는 시간이 길어진다. 이를 해결하고자 css 파일에 소스맵을 안 붙여서 내부 스타일 시트로 배포하는 수가 있지만, bundle.js의 몸뚱아리만 키워 로딩속도를 저하시키는 요인이 되기도 한다. http 리퀘스트 횟수를 줄이라고는 하지만 기본적으로 http 리퀘스트는 4개의 병렬로 처리된다. 따라서 몸뚱아리만 큰 bundle.js를 로딩시키는 것보다 css 파일을 따로 빼서 bundle.js와 병렬로 로딩시키게끔 처리하는 게 훨씬 효율적이다.
우선 개발과 상관없는 설정 파일들은 루트 디렉토리로 빼버렸다. 그리고 개발에 집중하고자 개발용 파일들을 src 폴더에 체계적으로 분류하였다. 또한 웹팩의 설정 파일을 개발용과 배포용으로 나눴는데 이유는 다음과 같다.
개발용 파일은 디버깅이 주 목적이라 소스맵이 필요하다. 또한 난독화시키는 것은 번들링 타임을 증가시키는 주범이므로 뺀다. 그리고 HTML 파일을 핫리로드하게 만들어야 한다.
핫리로드란 서버의 재시작 없이 내용이 재교체되는 것을 뜻한다.
또한 브라우저에서 자동으로 새로고침이 이루어진다.
그리고 스타일 시트를 외부로 빼면 HMR을 이용할 수 없으므로 따로 빼지 않는다.
> HMR(Hot Module Replacement)이란 서버의 재시작 없이,
브라우저가 새로고침하지 않고, 수정된 부분만 바꾸는 것을 의미한다.
배포용 파일은 실 사용이 주 목적이라 용량을 경량화 시킬 난독화 작업이 진행되고, 디버깅 할 필요가 없으므로 소스맵도 붙이지 않고, 또한 HTML 파일은 핫리로드하게 만들 필요가 없고, HMR을 사용할 필요가 없으므로 스타일 시트를 외부로 뺀다.
이를 위해서 추가로 패키지를 설치할 필요가 있다.
1 2
$ npm i -g webpack-dev-server $ npm i -D webpack-dev-server raw-loader webpack-browser-plugin extract-text-webpack-plugin webpack-strip clean-webpack-plugin
webpack-dev-server -g로 설치하는 이유는 해당 명령어를 터미널에서 쓰기 위함이고 다시 한 번 -D로 설치하는 이유는 현재 프로젝트에서 해당 패키지를 쓰기 위함이다. webpack-dev-server는 실제 눈에 보이지 않는 디렉토리를 만들고 그 디렉토리에 번들링을 진행하고 watch하며 테스트를 하는 웹팩 개발용 서버이다.
raw-loader는 html 파일을 핫리로드하게 만드는 로더이다.
webpack-browser-plugin webpack-dev-server에서 번들링을 끝낸 후 자동으로 브라우저를 열어주는 플러그인이다. 자세한 옵션은 webpack-browser-plugin을 확인하자.
extract-text-webpack-plugin 스타일 시트를 따로 빼기 위한 플러그인이다.
webpack-strip js 파일에서 디버깅을 위해 찍어본 로그를 삭제해준다.
clean-webpack-plugin 배포용 파일을 빌드하기 전에 배포용 디렉토리를 지워주는 플러그인이다.
new CleanWebpackPlugin([‘dist’]) 빌드를 시작하기 전에 먼저 배포용 디렉토리를 지워줘야한다.
new webpack.DefinePlugin() process.env.NODE_ENV는 개발환경인지 배포환경인지 알고자 할 때 쓰인다. production이면 배포 모드, development이면 개발환경이다. 이는 HTML을 핫리로드하게 만들지 안 만들지를 결정하기 위해 썼다.
new webpack.optimize.OccurrenceOrderPlugin()
모듈을 할당하고 발생 카운트 아이디들을 발생(?chunk)시킨다. ID들은 종종 적은(짧은) id들을 얻는데 사용된다. 이것은 id가 예상가능하며 파일 전체 크기를 경감시켜 추천한다.
new HtmlWebpackPlugin({ minify: {}}) html 난독화 옵션을 참고하자.
new ExtractTextPlugin(‘styles/bundle.css’) 번들링한 스타일 시트 파일을 어디에다 추출할지 정해주는 플러그인.
‘webpack-strip?strip[]=debug,strip[]=console.log,strip[]=console.dir’ webpack-strip 로더를 사용하여 디버깅을 위해 로그에 찍었던 로그를 삭제했다.
ExtractTextPlugin.extract() 해당 스타일 시트를 css파일로 뽑아내는 로더이다.
또한 html을 핫리로드하게 만들어주려면 엔트리에 html 모듈을 추가해야한다. 하지만 배포용에서는 핫리로드하게 만들어줄 필요가 없기 때에 조건문을 쓰면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// entry-index.js if (process.env.NODE_ENV !== 'production') { require('../index.html') }
head에 폴리필을 넣어줬다. 현재는 존재하지 않지만 번들링을 통해 만들 예정이다. html webpack plugin이 head와 body 동시에 다른 파일 삽입이 불가능해서 부득이하게 이러한 방법을 사용했다. 또한 babel-polyfill을 minify하게 되면 IE8에서 오류가 나게 된다. 따라서 부득이하게 cdn을 이용하였다.