자바스크립트 모듈 시스템: ESM, CommonJS, AMD, UMD

0

모듈 시스템

자바스크립트에는 다양한 모듈 시스템이 있습니다. 모듈 시스템은 플러그인 파일이나 잘게 쪼개진 자바스크립트 코드 조각을 재사용하기 위해서 각각의 파일을 등록하고, 등록된 파일을 자바스크립트에서 불러와서 사용할 수 있게 해주는 프로그램입니다.

여기서 모듈은 프로그램을 구성하는 각각의 부품이라고 할 수 있는데, 이렇게 프로그램을 잘게 쪼개어 모듈화하는 이유는 다음과 같은 여러 장점이 있기 때문입니다.

  • 프로그램의 효율적인 관리 및 성능 향상
  • 전체적인 소프트웨어 이해의 용이성 증대 및 복잡성 감소
  • 소프트웨어 디버깅, 테스트, 통합, 수정 시 용이성 제공
  • 기능의 분리가 가능하고 인터페이스가 단순
  • 오류의 파급효과를 최소화
  • 모듈 재사용으로 개발과 유지보수가 용이

자바스크립트는 CommonJS, AMD, UMD, ESM과 같은 모듈 시스템을 사용할 수 있는데, 각 모듈 시스템에는 다음과 같은 특징이 있습니다.

CommonJS
서버 사이드에서 사용하며, 동기적으로 작동
require 문법 사용
AMD
서버 사이드와 클라이언트 사이드에서 사용할 수 있지만 클라이언트 사이드에서 주로 사용되며, 비동기적으로 작동
define – require 문법 사용
UMD
CommonJS와 AMD를 모두 사용하기 위한 구현 패턴
ESM
언어 자체에 표준으로 탑재된 모듈 시스템
export – import 문법 사용

자바스크립트에 모듈 시스템이 없었던 시절에는 클라이언트 사이드를 개발하는 경우에 필요한 파일을 만들어서 같이 배포한 다음, <script src="...">와 같이 필요한 파일을 추가해서 불러오는 방식을 주로 사용했는데, 이런 방식은 추가하는 파일이 많아지면 관리가 어려워지는 문제가 있었습니다.

외부의 코드를 추가하는 일반적인 방법
// 외부의 코드 불러오기
<script src="jquery.js"></script>
<script src="underscore.js"></script>

// 외부 코드에 정의 된 함수를 사용해 내 코드 작성
<script>
window.$
window._
</script>

파일을 추가해서 사용하는 방식은 특히 추가한 외부의 코드에서 같은 변수를 사용하는 경우에 문제가 될 수 있습니다. 예를 들어 다음과 같이 jqueryzepto 라이브러리를 같이 사용하용하게 되면 두 라이브러리가 모두 $를 사용하기 때문에 변수끼리 충돌이 일어날 가능성이 높아지는 것이죠.

<script src="jquery.js"></script>
<script src="zepto.js"></script>

<script>
// jquery와 zepto 라이브러리 모두 $를 변수로 사용
window.$ // 충돌 발생
</script>

결국 자바스크립트의 모듈 시스템은 외부의 라이브러리(모듈)를 추가해서 사용할 때 필요한 코드들만 사용하거나, 변수의 충돌과 같은 문제를 방지하고 관리의 용이성을 위해서 고안된 해결 방법입니다.

사실 라이브러리와 모듈은 개념상의 차이가 조금 있는데, 실제로 사용하는 방식은 거의 동일합니다. 라이브러리가 자동차의 네비게이션 시스템, 차량용 스피커 등의 외부 자원을 의미한다면 모듈은 자동차의 핸들이나 바퀴와 같은 부품을 의미한다고 볼 수 있지만, 모두 자동차의 본체와 연결되어 작동하는 것은 동일한 것 처럼 말이죠.

ESM은 ES6에서 부터 표준화 된 모듈 시스템인데, 주로 클라이언트 사이드에서 사용되고, CommonJS는 Node.js에서 주로 사용되는 모듈 시스템입니다.

AMD는 require.js라는 구현체를 활용하는 방식으로, 환경에 구애받지 않고 사용할 수 있는 장점이 있지만 최근에는 ESM이 기본 탑재되면서 많이 사용되지 않는 방식이고, UMD는 특정한 모듈 시스템을 지칭하는 것이 아니라 CommonJS와 AMD를 모두 사용할 수 있도록 구현한 일종의 디자인 패턴이라고 할 수 있는데, 각각의 모듈 시스템에 대해서는 조금 더 자세히 알아보겠습니다.

ESM

ESM은 ES6 부터 지원하고 있는 표준 모듈 시스템입니다. ES6 이전까지는 브라우저 환경에서 사용할 수 있는 표준 모듈 시스템이 없었기 때문에, 필요한 파일(모듈)을 만들어서 같이 배포하고, <script src="script.js">의 형태로 파일을 직접 불러오는 방법을 사용했습니다.

그래서 자바스크립트로 작성된 외부 라이브러리는 공식적으로 배포되는 스크립트 파일을 다운로드한 후에 함께 묶어서 배포하거나 CDN으로 제공되는 주소를 로드해서 사용하는 방법을 주로 사용했습니다.

사실 <script> 태그로 외부의 라이브러리를 불러오는 것도 하나의 모듈 시스템이라고 할 수 있는데, <script>만으로는 크고 복잡한 시스템에서 사용되는 수많은 파일들을 효율적으로 관리하기가 어렵다는 단점이 있었기 때문에, 크고 복잡한 시스템에서는 파일들을 조금 더 쉽고 효율적으로 관리할 수 있도는 모듈 시스템이 필요하게 되어, ES6 부터는 정식으로 표준 모듈 시스템이 도입되었고, importexport 문법을 통해서 모듈을 불러오거나 내보낼 수 있게 되었습니다.

export

ES6의 import를 설명하기 전에 export 문법을 명확히 이해할 필요가 있습니다. export는 기본적으로 named exportdefault export의 두 가지 방식과 exportfrom을 조합해서 사용하는 방식이 있는데, 각각의 사용법에 대해 알아보겠습니다.

named export

named export는 선언된 변수명을 그대로 export하는 방식입니다. named export는 모듈 내에 여러개의 export가 존재할 수 있습니다. 그리고 변수를 선언함과 동시에 내보내기를 할 수도 있고, 먼저 정의된 변수들을 모아서 내보내거나, 먼저 정의된 함수를 별칭으로 바꿔서 내보낼 수도 있습니다.

변수 선언 즉시 내보내기
export let name1;
export const name2;
export var name3;
export function name4 () {/*...*/}
export class MyClass {/*...*/}
변수 먼저 정의하고, 모아서 내보내기
const var1;
let var2;
var var3;
export { var1, var2, var3 }
먼저 정의된 함수를 별칭으로 바꿔서 내보내기
let var4;
export { var4 as var5 } // 다른 모듈에서 import 할 때는 var5라는 이름으로 import
default export

default export는 모듈에서 하나만 존재할 수 있고, named export와 같이 변수를 직접 내보낼 수는 없습니다. 만약에 default exportexport default {const, let, var} 처럼 변수를 직접 내보내는 방식으로 사용하면 에러가 발생합니다.

default export를 사용하는 방법들
export default expression;
export default function () { /*...*/ }            // 익명함수
export default function myFunction() { /*...*/ } // 기명함수
export default class { /*...*/ }                  // 클래스
export default class MyClass { /*...*/ }          // 커스텀 클래스
export default function* () { /*...*/ }           // 제너레이터

// named export 처럼 묶어서 내보내기
const myModule = { /*...*/ }
const var1 = () => {}
export { myModule as default, var1 } // as를 이용해 별칭으로 default export

// 아래와 같이 변수를 직접 내보내면 에러 발생
export default const test = /*...*/ // Uncaught SyntaxError: Unexpected token const

export default를 두 번 사용하는 경우에도 에러가 발생되는데, 이는 default라는 식별자export 문이 내부적으로 사용하기 때문입니다.

에러 메세지
export default function () { /*...*/ }
export default MyClass () { /*...*/ }
Uncaught SyntaxError: Identifier '*default*' has already been declared

export-from

export-from은 import와 export를 한 번에 처리할 수 있는 문법입니다. 이 문법은 주로 패키지의 다른 모듈들을 한 번에 모아서 일관된 형태로 내보내거나 관리하고 싶은 경우에 사용할 수 있는데, Node.js 같은 서버 사이드가 아닌 클라이언트 사이드의 환경에서는 from을 사용할 때 .js 확장자를 꼭 적어주어야 합니다. 만약 확장자를 적어주지 않으면 404 Error가 발생합니다.

우선 다음처럼 index.js math.js, logger.js, config.js 모듈을 포함하는 src/utils 패키지가 있다고 가정해 보겠습니다.

src/utils/math.js
export function add (a, b) { /*...*/ }
export function subtract (a, b) { /*...*/ }
export function multiply (a, b) { /*...*/ }
export function divide (a, b) { /*...*/ }
src/utils/logger.js
export default {
  print() { /*...*/ }
}
src/utils/config.js
export const DB_HOST = 'localhost';
export const DB_USER = 'scott';
export const DB_PASSWORD = 'tiger';

src/utils/ 패키지에 있는 src/utils/index.js에서는 export-from 문법을 다음처럼 사용할 수 있습니다. 그리고 src/utils/index.js에서 export된 모듈은 src/index.js에서 import 명령어로 불러와서 사용할 수 있습니다.

src/utils/index.js
// math 모듈에서 일부만 import 한 뒤 다시 export
export { add, subtract } from './math';

// config 모듈의 (export가 가능한)모든 변수를 export
export * from './config';

// logger 모듈의 default export를 Logger라는 이름으로 export
export { default as Logger } from './logger';
src/index.js
// 디렉토리를 import하면 기본적으로 index.js 파일을 탐색
import * as utils from './utils';

// utils라는 별칭으로 import한 메소드, 변수 사용
utils.add(1, 2) // 3
utils.DB_HOST // 'localhost'
utils.Logger.print('TEST') // 'TEST'

이렇게 src/utils/패키지의 index에서는 모듈을 한 번 정리해 주고, 이렇게 정리한 모듈을 하나의 모듈로 내보내서 필요한 메서드와 변수 등을 사용할 수 있습니다.

export-from 구문을 사용하면 importexport를 한번에 처리할 수 있는 장점이 있지만, 다음의 코드 처럼 export-from을 처리하는 파일 스코프에서는 식별자가 바인딩 되지 않기 때문에 메서드나 변수에 직접 접근하면 에러가 발생합니다. 에러가 발생하는 이유는 해당 메서드가 스코프내에 존재하지 않기 때문입니다.

src/utils/index.js
// math 모듈에서 일부만 import 한 뒤 export
export { add, subtract } from './math';

// export했지만 현재 파일에서 그냥 add 함수를 사용하면 스코프내에 존재하지 않아 에러 발생
add(1, 2); // Uncaught ReferenceError: add is not defined

import

import는 다른 파일에서 모듈을 불러오는 명령어입니다. import 명령어로 다음 코드처럼 모듈을 불러올 수 있고, as 명령어를 이용해서 별칭으로 불러오거나, *를 사용해서 named export의 모든 변수와 메서드를 불러올 수도 있습니다.

import 명령어로 모듈을 로드하는 방법들
import name from "module-name";
import * as name from "module-name";
import { member } from "module-name";
import { member as alias } from "module-name";
import { member1 , member2 } from "module-name";
import { member1 , member2 as alias2 , [...] } from "module-name";
import defaultMember, { member [ , [...] ] } from "module-name";
import defaultMember, * as alias from "module-name";
import defaultMember from "module-name";
import "module-name";

코드의 nameimport한 값의 현재 스코프 이름이고, member ~ memberNexport한 모듈의 멤버 이름, defaultMemberdefault export한 값을 받는 이름, alias ~ aliasNimport한 member에 새로 주어진 별칭을 뜻합니다. util.js 라는 모듈을 만들어야 하는 경우라면, 다음과 같이 코드를 작성하면 됩니다.

util.js
export function print (msg) {};
export function fetch (url) {};
export default {
  add(a, b) {},
  subtract(a, b) {}
}
default export만 가져오는 경우
app.js
// default export만 가져올 때
import util from './util';
util.add(1,2) // 3
named export를 가져오는 경우
app.js
// named export를 가져올 때
import { print, fetch } from './util';
print('Hello World') // Hello World 출력
별칭으로 가져오는 경우
app.js
// 별칭으로 가져올 때
import { print as logger } from './util';
logger('Hello World') // Hello World
named export를 모두 가져오는 경우
app.js
// *(asterisk)로 named export를 모두 불러오기
import * as util from './util'; // util 대신 별칭 사용
util.print('Hello World') // Hello World
default export, named export를 모두 가져오는 경우
app.js
// default export, named export 모두 불러오기
import util, { fetch, print } from './util';

util.add(2, 3) // 5
print('Hello!') // Hello!
fetch('https://httpbin.org').then(/*...*/)
바인딩 없이 모듈의 전체 메서드만 가져오는 경우
app.js
// 바인딩 없이 모듈의 전체 메서드만 가져오기
import 'util'; // util.js 코드가 한 번 실행되며, 변수에 바인딩 하지 않았기 때문에 사용할 수는 없음, 하지만 다음과 같이 default export와 named export를 같이 사용하는 모듈을 *(asterisk)로 불러오면 default라는 프로퍼티가 자동으로 생성됨

// default 프로퍼티는 default export 된 모듈과 같음
import * as util from './util';
util.default.add(1, 2) // 3

Module Type

<script> 태그에 type="module"을 선언하면 자바스크립트 파일은 모듈로 동작하게 되는데, 이 때 모듈이라는 것을 명확하게 표현하기 위해 확장자는 mjs라고 붙여주는 것을 권장한다고 합니다.

type="module"을 사용하면 해당 파일에서는 importexport를 사용할 수 있는데, 파일마다 독립적인 스코프를 갖게 되고, 각각의 mjs 파일에 있는 window 객체는 서로 공유되지 않습니다.

<script type="module" src="index.mjs">

ESM은 ECMAScript에서 지원하는 자바스크립트 공식 모듈 시스템이지만, 브라우저에 따라서는 아직 importexport를 지원하지 않는 경우도 있어서 가능하면 webpack과 같은 번들러를 사용하는 것이 좋습니다.

index.html
<!DOCTYPE html>
<html>
<body>
  <script type="module" src="foo.mjs"></script>
  <script type="module" src="bar.mjs"></script>
</body>
</html>
foo.mjs
var x = 'foo';
console.log(x); // foo
// 변수 x는 전역 변수가 아니고 window 객체의 프로퍼티도 아님
console.log(window.x); // undefined
bar.mjs
// import 사용
import test from './module.mjs';
console.log(test);

// 변수 x는 foo.mjs에서 선언한 변수 x와 스코프가 다른 변수
var x = 'bar';
console.log(x); // bar

// 변수 x는 전역 변수가 아니고 window 객체의 프로퍼티도 아님
console.log(window.x); // undefined
module.mjs
const test = 'hello world!';

// export 사용
export default test;

CommonJS

CommonJS는 새로운 자바스크립트 표준을 만들기 위해 2009년부터 시작된 프로젝트입니다. 처음에 CommonJS를 만든 Kevin Dangoor는 다음과 같은 자바스크립트의 문제점을 지적했습니다.

  • 자바스크립트는 모듈 시스템이 없다.
  • 자바스크립트는 표준 라이브러리가 없다.(브라우저 API, date, math만 존재함)
  • 웹 서버 또는 데이터베이스등을 위한 표준 인터페이스가 없다.
  • 자바스크립트는 의존성을 관리하고 설치할 수 있도록 해주는 패키지 관리 시스템이 없다.

CommonJS는 앞에서 지적된 자바스크립트의 문제들을 해결하기 위해 시작된 워킹그룹이라고 할 수 있는데, 특히 두 번째로 지적된 자바스크립트 표준 라이브러리가 없다는 점에 대해서는 path, fs 등이 Node.js에서 내장 API로 제공되도록 결정적인 역할을 하기도 했습니다.

CommonJS는 동기적인 특징 때문에 서버사이드에 사용하기 용이한 장점이 있습니다. 그래서 CommonJS는 Node.js에서 채택해 사용하고 있는데, 앞에서 살펴본 ESM에 핵심 키워드인 import, export가 있는 것 처럼 CommonJS에도 module.exports, exports, require의 세 가지 핵심 키워드가 있습니다.

module.exports

module 이라는 키워드는 공식 문서에 다음과 같이 정의되어 있습니다.

The module free variable is a reference to the object representing the current module.

module은 현재의 모듈을 나타내는 자유변수(예약어)라는 의미로, 결국 module이라는 키워드는 현재 모듈에 대한 정보를 담고 있는 일종의 객체라고 할 수 있습니다. 기본적인 문법은 module.exports = expression인데, 자바스크립트에서는 변수에 모든 형태의 데이터를 넣을 수 있기 때문에 module.exports도 값으로 허용되는 모든 데이터를 export 할 수 있습니다. 예를 들어, utils.js라는 모듈이 있다면 다음과 같이 사용할 수 있습니다.

utils.js
const PI = 3.14;
module.exports.PI = PI;
app.js
const utils = require('./utils');
console.log(utils.PI); // 3.14

앞의 코드와 같이 모듈을 module.exports하면 utils.js에 있는 module 객체는 다음과 같은 형식으로 데이터를 저장하게 됩니다.

Module {
  ...
  exports: { PI: 3.14 }, // exports 라는 객체 안에 PI라는 프로퍼티가 생성됨
  parent: Module {/*...*/},
  filename: '...',
  loaded: false,
  children: [],
  paths: [ /*...*/ ]
}

module.exports.PI로 데이터를 내보내면 module.exports라는 객체에 PI라는 데이터가 담기게 됩니다. 그렇다면 module.exports.property 대신 그냥 module.exports에 직접 값을 넘기면 어떻게 될까요?

당연하게도, 다음처럼 module.exports에 값이 직접 추가되기 때문에 다른 모듈을 함께 내보내는 경우에는 필요한 데이터를 가져올 수 없거나 에러가 발생하게 됩니다.

utils.js
module.exports = 3.14;
console.log(module);
Module {
  ...
  exports: 3.14, // exports에 객체가 아닌 데이터가 추가됨
  parent: Module {/*...*/},
  filename: '...',
  loaded: false,
  children: [],
  paths: [ /*...*/ ]
}

exports

exports도 module.exports와 같은 동작을 수행합니다. 하지만 약간의 차이가 있는데 Node.js의 API 문서에서는 다음과 같이 설명합니다.

exports shortcut

The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated.

exports 변수는 모듈의 파일 레벨 스코프 안에서 사용 가능하고, 모듈이 평가되기 전에 module.exports의 값을 할당받는 다는 내용으로, 다음 코드처럼 module.exportsexports는 완전히 동일한 동작을 수행하지만, 실제적으로는 exportsmodule.exports의 데이터를 참조합니다.

utils.js
exports.PI = 3.14;
console.log(module.exports === exports); // true
console.log(exports); // { PI: 3.14 }

exportsmodule.exports를 참조하고 있기 때문에 다음처럼 module.exports의 데이터를 가져오게 되는 걸 확인할 수 있습니다. 결국 module.exportsexports는 완전히 동일하다고 할 수 있습니다.

utils.js
module.exports = 3.14;
exports.PI = 3.14;
console.log(module.exports); // 3.14

위의 코드 결과는 사실 자바스크립트에서는 당연한 결과입니다. exports가 참조하는건 module.exports라는 빈 오브젝트인데, 이걸 3.14라는 데이터로 직접 덮어 씌워 버렸기 때문에 exports가 참조하던 객체는 사라져 버리고, 그냥 module.exports의 값을 참조하게 됩니다. 그래서 exportsmodule.exports와는 함께 사용하지 않는 것이 좋은데, 꼭 함께 사용해야 하는 경우라면 다음의 코드처럼 꼭 프로퍼티를 추가해 주는 것이 좋습니다.

export.PI = 3.14;
module.exports.sin = function (a) { /*...*/ }
console.log(module.exports) // { PI: 3.14, sin: function(a) { /*...*/ }

require

require는 다른 모듈 파일을 볼러오는 명령어인데, 공식 문서에서는 다음의 4가지 방식으로 모듈을 불러올 수 있다고 설명합니다.

  • File Modules
  • Folders as Modules
  • node_modules
  • Global Directory
  • File Modules

require는 ‘상대경로’에 있는 파일 중 확장자가 .js, .json, .node 인 파일을 모듈로 불러올 수 있는데, 다음 코드처럼 확장자는 생략하고 모듈의 이름만 적어주면 됩니다.

data.json
{ "data": "Hello World!" }
utils.js
module.exports.PI = 3.14;
app.js
const data = require('./data');
const utils = require('./utils');

console.log(data); // { data: "Hello World!" }
console.log(utils.PI); // 3.14
Folders as Modules

Node.js는 상대경로(./, ../)나 절대경로(/)로 모듈을 호출하면 다음과 같은 순서로 필요한 파일을 탐색하는데,첫 번째와 두 번째에서 파일 탐색에 실패하면 Cannot find module 에러를 던집니다.

  • package.json 파일에 정의된 name과 main의 값
  • 1번에서 탐색에 실패하면, 해당 폴더에 index.js 또는 index.node 파일이 있는지 확인
  • 모두 실패하면 Cannot find module 에러를 던짐

즉, 첫 번째로 프로젝트 경로의 package.json에서 namemain 프로퍼티가 없다면, 두 번째로 패키지의 경로에서 파일을 찾는 겁니다.

src/utils/index.js
// utils 폴더 아래에 index.js 파일 생성
module.exports.PI = 3.14;
src/app.js
const utils = require('./utils'); // 자동으로 ./utils 경로에 있는 index.js 파일을 탐색
console.log(utils.PI); // 3.14
node_modules

Node.js에서 상대경로(./, ../)나 절대경로(/)를 표시하지 않고 require를 호출하면 Node.js는 현재 모듈의 최상위 디렉토리에서 부터 /node_modules 폴더를 탐색합니다. 즉 경로없이 require(moduleName)를 호출하면 다음과 같은 순서로 탐색을 하는데, 이 순서는 module.paths에 정의되어 있는 프로퍼티의 값과 같습니다.

  • /home/{win_user}/project/node_modules/{module_name}.js
  • /home/{win_user}/node_modules/{module_name}.js
  • /home/node_modules/{module_name}.js
  • /node_modules/{module_name}.js
GLOBAL_DIRECTORIES

Node.js는 앞에서 이야기한 방법들로 모듈을 찾지 못하면 다음처럼 OS의 글로벌 파일 경로인 GLOBAL_DIRECTORIES를 탐색합니다. 참고로 $PREFIX는 Node.js에서 설정한 node_prefix 경로입니다.

  • $HOME/.node_modules
  • $HOME/.node_libraries
  • $PREFIX/lib/node

AMD

AMD는 다음과 같은 목적을 위해 CommonJS에서 독립한 그룹입니다.

The Asynchronous Module Definition (AMD) API specifies a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded.

AMD API 는 모듈과 종속성 파일들을 비동기적으로 로드할 수 있도록 모듈을 정의하는 매커니즘이라고 할 수 있는데, CommonJS와 ESM 모두 동기식 로딩 방식을 채택하고 있기 때문에 로드한 모듈이 아직 사용되지 않았음에도 불구하고 미리 로딩해야 한다는 단점이 있습니다.

AMD는 동적 로딩과 의존성 관리, 모듈화를 지원하는 API를 제공하는데, 다른 모듈 시스템도 의존성 관리와 모듈화는 지원하고 있기 때문에 동적 로딩이 다른 모듈 시스템과의 가장 큰 차이점이라고 할 수 있습니다.

하지만 ES6의 등장과 함께 브라우저에서도 사용 가능한 ESM 내장 모듈 시스템이 등장했고, webpack과 같은 모듈 번들러에서는 비동기적으로 모듈 로딩을 처리해 주고, 최신 Node.js는 ESM을 표준으로 도입하기 위해 .mjs도 제공하고 있는 만큼 많이 사용되고 있는 모듈 시스템은 아닙니다.

하지만 클라이언트 사이드의 자바스크립트 개발이라면 여전히 유용하게 활용될 수도 있습니다. AMD의 비동기적 모듈을 구현한 가장 유명한 스크립트는 RequireJS가 있는데, 다음과 같이 사용할 수 있습니다.

index.html
<!DOCTYPE html>
<html lang="en">
  <head>...</head>
  <body>
    <!-- require.js 로딩 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script>

    <!-- 스크립트 파일 로딩 -->
    <script src="./app.js"></script>
    </body>
</html>
utils.js
// require.js는 define의 callback 스타일로 모듈을 정의하여 내보냄
define(function() {
  return {
    add(a, b) {
      return a + b;
    },
    subtract(a, b) {
      return a - b;
    }
  };
});
app.js
// require 함수의 첫번째 인자는 불러올 모듈, 두번째 인자는 함수로 인자는 물러온 모듈의 이름임
require(["./utils"], function(utils) {
  const result = utils.add(1, 2);
  console.log(result); // 3
});

UMD

UMD는 모듈 시스템이 AMD와 CommonJS를 쓰는 두 그룹으로 나누어지면서 서로 호환이 되지 않는 문제를 해결하기 위해 제안된 방식으로, AMD나 CommonJS를 모두 사용할 수 있도록 구현한 것으로, UMD는 어떤 구현된 프로그램이라기 보다는 디자인 패턴에 더 가깝다고 볼 수 있습니다.

조금 더 세부적인 내용을 살펴보면 AMD는 define을 사용하고, CommonJS는 module.exports를 하기 때문에, 이 차이를 활용해서 어떤 방식으로도 동작할 수 있도록 다음처럼 모듈 시스템을 구현할 수 있습니다. UMD는 모듈 로더를 확인하는 즉시 실행 함수와 모듈을 생성하는 익명 함수로 구성되어 있는 것이 특징입니다.

myModule.js
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['exports', 'b'], factory);
  } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
    // CommonJS
    factory(exports, require('b'));
  } else {
    // Browser globals
    factory((root.commonJsStrict = {}), root.b);
  }
}(this, function (exports, b) {
  // 모듈 로더를 확인하는 즉시 실행하는 즉시 실행 함수
  exports.action = function () {};
}));

모듈 로더를 확인하는 즉시 실행 함수는 root(전역 범위)와 factory(모듈을 선언하는 함수)로 구성된 2개의 파라미터를 가지는데, 모듈을 생성하는 익명 함수가 즉시 실행 함수의 2번째 파라미터로 전달됩니다. 앞의 코드처럼 exportsmodule이 존재하면 CommonJS 방식으로 동작하고, define의 타입이 함수이고 define.amd가 존재하는 경우에는 AMD 방식으로 동작합니다.

모두 존재하지 않는 경우에는 root인 window 객체로 모듈을 내보내는데, 이렇게 UMD는 클라이언트 사이트에서 많이 사용되는 AMD와 서버 사이드에서 많이 사용되는 CommonJS를 모두 사용할 수 있기 때문에 각각의 모듈을 따로 만들 필요가 없다는 장점이 있습니다.

Leave a Reply