JavaScript - CommonJS, AMD, UMD, ESM

 

모듈(Module)이란?

개발한 프로그램의 크기가 커지면 하나의 파일에 함수를 모아두지 않고 여러 파일로 분리하게 된다. 이때 분리된 파일을 모듈이라고 한다. 보통 클래스 하나 혹은 특정한 목적을 가진 복수의 함수로 구성된 라이브러리 하나로 구성된다.

:heavy_check_mark: 모듈화 장점

  • 자주 사용되는 코드를 별도의 파일로 만들어서 필요할 때마다 호출하여 재사용가능
  • 의존성을 줄여주기 때문에 유지보수에 용이
  • 코드를 용도별로 모아두기 때문에 필요한 로직을 빠르게 찾음
  • 필요한 로직만을 로드해서 메모리의 낭비를 줄임
  • 한번 다운로드 된 모듈은 웹브라우저에 저장되어 동일한 로직을 로드 할 때 시간과 네트워크 트래픽 절약. (브라우저에서만 해당)
    모듈 시스템인 CommonJS, AMD, UMD, ESM를 알아보자

CommonJS

JavaScript의 공식 스펙이 브라우저만 지원했기 때문에 이를 서버사이드 및 데스크탑 어플리케이션에서 지원하기 위한 그룹이다. 서버사이드의 대표적으로 Nodejs가 있고 Nodejs는 기본적으로 CommonJS의 규칙을 따른다.

:white_check_mark: export

export 방식으로는 2가지가 있다. print라는 함수를 export해보자

const print = () => {console.log('export');};

1. module.exports

module.exports = { print };

여기서 module은 현재 모듈을 나타내는 자유 변수, 빈 객체로 초기화되어있는 상태
즉, 현재 모듈에 대한 정보를 담은 일종의 빈 Object({})라는 뜻이다.

2. exports

exports.print = print;

정리

  • module.exports 는 빈 객체를 참조한다.
  • exportsmodule.exports 를 참조한다.
  • require 는 항상 module.exports 를 리턴받는다.

exports 가 call by reference로 module.exports를 바라보고 있고, 리턴값은 module.exports이다.
module.exports, exports 모두 하나의 객체를 바라보고 있어서 exports에 멤버를 추가하면 module.exports에도 추가된다.

:white_check_mark: import

const p = require('./print.js');
p.print();

위에 export코드가 print.js에 있다고 치면 require('./print.js')로 import하여 print()함수를 사용할 수있다.

AMD(Asynchronous Module Definition)

CommonJS 그룹에서 의견이 맞지 않아 만든 비동기 모듈에 대한 표준안을 다루는 그룹이다.
브라우저에서는 모든 모듈이 다 로딩될 때까지 기다릴 수 없기 때문에 비동기 모듈 로딩방식으로 구현을 해놓았다.
CommonJS가 서버쪽에서 장점이 많은 반면에 AMD는 브라우저 쪽에서 더 큰 효과가 있다.
대표적인 모듈시스템으로 RequireJS가 있다.

  • 모듈 정의 Module Define / 모듈 호출 Module Require

index.html

<script data-main="index.js" src="require.js"></script>

require.js 다운받아 위에처럼 import해주면 require.js가 로드되자마자 index.js가 실행된다.

sub.js

// 모듈 정의
define(() => {
  return {
    print: () => console.log('a')
  }
});

define() 을 통해서 정의된다. 여기서도 require() 에서 의존성 모듈을 설정해준 것처럼 콜백함수가 실행되기 전에 로드되어야 할 모듈들을 지정해줄 수 있다.

index.js

require.config({
  baseUrl: '/',
  paths: {
    jquery: 'lib/jquery/jquery-1.7min',
    sub : 'lib/sub'
  }
});

requireJs의 기본설정 부분이다. baseUrl는 파일이 있는 기본경로를 설정한다. paths의 특정 라이브러리 경로를 선언한다.

// 모듈 호출
require(['jquery',sub], ($,sub) => {
    $(function(){
        sub.print();
    });
});

require통해 jquery가 로드된 후에 $에 jquery모듈이 주입되어 코드가 실행된다.
sub.js도 주입되어 print()를 사용할 수 있다.

UMD(Universal Module Definition)

AMD, CommonJS, window에 추가하는 방식에까지 모두 적용할 수 있는 모듈 패턴이다.

// module.js
// 모듈 선언을 위해 IIFE 패턴을 사용하고 있다.
// 이 예제에서는 의존 모듈을 표현하기 위해 임의로 모듈 'b'를 사용한다
// root 파라미터는 실행 환경에 따라 브라우저의 window 또는 node.js의 global을 가리킨다.
// factory 파라미터는 모듈 코드를 감싸고 있는 함수를 가리킨다.
(function (root, factory) {

 	// AMD에서 사용하는 define 함수를 사용할 수 있는지 확인한다.
  if (typeof define === 'function' && define.amd) {
    // AMD 포맷에 따라 모듈을 선언한다. 의존 모듈은 b가 된다.
    define(['b'], factory);

    // 의존 모듈이 없다면 아래와 같이 작성한다
    // define([], factory);

	// AMD를 지원하지 않는다면 CommonJS 모듈을 사용할 수 있는지 확인한다.
  } else if (typeof module === 'object' && module.exports) {
		// CommonJS 포맷에 따라 모듈을 선언한다.
		// 의존 모듈을 factory 함수의 파라미터로 전달해준다.
    module.exports = factory(require('b'));

    // 의존 모듈이 없다면 아래와 같이 작성한다
    // module.exports = factory();
  } else {
    // AMD도 CommonJS도 아니라면 브라우저라고 판단한다.
    // 여기서 root는 window가 된다. returnExports는 전역에서 모듈의 이름이다.
    root.returnExports = factory(root.b);

    // 의존 모듈이 없다면 아래와 같이 작성한다
    // root.returnExports = factory();
  }

// 함수를 즉시 실행한다.
// 첫번째 파라미터는 전역 변수.
// (여기서는 Web Worker 지원을 위해 self 변수를 확인하지만 그럴 필요가 없다면 this만 전달해도 된다)
// 두번째 파라미터는 factory에 해당한다.
}(typeof self !== 'undefined' ? self : this, function (b) { // 의존 모듈 b
	// return하는 값이 이 모듈이 내보내는 값이 된다(이 예제에서는 객체).
  return {};
}));

AMD는 define, CommonJS는 module.exports를 사용한다는 점을 이용하는 것이다.
이렇게 되면 각각의 환경에서 모두 모듈개념을 사용할 수 있게 된다.

ESM(ECMAScript Module)

import 와 export 구문을 사용하는 방식으로 ES6부터 지원되는 자바스크립트 자체 모듈 시스템이다. 하지만 모든 브라우저가 지원하는 것이 아니기 때문에 Babel을 이용하여 변환한다.

:white_check_mark: export

1. named export

선언된 변수명 그대로 export 한다. named export 는 모듈 내에 여러 개 존재할 수 있다.

  • 변수 선언 즉시 내보내기
    export let name1;
    export function name4 () {/*...*/}
    export class MyClass {/*...*/}
    
  • 변수 먼저 정의하고, 모아서 내보내기
    const var1;
    let var2;
    export { var1, var2 }
    
  • 먼저 정의된 함수를 별칭으로 변경해서 내보내기
    let var4;
    export { var4 as var5 } // 다른 모듈에서 import 할 때에는 var5 로 import 해야 함
    

2. default export

default export 는 모듈에서 하나만 존재할 수 있다.
export default { let, const, var } 와 같은 건 안된다.

export default expression;
export default function () {/*...*/} // 익명함수
export default function myFunction () {/*...*/} // 기명함수
export default class {/*...*/}
export default class MyClass {/*...*/}
export default function* () {/*...*/} // 제너레이터도 동일

// Uncaught SyntaxError: Unexpected token const
export default const test = /*...*/

// named export 처럼 묶어 내보내기도 가능합니다
const myModule = {/*...*/}
const var1 = () => {}
export { myModule as default, var1 } // as 를 이용해 default export 하였습니다

:white_check_mark: 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 방식들이다.
*를 통해 한번에 import할 수 있고 alias를 통해 별칭을 지정해서 코드에서 사용할 수 있다.



참고자료 : https://velog.io/@syoung125/%EA%B0%9C%EB%85%90%EA%B3%B5%EB%B6%80-1.-Javascript
참고자료 : https://baeharam.netlify.app/posts/javascript/module
참고자료 : https://velog.io/@zeros0623/JavaScript-ModuleCommonJS-Nodejs-RequireJS-AMD-ESM-UMD
참고자료 : https://blog.rhostem.com/posts/2019-06-23-universal-module-definition-pattern
참고자료 : https://velog.io/@doondoony/JavaScript-Module-System#-es6-modulesesm