Module Bundler에 대해 알아보고 실습해보겠습니다.
Module Bundlers
모듈 번들러는 일반 브라우저에서 사용하기 위해 자바스크립트와 해당 종속성을 단일 파일로 묶는 도구입니다.
일반적으로 하나의 entry 파일로 시작해 해당 파일에 필요한 모든 코드를 묶는(bundles up) 작업을 수행합니다.
번들러에는 두 가지 주요 단계가 있는데, 종속성 해결(dependency resolution)과 packing입니다.
app.js
와 같은 entry point에서 시작해 종속성 해결의 목표는 작동하는 데 필요한 다른 코드 조각들, 모든 종속성을 찾고 해당 그래프를 구성하는 것입니다.
대표적인 모듈 번들러로 Webpack
, Parcel
, Rollup
등이 있습니다.
의존성(Dependencies)
코드에서 두 모듈 간의 연결 혹은 두 클래스 간의 관계를 의미합니다.
다른 프로그래밍 언어의 개발처럼, 자바스크립트도 개발 중에 애플리케이션에 필요한 어떤 기능을 만들고, 그 기능들을 관리하기 쉽게 기능 별로 코드를 나누고 파일로 나눠 모듈로 만들게 됩니다. 그렇게 나눈 모듈들은 서로를 참조하고, 어떤 모듈은 다른 모듈을 참조하면서 해당 모듈도 다시 자신을 참조하기 때문에 관계가 복잡해집니다.
또, 모듈을 나누고 다시 합치는 과정에서 해당 모듈에서 사용했던 변수명이나 함수명 등이 겹치는 문제가 발생할 수도 있습니다.
의존성 혹은 종속성이라고 합니다.
모듈 번들러를 사용하는 이유
요즘 브라우저는 그렇지 않지만, 모듈 시스템을 지원하지 않던 브라우저를 위해 필요합니다.
코드의 종속성(dependency) 관계를 관리하는 데 도움이 되며, 종속성 순서대로 모듈을 로드합니다.
종속성 순서, 이미지, CSS 자산 등을 로드하는 데 도움이 됩니다.
예를 들어, 아래와 같이 여러 자바스크립트 파일로 구성된 웹 애플리케이션을 구축할 때, 각 파일에는 별도의 http 요청이 필요합니다.
1
2
3
4
5
6
7
<html>
<script src="/src/foo.js"></script>
<script src="/src/bar.js"></script>
<script src="/src/baz.js"></script>
<script src="/src/qux.js"></script>
<script src="/src/quux.js"></script>
</html>
그보다는 아래와 같이 5개의 파일을 결합해 하나의 파일을 한 번만 요청하는 것이 좋습니다.
1
2
3
<html>
<script src="/dist/bundle.js"></script>
</html>
여기서 dist/bundle.js
파일이 생성되는 과정에서 다음 문제가 발생합니다.
- 포함할 파일의 순서(종속성 순서)를 어떻게 유지하는가?
- 파일 간에 이름 충돌을 어떻게 방지하는가?
- 번들 내에서 사용하지 않은 파일을 어떻게 확인하는가?
위 문제들은 아래와 같은 각 파일 간의 관계를 알면 해결할 수 있습니다.
- 어떤 파일이 다른 파일에 의존하는가?
- 파일에서 노출되는 인터페이스는 무엇인가?
- 어떤 노출된 언터페이스가 사용되는가?
우리에게 필요한 것은 파일 간의 관계를 설명하는 선언적 방법이며, 이를 통해 자바스크립트 모듈이 탄생했습니다.
CommonJS 또는 ES Modules는 의존하는 파일과 파일에서 사용하는 인터페이스를 지정하는 방법을 제공합니다.
1
2
3
4
5
6
7
// CommonJS
const foo = require("./foo");
module.exports = bar;
// ES Modules
import foo from "./foo";
export default bar;
번들 방식
모듈 시스템에서 수집한 정보를 사용해 파일을 함께 연결하고 모든 것을 캡슐화하는 번들 파일을 생성하기 위한 여러 방법 중 두 가지 상반된 방식을 보겠습니다.
예시 파일 3가지가 있다고 가정하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// circle.js
const PI = 3.141;
export default function area(radius) {
return PI * radius * radius;
}
// square.js
export default function area(side) {
return side * side;
}
// app.js
import squareArea from './square';
import circleArea from './circle';
console.log('Area of square: ', squareArea(5));
console.log('Area of circle', circleArea(5));
Webpack의 방식
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
// webpack-bundle.js
const modules = {
"circle.js": function (exports, require) {
const PI = 3.141;
exports.default = function area(radius) {
return PI * radius * radius;
};
},
"square.js": function (exports, require) {
exports.default = function area(side) {
return side * side;
};
},
"app.js": function (exports, require) {
const squareArea = require("square.js").default;
const circleArea = require("circle.js").default;
console.log("Area of square: ", squareArea(5));
console.log("Area of circle", circleArea(5));
}
};
webpackStart({
modules,
entry: "app.js"
});
- 함수에 의해 래핑된 모듈 자체에 모듈 이름을 딕셔너리처럼 매핑합니다. 모듈 맵은 레지스트리와 같으며 항목을 추가하여 모듈을 쉽게 등록할 수 있습니다.
- 각 모듈은 함수로 래핑됩니다. 이 함수는 모듈 내 선언된 모든 것이 자체적으로 범위가 지정되는 모듈 범위를 시뮬레이트합니다. 함수 자체를 모듈 팩토리 함수라고 합니다. 작성된 코드 처럼 모듈이 인터페이스를 내보내고 다른 모듈에서 요구할 수 있도록 몇 가지 매개 변수를 사용합니다.
- 애플리케이션은 모든 것을 함께 붙이는
webpackStart
함수를 통해 시작합니다. 런타임이라고도 하는 이 함수는 번들의 가장 중요한 부분입니다. 모듈 맵과 엔트리 모듈을 사용해 애플리케이션을 시작합니다.
webpackStart
함수를 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack-bundle.js
function webpackStart({ modules, entry }) {
const moduleCache = {};
const require = (moduleName) => {
// if in cache, return the cached version
if (moduleCache[moduleName]) {
return moduleCache[moduleName];
}
const exports = {};
// this will prevent infinite "require" loop
// from circular dependencies
moduleCache[moduleName] = exports;
// "require"-ing the module,
// exported stuff will assigned to "exports"
modules[moduleName](exports, require);
return moduleCache[moduleName];
};
// start the program
require(entry);
}
webpackStart
함수는 require
함수와 모듈 캐시 두 가지를 정의합니다. require
는 CommonJS의 require와 다릅니다. require
는 모듈 이름을 취하고 모듈 내에서 내보낸 인터페이스를 반환합니다. 내보낸 인터페이스는 모듈 캐시에 캐시되므로 동일한 모듈 이름의 require
를 반복해서 호출하면 모듈 팩토리 함수가 한번만 실행됩니다. require
가 정의된 상태에서 애플리케이션을 시작하면 항목 모듈을 require
하는 것입니다.
Rollup의 방식
1
2
3
4
5
6
7
8
9
10
11
12
13
// rollup-bundle.js
const PI = 3.141;
function circle$area(radius) {
return PI * radius * radius;
}
function square$area(side) {
return side * side;
}
console.log("Area of square: ", square$area(5));
console.log("Area of circle", circle$area(5));
- Webpack 번들의 방식보다 훨씬 작습니다.
- 모듈 맵이 없습니다.
- 모든 모듈은 번들로 평평해집니다(flatten).
- 모듈의 함수 래핑이 없습니다.
- 모듈 내에서 선언된 모든 변수, 함수는 이제 전역 범위로 선언됩니다.
개별 모듈 범위에서 선언된 것들이 전역 범위로 선언된 경우, 2개의 모듈이 동일한 이름의 변수/함수를 선언할 수 있기 때문에, Rollup은 이름 충돌이 발생하지 않도록 변수/함수의 이름을 바꿉니다.
그리고 번들 내의 모듈의 순서가 중요합니다. 종속성 순서로 모듈을 정렬하는 것은 Rollup의 방식에서 중요합니다.
단점으로 이 방식은 때때로 순환(circular) 종속성에 잘 작동하지 않습니다.
참고
Let’s learn how module bundlers work and then write one ourselves