차세대 JS모듈 번들러 - RollupJS 소개

번들러(Bundler)

소프트웨어를 개발할때, 라이브러리나 어플리케이션을 작은 조각으로 세분화하고 나누어 작업하는것은 일반화되어 있다. 써드파티 라이브러리를 사용할때는 더욱 그러하다. 하지만, 그 결과로 무수하게 작은 파일들이 생겨나고 이것을 결코 브라우저에게 좋은 소식이 아니다. 결과적으로, 브라우저는 매번 요청을 생성하고 속도는 느려지기 때문이다.

해결책은 코드를 모듈화하고, 모듈 번들러를 이용해 모든것을 하나의 파일로 만드는것이다. BrowserifyWebpack이 대표적인 예이다.

하지만, 기존의 라이브러리를 번들화 한다고 하여 그 크기가 줄어드는것은 아니다.

1
2
3
4
var utils = require( 'utils' );
var query = 'Rollup';
utils.ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

결국, 실제로 사용하지 않는 utils라이브러리를 모두 다운받게 된다.

ES6 모듈은 이 문제를 해결하였다. 다음과 같은 방법으로 utils를 모두 포함하지 않고, 필요한 ajax함수만 가져온다.

1
2
3
4
import { ajax } from 'utils';
var query = 'Rollup';
ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

Rollup은 코드와 종속관계의 라이브러리를 정적으로 분석하고, 최소한의것들만 번들링한다.

rollupjs

Minifier의 한계

만일, UglifyJS와 같은 minifier를 사용하여도, 불필요한 코드를 삭제하긴 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function () {
function foo () {
console.log( 'this function was included!' );
}
function bar () {
console.log( 'this function was not' );
baz();
}
function baz () {
console.log( 'neither was this' );
}
foo();
})();

minifier는 foo는 호출되지만, bar는 사용되지 않으므로 삭제한다. baz도 마챦가지 이다. 하지만, 정적인 코드분석의 한계로 인해 동적인 형태의 소스코드에 대해서는 무용지물이 된다. 다음과 같은 코드에 대해서는 동일한 작업을 수행하지 못한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function () {
var obj = {
foo: function () {
console.log( 'this method was included!' );
},
bar: function () {
console.log( 'so was this :-(' );
this.baz();
},
baz: function () {
console.log( 'and this :-(' );
}
};
obj.foo();
})();

불행히도, 기존의 모듈들(CommonJS, AMD)은 두번째 형태의 코드를 생산해내므로 최적화가 어렵다. 그래서, 사용되지 않는 코드를 배제하기보다, 사용되는 코드를 포함시키는 방향으로 최적화를 하는것이 낫다. ES6 모듈과 같은 방식으로 한다면 가능하다.

Rollup과 JSPM, ES6

이러한 방식을 Tree-shaking이라고 부른다. Rollup은 플러그인을 이용해 CommonJS모들을 ES6모듈로 변환하고 번들링한다. 또한, package.json에 jsnext:main 필드를 추가하여 CommonJS또는 UMD버전 대신 ES6버전의 패키지를 만들수도 있다. rollup-starter-project는 훌륭한 지침이 될것이다.

JSPM은 뛰어난 패키지 매니저이다. 또한, 이것은 내부적으로 Rollup을 이용해 번들링을 하고 있다. JSPM은 어떤 모듈포맷이든지 지원하며 빌드과정도 필요없으므로 어플리케이션을 개발하는데 있어 훌륭한 선택이될 수 있다. 하지만, Rollup은 복잡한 SystemJS포맷을 사용하지 않으므로 라이브러리를 만드는데 있어서는 Rollup을 단독으로 사용하는것이 더 나은 선택일 수 있다.

HOW-TO

Rollup은 Javascript API 또는 CLI를 이용할 수 있다.

1
npm install -g rollup

Javascript API

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
var rollup = require( 'rollup' );
// used to track the cache for subsequent bundles
var cache;
rollup.rollup({
// The bundle's starting point. This file will be
// included, along with the minimum necessary code
// from its dependencies
entry: 'main.js',
// If you have a bundle you want to re-use (e.g., when using a watcher to rebuild as files change),
// you can tell rollup use a previous bundle as its starting point.
// This is entirely optional!
cache: cache
}).then( function ( bundle ) {
// Generate bundle + sourcemap
var result = bundle.generate({
// output format - 'amd', 'cjs', 'es', 'iife', 'umd'
format: 'cjs'
});
// Cache our bundle for later use (optional)
cache = bundle;
fs.writeFileSync( 'bundle.js', result.code );
// Alternatively, let Rollup do it for you
// (this returns a promise). This is much
// easier if you're generating a sourcemap
bundle.write({
format: 'cjs',
dest: 'bundle.js'
});
});

entry가 최소한의 옵션이다. rollup은 bundle과 함께 프로미스를 반환하고, 결과를 처리하면 된다.

CLI

rollup -h로 사용법을 참고할 수 있다. 대부분의 경우 CLI를 이용하는것이 편리할 것이다. rollup CLI는 -c(–config)옵션으로 설정파일을 지정하여(기본값은 rollup.config.js) 빌드를 수행할 수 있으므로 gulp와 함께 사용하면 편리하다.

1
rollup -c

설정파일 예제

1
2
3
4
5
6
7
8
9
// rollup.config.js
import buble from 'rollup-plugin-buble';
export default {
entry: 'src/main.js',
dest: 'dist/bundle.js',
format: 'umd',
plugins: [ buble() ]
};

Rollup은 이미 webpack이나 browserify 못지않게 많은 플러그인들이 존재한다. 플러그인을 활용해 번들링전에 코드를 트랜스파일링 하거나, 써드파티 모듈을 찾는데 사용할 수 있다.

rollup은 es6개발 환경에서 최선의 모듈 번들러가 될것이다. 그리고, 이것을 잘 사용하려면 적절한 플러그인을 사용하여 빌드환경을 구성하는것이 관건이 될것이다.