Last Updated: 2023-12-19

module-system-cover

22년도에 vite에 관련된 세미나를 진행할 때 vite를 설명하기 전에 Module System과 Bundler를 먼저 설명하였다. 이유는 vite 역시 번들러에 속하며, 번들러가 왜 계속 발전해 오고 있는지 설명하고 vite에 대한 이해를 돕기 위해서이다. 처음 JavaScript 모듈 시스템과 번들러를 설명하고 이어 vite를 설명하려하였지만 이를 두 파트로 나누어 포스팅하려고 한다.

이 포스트에서는 JavaScript 모듈 방식인 CommonJSAMD, UMD, ESM에 대해 설명하고, Module Bundler에 대해 살펴보려고 한다.

Module System

Module System은 HTML에 JavaScript 원본 소스를 제공하여 순차적으로 로드하는 방식

과거 웹 서비스라 하면 정보를 제공하는 데 그쳤기 때문에 지금처럼 복잡하지도 않았으며 규모 역시 크지도 않았다. 웹 서비스를 구성하는 HTML이나 JavaScript 파일의 크기도 상대적으로 작았기 때문에 서비스를 유지하는 데 큰 무리가 없었다. 하지만 점차 웹 서비스의 규모가 커지고 V8 엔진의 등장으로 다방면으로 JavaScript 활용도도 증가하면서 JavaScript 파일도 많아졌다. 파일의 구조가 복잡해지고 거대해지면서 순차적으로 로드하는 방식은 각 모듈 간 scope가 구분되지 않기 때문에 다른 파일을 오염시키거나 충돌이 일어나는 경우가 발생했다. 또한 외부 라이브러리가 증가하면서 애플리케이션에 사용하는 모듈의 개수도 증가하게 되었다.

이렇게 규모와 개발되는 파일의 개수, 그리고 모듈이 증가함으로 인해 모듈화에 대한 필요성이 부각되면서 CommonJSAMD 가 등장하게 되었다.

순차적 모듈의 예시

<html>
<script src="src/A.js"></script>
<script src="src/B.js"></script>
<script src="src/C.js"></script>
<script src="src/D.js"></script>
</html>

CJS (CommonJS)

JavaScript를 브라우저에서뿐만 아니라 범용 언어 사용을 목적으로 둔 워킹 그룹

동기적 방식을 사용하고 모든 종속성이 로컬 디스크에 존재하여 바로 사용할 수 있는 환경을 전제로 한다. 과거에 모듈 시스템은 반드시 필요했기 때문에 Node.js는 Common JS 방식의 명세를 채택하고 NPM과 함께 큰 성장을 하였다. 우리가 흔하게 쓰는 방식으로 대표적으로 require를 말할 수 있다.

동기적 방식은 브라우저에서 필요로 하는 모듈이 모두 다운로드 할 때까지 기다려야 하는 단점과 같은 비동기적 로드를 고려하지 않은 설계로 인해 브라우저에서의 사용에는 한계가 있기 때문에 Node.js의 서버 사이드 환경에서 용이하다.

CJS 방식의 예

//importing 
const doSomething = require('./doSomething.js')

//exporting
module.exports = function doSomething (n) {
// do something
}

AMD (Asynchronous Module Definition)

JavaScript를 비동기적으로 사용하기 위해 CommonJS와 독립한 별도의 그룹

CJS와 다르게 비동기적으로 모듈을 호출하며, 브라우저에서의 모듈 실행을 우선으로 한다. 비동기적 방식으로 CJS보다 나은 성능을 보였으며, 브라우저/서버사이드 모두 호환되는 방식이지만 CJS보다 직관적이지 않은 단점이 있다.

AMD 방식의 예

define([
'package/lib_1',
'package/lib_2'
], function (pack_1, pack_2) {
function foo () {
pack_1.log('Package 1')
}

function bar () {
pack_2.log('Package 2')
}

return { foo, bar }
})
require([
'my_package'
], function (my_pack) {
my_pack.foo()
})

UMD (Universal Module Definition)

CJS와 AMD 모두 호환되면서 범용적으로 여러 모듈을 구성하는 디자인 패턴

CJS와 AMD가 서로 호환되지 않는 문제가 발생하면서 이를 해결하기 위해 나온 패턴으로 브라우저와 서버 사이드 모두 호환된다. 우리가 흔히 아는 Webpack, Rollup과 같은 번들러들은 ES6 방식으로 모듈 로드에 실패하였을 때 대체 모듈로 사용한다.

UMD 방식의 예

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([ 'jquery', 'underscore' ], factory)
} else if (typeof exports === 'object') {
// CJS
module.exports = factory(require('jquery'), require('underscore'))
} else {
// global
// root === window
root.Requester = factory(root.$, root._)
}
}(this, function ($, _) {
var Requester = {
// ...
}

return Requester
}))

ESM (ECMAScript Module)

ES6의 등장으로 JavaScript의 표준 모듈 시스템으로 명세

ES6 등장 이후 표준 모듈 시스템을 구현하고자 하는 JavaScript의 제안이며, 권장 사항이다. 많은 최신 브라우저에서 동작하며, CJS의 단순한 구문과 AMD의 비동기적 방식을 모두 갖춘 모듈 시스템이다.

HTML type을 module로 지정하여 하나의 독립된 모듈로 사용할 수 있으며, 레거시 브라우저에서는 ES6 문법을 지원하지 않기 때문에 Babel과 같은 트랜스파일러(Transpiler)가 등장하였다.

ESM 방식의 예

import { foo, bar } from './myLib'

export default function () {}
<script type="module">
import {func1} from 'my-lib';

func1();
</script>

Module Bundler

분리된 코드와 관련된 모든 모듈 종속성을 최소한의 파일로 병합하는 도구

SPA(Single Page Application)와 같은 Front-End에서 다양한 기술의 등장으로 웹 애플리케이션도 복잡도가 향상되었으며, 코드양도 증가하였다. 또한 많은 종속성 모듈에 대해 중복된 코드들이 증가하면서 이에 따라 효율성을 높이기 위한 도구로 번들러가 나오기 시작했다. 번들러는 애플리케이션에 필요한 모든 리소스들을 모듈 단위로 나누어 묶거나 난독화, Tree Shaking, CSS 전처리기 또는 최신 ESMAScript를 레거시 브라우저가 읽을 수 있도록 변환도 지원한다.

bundler

번들러에는 우리가 흔히 알고 사용하고 있는 Webpack, Rollup을 포함하여 Vite에서 기반으로 사용하고 있는 esbuild와 이외에도 snowpack, swc, parcel, browserify 등 많은 번들러가 출시되고 사용되고 있다.

  • 이미지

몇가지 대표적인 번들러를 비교해보자.

Entry Potint Dev-Server HMR Tree Shaking Transpile
webpack Javascript 파일이며, html은 플러그인을 사용 webpack-dev-server webpack-dev-server ES6 모듈만 지원, 별도의 설정 필요 babel-loader 등 로더 사용 및 구성
Rollup Javascript 파일이며, html은 플러그인을 사용 rollup-plugin-serve rollup-plugin-hotreload 코드를 정적으로 분석하며, 기존 도구와 모듈을 기반으로 빌드 가능 플러그인 지정
Parcel html 파일을 진입점으로 하며, html 파일 분석 후 Javascript 번들링 내장 dev-server 내장 HMR 모듈 ES6, CommonJS 모두 지원 설정없이 자동 진행

웹 애플리케이션의 발전과 번들러의 관계

애플리케이션의 기술 발전과 처리되어야 하는 Javascript 모듈의 개수가 늘어남에 따라 번들링의 성능도 향상이 되어야 하며, 캐싱 및 최적화 작업으로 인하여 개선된 성능이지만 대규모 종속성 클롤링으로 인한 개발 서버의 속도가 저하된다. 또한 종속된 모듈이 많을수록 개발 서버의 가동 시간과 HMR(Hot Module Replacement)의 시간이 증가하며, 다양한 프레임워크가 출시되면서 이를 지원하기 위해 번들러의 설정이 복잡해지고 있다.

결과적으로는 개발 생산성은 번들러로 인해 과거보다 증가하였지만, 웹 기술과 다양한 프레임워크의 등장, 그리고 증가하는 모듈과 애플리케이션의 규모에 따라 결국 다시 개발 생산성은 저하되는 아이러니한 상황이 발생한다.

실제로 필자가 진행한 프로젝트도 규모가 매우 큰 편인데 Webpack을 기반으로 사용기 때문에 최초 개발 서버 구동 시 3분 정도의 시간이 소요되며, HMR은 1분 안쪽, 빌드 시간은 5~6분으로 걸린다. 이 시간을 일 단위로 누적한다면 꽤 많은 시간을 비효율적으로 사용하고 있다. 그렇기에 vite를 사용하고자 세미나를 하고 마이그레이션 작업과 이 포스팅을 하고 있다.

이러한 문제점을 해결하기 위해 빠른 속도를 자랑하는 esbuild를 기반으로 한 vite가 출시되었다.


Vite를 알기 전 먼저 JavaScript의 모듈 시스템과 번들러를 알아보았고 왜 Vite와 같은 빠른 번들러가 나오는지 알아보았다. 다음 포스팅에서는 Vite에 대해 포스팅해 보도록 하겠다.