자바스크립트 모듈 시스템

이종경·2025년 7월 26일
0
post-thumbnail

JavaScript 모듈 시스템

// A.js
var name = 'foo';
function getName() {
  return name;
}

// B.js
function sayHello() {
  alert('Hello ' + name); // Hello foo
}
sayHello();

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

최초의 JavaScript는 매우 단순한 모듈 시스템만을 제공했습니다. HTML 페이지에서 JavaScript 소스 파일을 <script> 태그로 직접 로드하고, 브라우저가 이를 순차적으로 실행하는 방식이었습니다. 하지만 이 방식은 여러 JavaScript 파일이 하나의 전역 스코프를 공유하기 때문에 변수 충돌이 자주 발생하거나 로드 순서에 따라 의존성이 깨지는 문제가 빈번했습니다.

이러한 문제를 완화하기 위해 즉시 실행 함수(IIFE) 패턴이나 네임스페이스 객체를 활용해 변수의 범위를 격리하는 기법이 도입되었지만, 근본적인 한계를 극복하지는 못했습니다.

이 같은 배경에서 2008년 Google이 개발한 V8 엔진이 등장하면서 JavaScript 생태계에 큰 변화가 시작되었습니다. 특히 2009년부터는 JavaScript의 모듈 시스템을 본격적으로 표준화하려는 움직임이 나타나기 시작했습니다.

CJS (CommonJS)

CJS

2009년 Kevin Dangoor 등을 중심으로 서버 사이드 JavaScript에 적합한 모듈 표준을 만들기 위한 움직임이 시작되었고, 그 결과 CommonJS가 탄생했습니다. 초기에는 ServerJS라고 불렸으나, 서버 환경에만 국한되지 않고 범용적인 모듈 표준을 목표로 하면서 CommonJS로 개명되었습니다. 이후 CommonJS 모듈 사양은 Node.js에 채택되어 Node.js의 기본 모듈 시스템으로 자리 잡게 되었습니다.

CommonJS에서는 한 파일이 하나의 모듈이며, module.exports 또는 exports 객체를 통해 내보낼 값을 정의하고 다른 모듈에서는 require() 함수를 통해 이를 불러옵니다.

// CommonJS 모듈 정의
module.exports = foo;

// CommonJS 모듈 사용
const foo = require('./foo');

CommonJS의 가장 큰 특징은 모듈 로딩이 동기적이라는 점입니다. 즉, require() 호출이 발생하면 코드의 실행을 멈추고, 즉시 해당 모듈 파일을 로드한 뒤 평가하여 그 결과를 반환합니다.

하지만 이 동기적 설계는 처음부터 비동기 로딩을 고려하지 않았기 때문에 브라우저 환경에서는 CommonJS 모듈 시스템을 직접 사용할 수 없다는 근본적인 한계를 갖고 있었습니다. 따라서 CommonJS는 Node.js 프로젝트에서는 효과적이었지만 브라우저 환경에서는 제한적일 수밖에 없었고, 이를 극복하기 위한 첫 번째 빌드 도구로 Browserify가 등장하게 되었습니다.

AMD (Asyncronous Module Definition)

AMD

AMD는 브라우저 환경에서 사용할 수 있는 JavaScript 모듈 시스템입니다. 본래 CommonJS 진영에서 브라우저에서도 활용 가능한 JavaScript 모듈 방식을 논의했으나 합의에 이르지 못해 별도의 그룹으로 독립하여 만들어졌습니다. 초창기 AMD가 만들어진 주된 이유 역시 브라우저에서의 모듈 실행을 우선적으로 고려했기 때문입니다. 브라우저 환경에서는 필요한 모듈들을 네트워크를 통해 비동기적으로 다운로드한 후에야 사용할 수 있었기 때문입니다.

AMD 모듈은 define() 함수를 호출하여 정의합니다. define 함수는 모듈 ID(선택 사항), 의존 모듈의 배열, 그리고 팩토리 함수를 인자로 받습니다. 의존 모듈을 비동기적으로 불러온 뒤 모든 준비가 완료되면 팩토리 함수를 실행합니다.

// define 모듈 정의 (AMD)
define(['./util', 'jquery'], function(util, $) {
  // 의존 모듈 util.js와 jQuery를 모두 불러온 뒤 이 함수 실행
  function showValue(x) {
    $('#result').text(util.compute(x));
  }
  return { showValue };
});

// require로 모듈 사용 (AMD)
require(['./myModule'], function(myModule) {
  myModule.showValue(42);
});

CommonJS와 달리 AMD는 모듈을 비동기적으로 로드하기 때문에 브라우저가 파일을 내려받는 동안 다른 작업을 동시에 수행할 수 있으며, 이는 페이지 로딩을 블로킹하지 않는다는 장점으로 이어집니다. 또한 AMD는 전역 네임스페이스 오염을 방지하고 모듈별 스코프를 보장하여 여러 스크립트 간의 충돌 문제를 예방합니다.

한편 AMD는 CommonJS와 마찬가지로 모듈 시스템에 대한 사양일 뿐이므로, 실제 프로젝트에서 사용하려면 RequireJS와 같은 별도의 구현체를 포함해야 합니다.

UMD (Universal Module Definition)

자바스크립트 생태계가 발전하면서 하나의 라이브러리를 서버(Node)와 브라우저 모두에서 사용하려는 수요가 늘었습니다. 2010년 전후로 많은 라이브러리들이 브라우저용(AMD) 빌드와 Node용(CommonJS) 빌드를 별도로 제공하거나, 또는 둘 중 하나만 지원하여 사용자에게 호환성 문제라는 제약을 주었습니다. 이를 개선하기 위해 UMD가 등장하게됩니다.

Universal 이라는 이름에서 알 수 있듯이, CommonJS와 AMD 방식을 모두 호환할 수 있도록 조건문으로 분기하고, 이를 팩토리 패턴으로 구현했습니다.

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 환경: define을 사용하여 모듈 정의
    define(['lodash'], factory);
  } else if (typeof exports === 'object' && typeof module !== 'undefined') {
    // CommonJS 환경: module.exports 사용
    module.exports = factory(require('lodash'));
  } else {
    // 브라우저 전역 환경: window에 붙임
    root.MyLibrary = factory(root._);
  }
}(typeof globalThis !== 'undefined' ? globalThis : this, function (_) {
  // 모듈 본체 구현부
  function doSomething() { /* ... */ }

  return { doSomething };   // AMD나 CJS에서는 반환값이 exports
}));

ESM (ES6 Module)

ESM

과거 JavaScript의 모듈화는 주로 CommonJS와 AMD가 각자의 방식으로 구현했습니다. 그러나 언어 차원의 표준 모듈 시스템이 없다는 점은 계속해서 문제로 남아 있었습니다. 결국 2015년에 발표된 ECMAScript 6(ES6)에서 공식적으로 ECMAScript Modules(ESM)를 표준 모듈 시스템으로 채택하게 됩니다.

ESM은 importexport라는 간결한 키워드를 도입하여 모듈을 파일 단위로 명확히 구분합니다. 변수나 함수를 외부로 공개할 때는 선언 앞에 export를 붙이고, 다른 모듈의 내용을 가져올 때는 import를 사용하면 됩니다.

// math.mjs (ES 모듈 정의)
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default add;            // 기본(Default) 내보내기

// main.mjs (ES 모듈 사용)
import myAdd, { multiply as mul } from './math.mjs';
console.log(myAdd(2, 3));      
console.log(mul(2, 3));        

ESM은 동기적 로딩과 비동기적 로딩을 모두 지원하며, 문법도 간결하고 직관적입니다. 또한 CommonJS와 달리 실제 객체나 함수의 바인딩을 직접 참조하기 때문에 순환 참조 문제도 쉽게 관리할 수 있습니다. 특히 정적인 모듈 구조 덕분에 트리 쉐이킹을 통한 최적화도 용이해졌습니다.

정리

자바스크립트의 모듈 시스템은 단일 스크립트 파일 시절 → CommonJS/AMD로 이원화 → UMD로 임시 통합 → ES6 표준으로 공식 통합의 과정을 거쳤습니다. 현재는 ESM이 공식 표준으로 자리 잡았으며, 프런트엔드와 백엔드 모두 ESM을 사용하는 방향으로 나아가고 있습니다.

모듈 시스템의도된 환경로딩 방식문법 예시
CommonJS (CJS)서버 (Node.js)동기 (require)const m = require('x')module.exports = ...
AMD브라우저 (RequireJS)비동기 (define)define(['dep'], function(dep){ ... })
UMD범용 (브라우저+Node)혼합 (런타임 분기)IIFE 패턴으로 AMD/CJS 분기if (define.amd) define(...); else if (module.exports)...
ESM (ES Module)표준 (브라우저+Node)비동기 (정적 선언)import {foo} from 'm.js'export function foo(){}

참고
JavaScript Module System
JavaScript 표준을 위한 움직임: CommonJS와 AMD
의존성 관리
자바스크립트의 표준 정의 : CommonJS vs ES Modules
JavaScript 번들러로 본 조선시대 붕당의 이해

profile
작은 성취들이 모여 큰 결과를 만든다고 믿으며, 꾸준함을 바탕으로 개발 역량을 키워가고 있습니다

0개의 댓글