Reese-log
  • About
  • Blog

© 2025 Reese. All rights reserved.

2023년 12월 4일

JavaScript Module System(2) - UMD와 ESM

#Module

JavaScript 모듈 시스템의 발전 과정을 이어서 살펴보면, AMD와 CommonJS는 각각 브라우저와 서버 측 환경에 최적화된 솔루션을 제공했지만, 두 환경을 모두 지원하는 모듈을 작성하는 데에는 어려움이 있었습니다.

UMD는 이러한 문제를 해결하고자 등장했습니다.

UMD (Universal Module Definition)

UMD는 AMD와 CommonJS 모듈 시스템을 결합하여, 동일한 모듈이 브라우저 환경과 Node.js 환경에서 모두 동작할 수 있도록 하기 위해 만들어졌습니다.

이를 위해 UMD는 실행 환경을 자동으로 감지하고, 해당 환경에 맞는 모듈 정의 방식을 사용합니다. 이를 통해 모듈의 재사용성과 호환성을 높였습니다.

어떻게 AMD와 CommonJS 모듈 시스템을 결합할까?

⇒ 주요 환경을 감지하고 각 환경에 맞는 방식으로 모듈을 정의해서 사용합니다.

  • AMD 환경:
  • CommonJS 환경:
  • 전역 변수(Global) 환경:
  • javascript
    1(function (root, factory) {
    2    if (typeof define === 'function' && define.amd) {
    3        // AMD 환경
    4        define([], factory);
    5    } else if (typeof module === 'object' && module.exports) {
    6        // CommonJS 환경
    7        module.exports = factory();
    8    } else {
    9        // 전역 변수 (브라우저) 환경
    10        root.MyModule = factory();
    11    }
    12}(typeof self !== 'undefined' ? self : this, function () {
    13    // 모듈 정의
    14    return {
    15        sayHello: function() {
    16            return 'Hello, world!';
    17        }
    18    };
    19}));

    장점

  • 다양한 환경 지원
  • 단점

  • 복잡한 구조
  • 성능 저하
  • 테스트의 복잡성
  • *왜 환경을 감지할 때 AMD → CJS → 전역 변수 순서를 따르는게 일반적인가요?

  • 브라우저 환경의 우선 순위: AMD는 브라우저 환경에서 비동기 로딩의 이점을 제공하므로 우선순위를 앞에 둡니다.
  • 서버 환경의중요성: CommonJS는 서버 측 환경에서 동기 로딩의 장점을 제공하므로 두 번째로 감지합니다.
  • 최후의 수단으로 전역 변수: 모듈 로더가 없는 환경에서는 전역 변수를 사용하여 모듈을 정의하며, 이는 네임스페이스 오염을 피하기 위해 최후의 수단으로 사용됩니다.

  • 아쉬운 점은 UMD는 AMD와 CommonJS를 지원하기 위해 복잡한 조건문과 환경 감지가 필요하다는 것입니다.

    이때 모듈 시스템의 표준화를 통해 모듈 관리를 개선하고 최적화 하기 위해서 ESM이 등장하게됩니다.

    ESM(ECMAScript Modules)

    ECMAScript 표준에 따라 JavaScript 모듈을 정의하고 사용하는 방식입니다.

    JavaScript 모듈 시스템의 표준으로, 브라우저와 서버 환경 모두에서 사용됩니다.

    최신 브라우저와 Node.js에서 네이티브로 지원되기에 추가적인 라이브러리나 도구 없이 모듈을 사용할 수 있습니다.

    장점

  • 정적 분석과 최적화 import와 export문법을 사용해 모듈간의 의존성을 명확하게 정의하여 컴파일러와 번들러가 모듈의 의존성을 정적으로 분석할 수 있게 합니다. 트리 셰이킹 → 정적 분석을 통해 사용하지 않는 코드를 쉽게 식별하여 제거함으로 번들 크기를 줄이고 애플리케이션의 성능을 최적화할 수 있습니다.
  • 비동기 및 동적 로딩
  • 일관된 문법과 사용성
  • 네임스페이스와 코드 분리
  • 성능 최적화
  • 브라우저 및 서버 측 지원
  • strict mode
  • import를 사용한 동적 임포트 예제

    javascript
    1function loadModule() {
    2    import('./module.js').then(module => {
    3        module.default();
    4    });
    5}
    6
    7document.getElementById('loadButton').addEventListener('click', loadModule);

    *정적 분석이란? 코드 실행 없이 소스 코드를 분석하는 것을 의미합니다. 코드의 구조, 의존성, 사용되지 않는 코드 등을 파악할 수 있습니다.

    단점

  • 구형 브라우저 호환성
  • 서버 사이드에서는 별도 설정 필요

  • ESM에서의 정적 분석 처리 과정

  • 모듈 의존성 그래프 생성
  • javascript
    1// moduleA.js
    2export const foo = 'foo';
    3
    4// moduleB.js
    5import { foo } from './moduleA.js';
    6console.log(foo);

    위의 코드에서, moduleB.js는 moduleA.js에 의존합니다. 정적 분석을 통해 이 관계가 그래프로 표현됩니다.

    javascript
    //그래프를 시각화한 예시
    ├── moduleA.js
      │    └── moduleC.js
      └── moduleB.js
           └── moduleC.js

  • 트리 셰이킹(Tree Shaking)
  • javascript
    1// moduleA.js
    2export const foo = 'foo';
    3export const bar = 'bar'; // 사용되지 않음
    4
    5// moduleB.js
    6import { foo } from './moduleA.js';
    7console.log(foo);

    위의 코드에서 bar는 moduleB.js에서 사용되지 않으므로, 트리 셰이킹을 통해 번들에서 제거될 수 있습니다.

  • 코드 스플리팅(Code Splitting)
  • javascript
    1// main.js
    2import { foo } from './moduleA.js';
    3
    4function loadModule() {
    5    import('./moduleB.js').then(moduleB => {
    6        moduleB.doSomething();
    7    });
    8}
    9
    10console.log(foo);

    위의 코드에서 moduleB.js는 동적 import()를 통해 비동기적으로 로드됩니다. 이를 통해 초기 로딩 성능을 최적화할 수 있습니다.

  • 빌드 최적화
  • javascript
    1// main.js
    2import { foo } from './moduleA.js';
    3import { bar } from './moduleC.js';
    4
    5console.log(foo);
    6console.log(bar);

    위의 코드에서 moduleA.js와 moduleC.js의 의존성을 분석하여, 필요한 모듈만 포함하는 최적화된 번들을 생성합니다.

    ESM이 도입된 이후 UMD와 CommonJS 모듈 시스템을 사용하는 상황은?

    UMD는 라이브러리 배포와 다양한 환경 지원을 위해, CommonJS는 Node.js환경과 레거시 코드베이스 유지보수를 위해 사용됩니다.

    UMD를 사용하다가 ESM으로 마이그레이션 하려면 어떤 것들이 고려될까?

  • 환경 감지를 위한 조건문들 제거하게되고
  • import와 export 문법을 사용해 모듈들을 정의하게되고
  • 브라우저인지 Node.js인지에 따라 호환성을 고려해 설정을 수정해주는 작업

  • 이렇게 모듈 시스템의 변천사를 살펴봤습니다.

    각 모듈 시스템들은 이전 방식의 아쉬운 점들을 보완하기 위해 계속 발전해왔습니다.

    모듈 시스템의 발전은 UX/DX의 엄청난 상승을 가져왔다고 느꼈는데요.

    (CJS가 없었더라면 지금의 Framework들도 없었을 것이고 Node.js가 없었으면 npm이 없었으면.. )

    위 경우들처럼 개발생태계에 큰 기여를 하는 생에 꼭 경험해보고싶습니다.