초기 자바스크립트는 모듈을 불러오거나 내보내는 방법이 없어 하나의 파일에 모든 기능를 작성해야 했다. 이런 불편함을 해결하기 위해 CJS, AMD, UMD, ESM이 등장하였고 모듈로서 사용할 수 있게 되었다.
자바스크립트를 브라우저에서 뿐만 아니라, 서버사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하기 위한 방법이다.
fileA.js
var a = 1;
b = 2;
exports.sum = function(c, d) {
return a + b + c + d;
}
fileB.js
var a = 3;
b = 4;
var moduleA = require("fileA");
moduleA.sum(a, b); // 1 + 2 + 3 + 4
exports을 사용하여 다른 모듈로 추출할 수 있으며, require로 모듈을 변수에 담을 수 있다.
CommonJS는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 한다. 즉, 서버사이드 자바스크립트 환경을 전제로 한다. 브라우저에서 이런 방식은 필요한 모듈이 모두 다운될 때까지 아무것도 할 수 없는 상황이 발생한다. (동기적)
AMD는 비동기 상황에서도 자바스크립트 모듈을 사용하기 위해 CommonJS와 독립한 그룹이다. CommonJS는 자바스크립트를 브라우저 밖으로 꺼내기 위해 탄생된 그룹이고, AMD는 브라우저에 중점을 둔 그룹이다.
fileA.js
define(["package/lib", function(lib) {
function foo() {
lib.log("hello, lib!");
}
return { foobar: foo };
});
fileB.js
require(["package/fileA"], function(fileA) {
fileA.foobar();
})
브라우저 환경의 자바스크립트는 파일 스코프가 따로 존재하지 않기 때문에 define() 함수로 파일 스코프의 역할을 대신한다. 즉, 모듈에서 사용하는 변수와 전역변수를 분리한다.
정리하자면, 필요한 파일이 모두 로컬 디스크에 있어 바로 불러쓸 수 있는 서버사이드에서는 CommonJS, 필요한 파일을 네트워크를 통해 내려받아야 하는 브라우저 환경에서는 AMD가 더 유연한 방법을 제공한다.
AMD와 CommonJS 두 그룹으로 나눠지다 보니 서로 호환되지 않는 문제가 발생한다. 이를 해결하기 위해 나온 방법으로 AMD와 CommonJS, window에 추가하는 방식까지 모두 가능한 방식이다.
// root: 전역범위, factory: 모듈을 선언하는 함수
(function (root, factory) {
// AMD 방식
if (typeof define === 'function' && define.amd) {
define(['exports', 'b'], factory);
// CommonJS 방식
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
factory(exports, require('b'));
// Browser globals (window 객체에 모듈 내보냄)
} else {
factory((root.commonJsStrict = {}), root.b);
}
}(this, function (exports, b) { // 모듈을 생성하는 익명 함수
exports.action = function () {};
}));
ECMAScript에서 지원하는 자바스크립트 공식 모듈로 import와 export를 지원하지 않는 브라우저가 많아 번들러와 함께 사용해야 한다.
import lib from "package/lib";
function foo() {
lib.log("hello, lib!");
}
export { foobar: foo };
ESM은 자바스크립트 자체 모듈 시스템으로
<html>
<body>
<script type="module" src="fileA.mjs"></script>
<script type="module" src="fileB.mjs"></script>
</body>
</html>
fileA.mjs
var x = 'fileA';
console.log(x); // fileA
// 변수 x는 전역 변수가 아니며 window 객체의 프로퍼티도 아니다.
console.log(window.x); // undefined
fileB.mjs
// import 사용 가능
import test from './test.mjs';
console.log(test); // hello word!
// 변수 x는 fileA.mjs에서 선언한 변수 x와 스코프가 다른 변수이다.
var x = 'fileB';
console.log(x); // fileB
test.mjs
const test = 'hello world!';
export default test;
type="module"
을 사용하면 해당 파일에서 import와 export 사용이 가능하다. 파일마다 독립적인 스코프를 가져 각 파일의 window는 서로 공유되지 않는다. type="module"은 아직 많은 브라우저에서 모듈 시스템을 지원하지 않기 때문에 webpack 같은 번들러를 사용하는 것이 좋다.
pdf-lib.html
<html>
<script type="module" src="pdf-lib.js"> </script>
</html>
pdf-lib.js
// path 에러
import { PDFDocument } from "pdf-lib";
// 변경
import { PDFDocument } from "/node_modules/pdf-lib/dist/pdf-lib.esm.min.js";
import fs from "fs";
// fs는 html의 브라우저 레벨에서 사용될 수 없다. FileReader() 방법을 사용한다.
// 만약 node 자체적으로 실행한다면 가능하다.
npm 패키지 중 pdf-lib을 import 하기 위해서는 절대경로를 사용할 수 없다.
파일이 실행되는 기준의 경로(상대경로)로 import 해야 path 에러가 발생하지 않는다. react와 같은 라이브러리에서 상대경로를 사용할 수 있는 이유는 webpack과 같은 번들러가 설치되어 있기 때문이다. 이때 fs와 같은 파일 시스템은 상대경로도 사용할 수 없기 때문에 브라우저에서는 FileReader() 객체를 사용한다.
[1][JavaScript] CJS, AMD, UMD, ESM
https://beomy.github.io/tech/javascript/cjs-amd-umd-esm/
[2] JavaScript 표준을 위한 움직임: CommonJS와 AMD
https://d2.naver.com/helloworld/12864
[3] 리액트 상대경로 → 절대경로 임포트
https://engineering.huiseoul.com/리액트-상대경로-절대경로-변경-1485babb5198