Language/JavaScript

자바스크립트 모듈 시스템

SambaLim 2023. 4. 16. 16:33

처음 HTML에서 Javascript를 사용하여 프로그래밍을 할때는 대부분의 스크립트가 독립적인 작업을 수행하여 일반적으로 큰 스크립트가 필요하지 않았습니다.

하지만 프로젝트의 규모가 커지고 기능이 복잡해지면서 자바스크립트 커뮤니티에서 라이브러리를 공유하여 이를 프로젝트에 적용할 수 있는 형태로 발전했습니다. 자바스크립트 프로그램을 필요에 따라 가져올 수 있게 하기 위해서 코드를 모듈 단위로 구성해주는 다양한 시도를 하게 됩니다. 그리고 그 시도는 다음과 같은 모듈 시스템으로 이어졌습니다.

  • CJS(CommonJS)
    • JS 생태계를 브라우저뿐만 아니라 범용 언어로 사용할 수 있도록 만든 모듈시스템입니다.
  • AMD(Asynchronous Module Definition)
    • 가장 오래된 모듈 시스템 중 하나로 require.js라는 라이브러리를 통해 처음 개발되었습니다.
  • UMD(Universal Module Definition)
    • AMD와 CJS와 같은 다양한 모듈 시스템을 함께 사용하기 위해 만들어졌습니다.
  • ESM(ECMAScript Modules)
    • ES6부터 추가된 자바스크립트의 모듈 시스템입니다.

CJS(CommonJS)

주로 Node.js 환경에서 많이 사용되며 require()로 다른 모듈에서 내보낸 변수와 함수를 가져오고 module.exports 구문을 사용하여 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.

예제 코드

// require()을 사용하여 다른 모듈의 변수와 함수를 가져옵니다.
var lib = require('package/lib');

// 가져온 모듈을 사용할 수 있습니다.
function foo () {
  lib.log('hello world!');
}

// 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
exports.foobar = foo;

특징

  1. require() 함수를 이용하여 모듈을 로딩할 때, 동기적인 방식으로 로딩됩니다. 해당 모듈이 로딩되기 전에 다른 코드가 실행되지 않습니다.
  2. CJS 모듈은 파일 기반으로 정의되며, 파일 이름이 모듈 이름으로 사용됩니다.
  3. CJS는 npm(Node Package Manager)에서 지원하는 다양한 라이브러리와 패키지를 사용할 수 있습니다.

AMD(Asynchronous Module Definition)

CJS는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 합니다. 즉 동기적인 동작이 가능한 서버사이드 자바스크립트 환경을 전재로합니다. 이는 브라우저에서 모듈이 다운로드 될때까지 아무것도 할 수 없는 상태가 된다는 치명적인 단점을 가지고 있습니다.

AMD는 이러한 문제를 해결하기 위해 비동기적으로 모듈을 로딩하고 사용할 수 있도록 합니다. 따라서 AMD는 주로 브라우저 환경에서 사용됩니다.

예제 코드

// define()을 사용하여 다른 모듈을 가져옵니다. 이는 콜백함수의 매개변수에 담깁니다.
define(['package/lib'], function (lib) {
  // 가져온 모듈을 사용할 수 있습니다.
  function foo () {
    lib.log('hello world!');
  }

  // 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
  return {
    foobar: foo,
  }
});

특징

  1. AMD는 비동기적으로 모듈을 로딩하고 사용할 수 있습니다.
  2. define() 함수를 사용하여 모듈을 정의합니다. 모듈 로딩과 모듈 실행을 분리하여 정의할 수 있습니다.
  3. AMD는 모듈 로딩 시점에 의존성을 관리합니다. 모듈이 로딩되기 전에 필요한 의존성 모듈이 먼저 로딩됩니다.

UMD(Universal Module Definition)

UMD는 CJS, AMD가 서로 호환되지 않는 문제가 생겨 이를 해결하기 위해 생겨났습니다.

예제 코드

CJS, AMD의 호환을 위해 UMD는 다음과 같이 구성됩니다.

  • 모듈 로더를 확인하는 즉시 실행 함수(IIFE)
    • 이 함수는 매개변수로 root(전역 범위)와 factory(모듈을 선언하는 함수)를 가집니다.
  • 모듈을 생성하는 익명 함수
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['exports', 'b'], factory);
  } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
    // CJS
    factory(exports, require('b'));
  } else {
    factory((root.commonJsStrict = {}), root.b);
  }
}(this, function (exports, b) {
  // 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
  exports.action = function () {};
}));

ESM(ECMAScript Modules)

ES6부터 지원하는 자바스크립트 공식 모듈시스템 입니다. export와 import를 사용하여 다른 모듈의 변수, 함수를 불러오거나 외부로 내보내는 것이 가능합니다.

최신 브라우저들에서 import, export 지원하기 시작했지만 크로스 브라우징을 위해서 Babel을 사용하는 것이 필요합니다.

예제 코드

// 다른 모듈의 변수와 함수를 가져옵니다.
import lib from 'package/lib';

// 가져온 모듈을 사용할 수 있습니다.
function foo () {
  lib.log('hello world!');
}

// 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
export { foo as foobar };

특징

  1. 모듈은 항상 use strict로 실행됩니다. 선언되지 않은 변수에 값을 할당하는 등의 코드는 에러를 발생시킵니다.
  2. 동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 한 번만 실행됩니다.
  3. ESM은 정적 로딩(Static Loading) 방식을 사용합니다. 런타임 이전에 모듈을 로딩하여 불필요한 로딩시간을 줄이고 의존성 관리와 코드 최적화를 용이하게 합니다.
  4. 브라우저 환경에서 type="module" 속성이 추가된 스크립트는 마치 defer 속성을 붙인 것 처럼 실행됩니다.
    1. async 속성 추가도 가능합니다.

마무리

자바스크립트의 모듈시스템이 어떻게 생겨났는지 이야기하고 CJS부터 ESM까지 자바스크립트의 모듈 시스템의 예제 코드와 특징을 보았습니다. 각각 모듈 시스템은 서로 다른 사용 목적과 환경에 따라 적합한 모듈 시스템을 선택하여 사용해야합니다.

참고자료