이번 책 스터디에서는 자바스크립트와 리액트 디자인 패턴을 다루면서, 자바스크립트의 주요 모듈 시스템인 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(ECMAScript Modules)은 ES6에서 도입된 표준 모듈 시스템으로, import 와 export 구문을 사용하여 모듈을 불러오고 내보낸다.
// 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>
브라우저에 로드된 모듈을 확인할 수 있다. ↓

브라우저 환경에서는 위와 같이 <script type="module">을 사용하여 ESM을 로드할 수 있다.
그러나 Node.js에서 동일한 esm/index.js 파일을 실행하면 브라우저에서와는 다르게 동작한다.
Node.js가 기본적으로 .js 파일을 CommonJS(CJS) 모듈로 인식하기 때문이다.

Node.js에서 ESM을 제대로 인식하고 실행하기 위해서는 몇 가지 설정이 필요하다.
다음 두 가지 방법 중 하나를 선택할 수 있다.
.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
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
CommonJS는 Node.js에서 기본적으로 사용하는 모듈 시스템이다.
require와 module.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();
CommonJS 모듈은 Node.js에서 추가 설정 없이 간단하게 실행할 수 있다.

CommonJS 모듈을 ESM으로 변환하기 위해 Rollup과 같은 번들러를 사용할 수 있다.
다음은 cjs/index.js를 ESM 호환 esm/bundle.js로 번들링하는 방법이다.
npm install --save-dev rollup @rollup/plugin-commonjs
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()],
};
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);
번들링된 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는 주로 브라우저 환경을 위해 설계된 모듈 시스템으로, 모듈을 비동기적으로 로드하여 성능을 향상시킨다.
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는 브라우저와 Node.js 모두에서 사용할 수 있는 다목적 모듈 시스템이다.
CommonJS와 AMD의 기능을 결합하여 다양한 플랫폼에서 호환성을 제공한다.
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>

Node.js에서도 UMD 모듈은 문제없이 작동한다.
AMD가 감지되지 않으면 CommonJS로 폴백되기 때문이다.

ESM은 현대적이고 표준화된 접근을 제공하여 새로운 프로젝트에 적합하며, CommonJS는 Node.js의 서버 사이드 프로젝트에 최적화 되어 있다.
UMD는 다양한 환경에서 호환 가능하도록 설계되어 있으며, AMD는 브라우저 환경에서의 비동기 로딩을 통해 성능을 개선하는 데 중점을 두고 있다.
이처럼 자바스크립트 모듈 시스템은 다양한 환경과 사용 사례에 맞춰 크게 발전해왔다.
이번을 계기로 ESM, CommonJS, AMD, UMD의 각 모듈 시스템을 실제로 구현하면서 그 차이점과 각각의 장단점을 명확히 이해할 수 있었다.