자바스크립트 모듈 이해하기(CommonJS, AMD, ESM, UMD)

최소희·2024년 11월 24일
post-thumbnail

이번 책 스터디에서는 자바스크립트와 리액트 디자인 패턴을 다루면서, 자바스크립트의 주요 모듈 시스템인 CommonJS, AMD, ESM, UMD에 대한 이야기가 나왔다.

이 모듈 시스템들의 동작 원리와 차이점을 좀 더 깊이 이해하기 위해,

각 모듈 시스템을 직접 구현하고 실험해 보았다.

이를 통해 얻은 실행 방식의 차이점과 이해를 정리한 내용을 공유하고자 한다.

폴더 구조

이 글에서 진행할 프로젝트의 폴더 구조는 다음과 같이, 각 모듈 시스템 별로 구분하였다.

├── index.html
├── amd
│   ├── index.js
│   └── module.js
├── cjs
│   ├── index.js
│   └── module.js
├── esm
│   ├── index.js
│   └── module.js
├── umd
│   ├── index.js
│   └── module.js
├── assets
│   └── ...

ESM (ES6 모듈)

ESM(ECMAScript Modules)은 ES6에서 도입된 표준 모듈 시스템으로, importexport 구문을 사용하여 모듈을 불러오고 내보낸다.

코드 예시

// esm/module.js

export function sayHello() {
  console.log("Hello esm");
}
// esm/index.js

import { sayHello } from "./module.js";

function esm() {
  sayHello();
}

window.esm = esm;

모듈 로드

index.html 파일에서 esm/index.js 모듈을 로드하려면 <script> 태그에 type="module" 속성을 추가해야 한다.

이는 브라우저에게 해당 스크립트를 ES6 모듈로 처리하라고 지시하는 것과 동일하다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>ESM 예제</title>
  <script type="module" src="./esm/index.js"></script>
</head>
<body>
  <h1>ESM</h1>
  <button onclick="esm()">ESM Button</button>
</body>
</html>

브라우저에 로드된 모듈을 확인할 수 있다. ↓

esm

브라우저와 Node.js에서의 ESM 동작 차이

브라우저 환경에서는 위와 같이 <script type="module">을 사용하여 ESM을 로드할 수 있다.

그러나 Node.js에서 동일한 esm/index.js 파일을 실행하면 브라우저에서와는 다르게 동작한다.

Node.js가 기본적으로 .js 파일을 CommonJS(CJS) 모듈로 인식하기 때문이다.

esm-error

Node.js에서 ESM 사용하기

Node.js에서 ESM을 제대로 인식하고 실행하기 위해서는 몇 가지 설정이 필요하다.

다음 두 가지 방법 중 하나를 선택할 수 있다.

1. 파일 확장자 변경 (.mjs)

.mjs 확장자를 사용하면 Node.js가 해당 파일을 ESM으로 인식하게 된다.

코드 예시

// esm/index.mjs
import { sayHello } from './module.mjs';

sayHello();
// esm/module.mjs
export function sayHello() {
  console.log('Hello from ESM!');
}

실행

node esm/index.mjs

2. package.json"type": "module" 설정

프로젝트의 package.json 파일에 "type": "module"을 추가하면 .js 파일을 ESM으로 인식하게 된다.

package.json 설정

// package.json

{
  "name": "프로젝트 이름",
  "version": "1.0.0",
  "type": "module",
  "main": "esm/index.js",
  // 기타 설정
}

코드 예시

// esm/index.js
import { sayHello } from './module.js';

sayHello();
// esm/module.js
export function sayHello() {
  console.log('Hello from ESM!');
}

실행

node esm/index.js

CJS (CommonJS)

CommonJS는 Node.js에서 기본적으로 사용하는 모듈 시스템이다.

requiremodule.exports를 사용하여 모듈을 불러오고 내보낸다.

코드 예시

// cjs/module.js

function sayHello() {
  console.log("Hello cjs");
}

module.exports = {
  sayHello,
};
// cjs/indjex.js

const { sayHello } = require("./module.js");

function cjs() {
  sayHello();
}

cjs();

CJS 모듈 실행

CommonJS 모듈은 Node.js에서 추가 설정 없이 간단하게 실행할 수 있다.

cjs

CJS를 ESM으로 번들링

CommonJS 모듈을 ESM으로 변환하기 위해 Rollup과 같은 번들러를 사용할 수 있다.

다음은 cjs/index.js를 ESM 호환 esm/bundle.js로 번들링하는 방법이다.

1. Rollup 및 CommonJS 플러그인 설치

npm install --save-dev rollup @rollup/plugin-commonjs

2. 프로젝트 루트 경로에 rollup.config.js 파일 생성

// rollup,config.js

import commonjs from "@rollup/plugin-commonjs";

export default {
  input: "cjs/index.js", // CJS 모듈의 진입점
  output: {
    file: "cjs/bundle.js", // 번들링된 ESM 파일의 출력 경로
    format: "es", // ESM 형식으로 번들링
    name: "cjs",
  },
  plugins: [commonjs()],
};

3. 번들링 실행

npx rollup -c

번들링된 결과물 ↓

// cjs/bundle.js

function getDefaultExportFromCjs(x) {
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default")
    ? x["default"]
    : x;
}

var cjs = {};

var module;
var hasRequiredModule;

function requireModule() {
  if (hasRequiredModule) return module;
  hasRequiredModule = 1;
  function sayHello() {
    console.log("Hello bundled cjs");
  }

  module = {
    sayHello,
  };
  return module;
}

var hasRequiredCjs;

function requireCjs() {
  if (hasRequiredCjs) return cjs;
  hasRequiredCjs = 1;
  const { sayHello } = requireModule();

  function cjs$1() {
    sayHello();
  }

  window.cjs = cjs$1;
  return cjs;
}

var cjsExports = requireCjs();
var index = /*@__PURE__*/ getDefaultExportFromCjs(cjsExports);

4. 번들링된 CJS 모듈을 브라우저에서 사용하기

번들링된 js 파일을 index.html에 추가 후, 전역에 등록된 cjs 메서드를 버튼 이벤트 핸들러를 통해 호출한다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>CJS 번들 예제</title>
  <script src="cjs/bundle.js" type="module"></script>
</head>
<body>
  <h1>CJS</h1>
  <button onclick="cjs.greet()">CJS 버튼</button>
</body>
</html>

AMD (Asynchronous Module Definition)

AMD는 주로 브라우저 환경을 위해 설계된 모듈 시스템으로, 모듈을 비동기적으로 로드하여 성능을 향상시킨다.

주요 특징

  1. 비동기 로딩: 모듈을 병렬로 로드하여 페이지 로드 시 블로킹을 줄인다.
  2. 의존성 관리: 의존성을 명확히 정의하여 모듈이 올바른 순서로 로드되도록 한다.

RequireJS를 사용한 AMD

RequireJS는 비동기적으로 모듈을 로드할 수 있는 require 함수를 제공한다.

여기서 잠깐,

🤔
CJS에서 사용하는 require 함수와 ReuqireJS에서 사용하는 require 함수는 다른가요?

네, 다릅니다.
CJS에서 사용하는 require 함수는 Node.js에서 CommonJS 모듈 시스템의 일부로 기본적으로 제공되는 함수입니다.
별도의 라이브러리 없이도 모듈을 동기적으로 불러올 수 있게 해줍니다.
반면, AMD에서 사용되는 RequireJS의 require 함수는 비동기적으로 모듈을 로드할 수 있도록 동작하는 함수입니다.

코드 예시

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- amd 모듈 로드를 위한 require.js -->
   <script
      src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.7/require.js"
      integrity="sha512-H/RK9lhgLZE7IvypfHj5iUX0fnbaz5gA8y81NQ8F6azabccQuFAVeQdvOYDeAvAsl/WZTOGphkwhhlpCJi157A=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
    <script src="amd/index.js" type="module"></script>

    <button onclick="sayHelloAmd()">AMD Button</button>
  </body>
</html>
// amd/module.js

define(function () {
  function sayHelloAmd() {
    console.log("Hello amd");
  }
  return {
    sayHelloAmd,
  };
});
// amd/index.js

require(["amd/module.js"], function (module) {
  window.sayHelloAmd = module.sayHelloAmd;
});

UMD (Universal Module Definition)

UMD는 브라우저와 Node.js 모두에서 사용할 수 있는 다목적 모듈 시스템이다.

CommonJS와 AMD의 기능을 결합하여 다양한 플랫폼에서 호환성을 제공한다.

UMD의 작동 방식

UMD는 모듈 로더의 존재 여부를 감지하여, 브라우저, CommonJS, AMD 등 다양한 환경에서 모듈을 적절하게 정의한다.

즉, 하나의 모듈이 여러 환경에서 호환되도록 설계 할 수 있다는 장점이 있다.

아래는 팩토리 패턴 기반으로 UMD를 구현한 코드이다.

// umd/module.js

// factory 함수는 모듈을 생성하는 함수
// name 은 모듈 이름

(function (root, factory, name) {
  // AMD 환경인지 확인하고, 맞다면 define 함수를 사용하여 모듈 정의
  if (typeof define === "function" && define.amd) {
    define([], factory);
    // CommonJS 환경인지 확인하고, 맞다면 module.exports에 팩토리 함수의 반환값 할당
  } else if (typeof module === "object" && module.exports) {
    module.exports = factory();
    // 그 외의 경우, 전역 객체에 'module'이라는 이름으로 팩토리 함수의 반환값을 할당
  } else {
    // 전역 객체에 모듈 이름으로 팩토리 함수의 반환값을 할당
    root[name] = factory()[name];
  }
  // 현재 컨텍스트(this)와 팩토리 함수를 인자로 전달
})(
  this,
  function () {
    // 'sayHello' 함수 정의
    function sayHello() {
      // 콘솔에 "Hello" 출력
      console.log("Hello umd");
    }

    // 'sayHello' 함수를 포함하는 객체 반환
    return {
      sayHello,
    };
  },
  "sayHello" // 모듈 이름
);
// umd/index.js

const module = require("./module.js");

function umd() {
  module.sayHello();
}

umd();

브라우저에서 호출하기

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>UMD 예제</title>
  <script src="umd/bundle.js"></script>
</head>
<body>
 <h1>UMD</h1>
    <button onclick="sayHello()">UMD Button</button>
</body>
</html>

brower-umd

Node.js에서 호출하기

Node.js에서도 UMD 모듈은 문제없이 작동한다.

AMD가 감지되지 않으면 CommonJS로 폴백되기 때문이다.

node-umd

마무리

ESM은 현대적이고 표준화된 접근을 제공하여 새로운 프로젝트에 적합하며, CommonJS는 Node.js의 서버 사이드 프로젝트에 최적화 되어 있다.

UMD는 다양한 환경에서 호환 가능하도록 설계되어 있으며, AMD는 브라우저 환경에서의 비동기 로딩을 통해 성능을 개선하는 데 중점을 두고 있다.

이처럼 자바스크립트 모듈 시스템은 다양한 환경과 사용 사례에 맞춰 크게 발전해왔다.

이번을 계기로 ESM, CommonJS, AMD, UMD의 각 모듈 시스템을 실제로 구현하면서 그 차이점과 각각의 장단점을 명확히 이해할 수 있었다.

참고 자료

profile
Frontend Engineer

0개의 댓글