모듈 번들러가 필요한 이유와 Webpack의 번들링 방식

JavaScript 프레임워크인 React, Vue, Svelte, Angular 등을 사용하다 보면 모듈 번들러(module bundler)는 필수적으로 언급되는 내용이다. 모듈 번들러는 JavaScript 모듈을 브라우저에서 실행할 수 있는 단일 JavaScript 파일로 묶는 데 사용되는 도구다.
모듈(Module)
애플리케이션의 기능이 많아질수록 작성해야하는 코드가 길어질 수 있다. 모든 코드를 한 개의 파일에 작성하는 대신 기능(변수, 객체, 배열, 함수, 클래스 등)에 따라 별도의 파일에 코드를 분리할 수 있다. 코드를 분리하는 기준에 따라 파일이 상당히 많아질 수 있지만, 한 개의 파일에 코드를 작성하는 것보다 체계적이고 유지보수가 쉬워진다.
결국 모듈은 분리된 파일을 의미한다. JavaScript에서 현재 모듈을 다른 모듈에서 접근할 수 있도록 export 키워드를 사용하며, 분리된 모듈을 불러오기 위해 import 키워드를 사용한다.
모듈 번들러의 필요성
React, Angular, Vue와 같은 JavaScript 프레임워크가 등장하기 전에는 웹 사이트를 구축하기 위해 HTML, CSS, JavaScript 뿐이었기 때문에 파일 관리가 복잡하지 않았다. 하지만, 시간이 지나면서 조금 더 나은 성능, 속도를 만족하기 위해 다양한 JavaScript 프레임워크가 등장하였으며, 웹 사이트를 구축하기 위해 필요한 파일들이 점점 많아진다.
이렇게 많은 모듈을 하나로 묶는 과정은 쉽지 않으며, 변수 또는 함수 이름이 중복되는 경우 그리고 모듈간 종속성 때문에 배포하기 전부터 수많은 문제가 발생한다.
모듈간 종속성이란 예를 들어 설명하자면, A.js 파일은 B.js를 import 하고 B.js 파일은 C.js 파일을 import 하고.. 이렇게 모듈끼리 서로 종속되는 것을 의미하며, 모듈 간 종속성이 복잡한 경우 어떤 모듈에서 문제가 발생했는지 추적하는 것도 쉽지 않다.
하나의 파일로 병합하더라도 병합하는 과정에서 발생하는 문제를 해결하는데 상당히 많은 시간을 소모하게 되므로 상당히 비효율적이다.
모듈 번들러(module bundler)
모듈 번들러는 위에서 언급한 모든 문제를 해결하여 짧은 시간에 최상의 성능을 위해 애플리케이션을 최적화하는 개발 도구다. 모듈 번들러의 핵심 작업은 여러 JS 파일을 하나의 결합하여 단일 파일을 만드는 것이다. 따라서, Chrome과 같은 브라우저는 하나의 단일 파일을 로드함으로써 애플리케이션이 동작한다.
모듈 번들러는 JS 파일뿐만 아니라 CSS 파일과, 이미지, 글꼴 등 여러 리소스에 대해서도 번들링 된다. 모듈 번들러는 웹팩, 롤업처럼 다양한 개발 도구가 존재하며 번들링 동작 방식은 개발 도구마다 다르다.
웹팩의 번들링 방식
3개의 파일 circle.js, square.js 및 app.js가 있다고 가정해 보겠다.
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-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'
});
가장 먼저 눈에 띄는 것은
“모듈 맵”
이다.
- 함수에 의해 래핑된 모듈 자체에 모듈 이름을 매핑하는 딕셔너리다.
- “모듈 맵”은 레지스트리와 같으며 항목을 추가하여 모듈을 쉽게 등록할 수 있다.
둘째, 각 모듈은 함수로 래핑된다.
- 이 함수는 모듈 스코프를 시뮬레이션 한다
- 함수 자체를 “모듈 팩토리 함수”라고 한다.
- 모듈이 인터페이스를 export 하고 다른 모듈에서 require할 수 있도록 몇 가지 매개 변수를 사용한다.
셋째, 응용 프로그램은 모든 것을 함께 붙이는 함수인
webpackStart
를 통해 시작된다.
- 종종 “런타임“이라고 하는 함수이며, 번들의 가장 중요한 부분이다.
- “모듈 맵”과 엔트리 모듈을 사용하여 애플리케이션을 시작한다.
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”는 모듈 이름을 가져오고 모듈에서 내보낸 인터페이스를 반환한다.
- 예: circle.js의 경우 { default: function area(radius){ … } }이다.
- 내보낸 인터페이스는 모듈 캐시에 캐시된다.
- 동일한 모듈 이름의 “require”를 반복해서 호출하면 “모듈 팩토리 함수”가 한 번만 실행된다.
- “require”가 정의된 상태에서 응용 프로그램을 시작하면 엔트리 모듈을 “require”한다.

다음 글은 로더랑 플러그인에 대해 써야겠다
정리
- 모듈은 특정 목적 또는 기능에 따라 분리된 파일을 의미한다.
- 모듈 번들러는 분리된 파일을 하나의 파일로 병합하는 개발 도구다.
- 웹팩 방식:
- 모듈 맵 사용 함수를 사용
- 각 모듈을 함수로 래핑
- 모듈을 함께 붙이는 런타임 코드가 있음
