번들러 파헤치기 1 - 모듈 시스템의 발전과 역사 (commonJS, AMD, UMD, ESM-esmodule)
Code/JavaScript

번들러 파헤치기 1 - 모듈 시스템의 발전과 역사 (commonJS, AMD, UMD, ESM-esmodule)

반응형

Photo by Maarten van den Heuvel on Unsplash

개인적으로 클라이언트 환경에서 가장 진입장벽이 높게 느껴지는 부분은 빌드 환경인 것 같습니다.
다른 부분은 사실 실무에서도 자주 다루고 접하다 보니 금방 익숙해지는 반면, 프론트엔드의 경우 한 번 구성해 놓으면 직접 수정 할 일이 적다 보니 유난히 어렵게 느껴지기 쉬운 듯 합니다.

번들러 파헤치기 포스팅은 모듈 시스템의 역사, 번들러의 역사, 빌드 환경 구성으로 나누어 포스팅을 하려고 합니다.
각 시리즈는 이전 내용들에 의존하고 있는 부분이 있으니 길더라도 환경 구성 전 모든 시리즈를 훑어보시는 걸 추천드립니다.

빌드 환경 자체가 모듈 시스템, 번들러, 컴파일러 등 많은 부분이 서로 의존성이 높고, 공통점, 차이점, 특징도 워낙 다양한 부분이다 보니 선수 이해가 있어야 번들러를 이해하고 알맞은 구성을 할 수 있기에 포스팅도 최대한 흐름이 자연스러울 수 있도록 작성할 생각입니다.


번들러 파헤치기

1부 - 모듈 시스템의 발전과 역사 (commonJS, AMD, UMD, ESM-esmodule)

2부 - 번들러의 발전과 역사 (HTTP/1.1, webpack, rollup, parcel, snowpack, exbuild, vite, turbopack)

3부 - 오픈소스 라이브러리 만들기 (rollup / react / typescript / babel)


1부 모듈 시스템의 역사

자바스크립트는 태초에 복잡한 어플리케이션을 구현하기 위해 탄생한 언어가 아닙니다.

구조적인 특징만을 가지고 있던 정적인 '문서'에 동적인 처리를 추가해서 좀 더 문서를 잘 보여줄 수 있기 위해 탄생했기에 자바스크립트가 현대 웹 생태계의 거대한 주축이 될 것이라고 아무도 생각하지 못했고, 그렇다 보니 빠르게 발전하는 웹 생태계의 속도를 자바스크립트로만은 따라가기 어려운 문제들이 많았습니다.

자바스크립트 생태계는 대부분 이러한 문제를 해결하기 위해 다양한 라이브러리와 프레임워크 등이 나오고 또 지속적으로 ECMAScript의 표준 명세가 업데이트 되고 있는 것이라고 볼 수 있습니다.

반응형

1. '모듈'의 의미

들어가기에 앞서 우선 '모듈'의 의미에 대해 설명을 해야할 것 같습니다.
'모듈'은 일반적으로 코드와 데이터의 묶음을 의미합니다. Node 환경에서 모듈은 분리된 개별 파일 1개를 뜻합니다.

 

2. 모듈 시스템 탄생 전

javascript 파일은 html의 <script> 태그로부터 불러와집니다.

브라우저가 문서를 보여주기 위해서는 html을 파싱하는 단계를 거치게 됩니다.
html에서 외부 리소스를 요청하는 태그가 여럿 존재하지만 그 중 html의 파싱과, 렌더링에 직접적인 영향을 주는 요소로 CSS와 JS파일이 존재합니다.

그 중 javascript를 불러오는 <script> 태그는 render-blocking 요소로서, html 파싱 도중 script 태그를 만나게되면 파싱을 잠시 멈춘 뒤 javascript의 파일에 대한 파싱과 AST를 생성한 뒤 실행하고 그 뒤에 다시 html 파싱을 중단점으로부터 이어서 진행한다는 특징이 있습니다.

(지금의 script태그는 async, defer 등의 어트리뷰트를 통해 js파일 병렬 로드 처리, 실행 시점 제어가 가능하나, 여기선 아무 속성이 없는 경우로 설명합니다)

이 당시 자바스크립트는 모듈 시스템이 존재하지 않았습니다.
script 태그를 통해 각각의 분리된 모듈을 불러오는 경우에도 모두 하나의 파일에서 동작하는 것처럼 같은 스코프를 공유했습니다.

모듈 모두 같은 스코프에서 같은 전역객체를 보기 때문에, 전역 상태의 오버라이딩 등 예상치 못한 오류가 발생할 수 있는 문제들이 존재했습니다.

이를 해결하고자 즉시 실행 함수 IIFE를 통해 불필요한 전역 변수를 선언하지 않도록 하거나, namespace를 통해 관리하는 등의 방법이 나왔지만, 여전히 전역 객체의 모듈을 관리하는 것은 까다로운 문제였습니다.

 

3. 모듈 시스템의 발전 및 역사

모듈시스템의 탄생

 

3-1. CommonJS의 탄생 (2009) - 구) ServerJS

서버를 위한 모듈 표준을 만들자, 첫 모듈 시스템

2005 ~ 2008년, J-Query의 탄생, AJAX의 보편화로 인한 사실상 프론트엔드의 탄생이었던 시기, 웹 생태계가 엄청난 발전을 하게 되면서 자바스크립트가 클라이언트 사이드에서만 작동하지 않고, 서버 사이드에서도 동작해야 한다라는 목소리가 나오기 시작했습니다.

당시 자바스크립트는 브라우저 위에서만 동작했으며, 서버 사이드에서 사용할 수 있을 정도의 기능이나 표준이 마련되어 있지 않은 상태였습니다.

하지만 클라이언트와 서버를 같은 언어로 통합한다면 리소스 측면에서도 많은 이점이 있었기에 수요는 늘어갔으며,
서버 사이드에서도 사용하는 데에 문제가 없도록 골칫덩어리인 모듈 문제를 해결하고자 모듈 표준을 만들자는 움직임이 일어났고 Kevin Dangoor을 주축으로 모인 개발자들에 의해 CommonJS가 탄생하게 되었습니다.

CommonJS는 초기에 ServerJS였습니다.

이후에는 환경 제약없이 범용적으로 사용하는 것을 목표로 하여 commonJS로 이름을 변경하였으나, 초기 이름을 통해 서버 사이드 수요로 인해 탄생했다는 것을 명확히 알 수 있습니다.

// test.js
module.exports = 'hi'

// index.js
const Hi = require('./test.js')

commonJS는 module.exports, 숏컷인 exports 객체를 통해 모듈을 내보내고, require 함수를 통해 모듈을 불러올 수 있었습니다.

다시 말해, module.exports라는 전역 객체에 값을 할당한 뒤, require 함수를 실행시켜 module.exports 전역 객체에 접근하여 값을 가져오는 방법을 사용할 수 있게 된 것입니다.

commonJS의 경우 모듈 로더가 동기적으로 작동한다는 특징이 있습니다. (명확히는 require() 함수가 동기적으로 작동하기 때문)

모듈을 순서대로 하나씩 불러오고 처리하는 것인데, 서버사이드 작동을 위해 고안되었기때문에 비동기로 동작하는 브라우저 환경에 사용하기에는 무리가 있었습니다. (commonJS만 존재하던 시기에는 클라이언트 사이드에서도 commonJS를 사용하는 던 시기가 존재했습니다)

 

왜 서버는 동기적으로 작동해도 괜찮은가?

서버 어플리케이션은 파일 시스템에 직접 접근할 수 있으며, 필요한 모듈이나 데이터를 로컬에서 빠르게 로드할 수 있습니다. 이러한 방식은 네트워크 지연 시간이 거의 없기 때문에 동기적 로딩이 사용자 경험에 미치는 영향이 적다고 볼 수 있습니다.

 

왜 브라우저는 동기적으로 작동하면 안되는가?

브라우저의 경우 서버 환경과 달리 네트워크를 통해 모듈이나 라이브러리를 로드해야 하기에, 브라우저 환경에서의 동기적 로딩은 웹 페이지의 로딩 시간을 크게 증가시킬 수 있습니다. 

규모가 큰 어플리케이션의 경우에는 더 많은 영향을 받을 수 밖에 없었고, 브라우저 환경을 고려한 비동기로 동작하는 모듈 시스템 AMD가 탄생하게 됩니다.

 

3-2. AMD (Asynchronous Module Definition) - 2009

비동기 로드 모듈 시스템을 위한 표준

(CommonJS 그룹에서 브라우저 환경을 위해 비동기 동작도 지원해야 한다는 의견이 있던 구성원들이 독립하며 만들어진 그룹)

AMD 그룹은 브라우저 환경을 위한 브라우저 모듈의 표준을 만들고자 했으며,
AMD 그룹은 자바스크립트 모듈과 의존성을 비동기적으로 로드하는 방법을 정의하는 개방형 표준을 공개했습니다. AMD 표준 명세 

이 표준 명세를 기반으로 모듈 시스템을 위한 다양한 라이브러리가 탄생했습니다. 
표준이 알려지며 가장 널리 채택되기 시작한 라이브러리로는 RequireJS가 존재합니다.

(AMD로는 curl.js, SystemJS 등 다양한 라이브러리도 존재했습니다.)

/* RequireJS */

// messages.js
define(function () {
    return {
        getHello: function () {
            return 'Hello World';
        }
    };
});


// main.js
define(function (require) {
    // Load any app-specific modules
    // with a relative require call,
    // like:
    var messages = require('./messages');

    // Load library/vendor modules using
    // full IDs, like:
    var print = require('print');

    print(messages.getHello());
});

RequireJS는 AMD명세를 따르며, define을 통해 모듈을 정의하고, require를 통해 모듈을 비동기적으로 불러와 사용할 수 있게되었습니다.

 

3-3. UMD (Universal Module Difinition) - 2009 ~ 2010

다양한 모듈 방식을 분기처리를 통해 관리하는 프로그래밍 패턴

자바스크립트 생태계가 점점 넓어지며, CommonJS / AMD 모듈 시스템을 모두 지원해야 하는 상황이 생기게 되었고 (브라우저/서버 런타임을 모두 지원하는 라이브러리), 이 두 모듈 방식을 좀 더 효율적으로 구성하기 위해 모듈 관리 패턴 UMD가 탄생했습니다. UMD API 명세

(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define([], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory();
  } else {
    // browser
    root.isDev = factory();
  }
})(this, function() {
  return process.env.NODE_ENV === 'development';
});

 

3-4. ESM (esmodule) - 2015

자바스크립트 모듈 표준의 등장, ECMAScript2015 

모듈화를 위한 다양한 움직임 끝에 드디어 ECMAScript2015 표준 명세에 ECMA modules이 등장하며, 자바스크립트 자체 모듈 시스템을 사용할 수 있게 되었습니다.

/* package.json */
{
	"type" : "module"
}

/* html */
<script type="module" src="./test.js" />
<script type="module" src="./test.mjs" />

package.json 내부에 모듈 타입을 정의하여 (기본값 commonJS) ESM 사용을 명시하며, script 태그의 type을 module로 설정하여 mjs, js포맷 파일에 대해 esmodule을 사용할 수 있게되었습니다.

/* test.js */
const hello = 'hello'
export default helo;

/* index.js */
import test from './test.js';
console.log(test) // 'hello'
/* test.js */
export const hello = 'hello'
export const test = 'test'

/* index.js */
import {hello, test} from './test.js';
console.log(test) // 'hello'

export, import 구문을 통해 내보낼 모듈과 가져올 모듈을 정의할 수 있습니다.
(단일 모듈은 export default 를 통해 정의)

commonJS는 module.exports 객체를 통해 모듈을 정의하다 보니, 객체 특성상 재할당 등을 통해 동적으로 값이 변경될 여지가 많아 명확히 의존성을 파악하기 어려운 구조를 가지고 있었습니다.

반면, ESM의 경우 import, export 구문을 사용해 모듈을 명확히 정의할 수 있게 되었고, 이로 인해 각 모듈 간 명확한 의존성 파악을 가능하게 하였습니다.

가장 큰 특징을 다시 짚고 넘어가겠습니다.
명확한 모듈 정의 방식을 사용함으로서 모듈 간 의존성 파악이 명확해졌다.

 


 

4. 정리

흐름도 : CJS -> AMD -> UMD -> ESM
자바스크립트의 모듈 표준이 존재하지 않던 시절, 모듈화를 위한 움직임이 모듈 시스템을 탄생시켰다.

CommonJS

  • 자바스크립트를 서버 사이드에서도 사용하기 위한 첫 모듈 시스템
  • 탄생 : 서버 사이드에서 사용하기 위해선 모듈 표준이 있어야 한다.
  • 동기적 모듈 로더 방식

AMD

  • 비동기로 동작하는 브라우저를 위한 모듈 시스템
  • 탄생 : 브라우저 환경을 위한 모듈 표준이 있어야 한다.
  • 비동기적 모듈 로더 방식

UMD

  • commonJS, AMD, 브라우저 등 다양한 모듈을 환경에 맞춰 제공하기 위해 고안된 프로그래밍 패턴

ESM

  • ECMAScript2015 채택된 자바스크립트 모듈 시스템
  • import, export 구문을 통해 사용하는 모듈을 명확히 정의하여 사용할 수 있음
  • 명확한 의존성 파악 가능

 

이러한 특징들은 이후 번들러의 발전, 역사 등과 또 아주 깊은 관련이 있기때문에 1부로 먼저 포스팅을 작성했습니다.
다음 포스팅은 번들러의 발전, 역사로 찾아오겠습니다!

다음 포스팅 : 번들러의 발전과 역사

감사합니다.

 


참고

- https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1

- https://youthfulhps.dev/javascript/nodejs-module-system/

- https://yceffort.kr/2023/05/what-is-commonjs

- https://d2.naver.com/helloworld/591319

- https://d2.naver.com/helloworld/12864

- https://blog.rhostem.com/posts/2019-06-23-universal-module-definition-pattern

- https://github.com/volojs/create-template/blob/master/www/app/main.js

- https://ko.javascript.info/modules-intro

- https://github.com/amdjs/amdjs-api/blob/master/AMD.md

- https://velog.io/@yesbb/%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EC%97%AD%EC%82%AC-%EA%B7%B8%EB%A6%AC%EA%B3%A0-ESM

반응형