September 3, 2021 ˑ 11min read
Javascript는 언어가 발전하면서 여러 방식의 모듈 시스템을 거쳤습니다. 아주 초기에는 모듈 시스템을 지원하지 않았지만, Node.js의 등장과 ES6의 도입에 따라 점차 발전된 형태의 모듈 시스템 또한 등장하게 되었는데요. 이번 글에서는 Javascript의 모듈 시스템이 어떻게 변모해 왔는지 알아보려고 합니다.
Javascript가 처음 등장(1995년)했을 때는 모듈이라는 개념이 없었고, <script />
태그를 사용하여 Javascript를 작성하거나, 파일을 로드하였습니다. 모든 코드가 전역 변수 및 함수로 선언되었기 때문에 전역스코프 오염이 발생하여 변수의 충돌이 발생하고 유지 보수에 어려움을 겪었습니다.
<script src="file1.js"></script>
<script src="file2.js"></script>
이러한 방식은 전역 스코프 오염으로부터 발생하는 문제 뿐만 아니라 코드의 의존 관계 파악에도 어려움이 있었습니다. 문제를 해결하기 위해 네임스페이스 패턴을 사용하는 즉시실행함수(IIFE, Immediately Invoked Function Expression)
패턴이 등장하였습니다.
Note네임스페이스 패턴이란?
네임스페이스 패턴(Namespace Pattern)은 변수, 함수, 클래스 등의 이름 충돌을 방지하고 코드의 모듈성을 높이기 위해 특정한 네임스페이스(공간)를 정의하여 사용하는 프로그래밍 기법을 의미합니다. 대규모 어플리케이션이나 라이브러리 개발에 유용하며, 전역 스코프 오염을 방지할 수 있습니다.
이 시점에도 Javascript는 공식적으로 지원하는 모듈 시스템이 없었습니다. 따라서, 전역 스코프 오염과 같은 문제를 회피하기 위해 네임스페이스 패턴을 통해 문제를 해결하고자 했습니다. 네임스페이스 패턴은 관련된 기능을 한데 모아 코드 관리를 용이하게 하고, 동일한 이름의 변수나 함수가 있어도 네임스페이스가 다르다면 충돌을 회피할 수 있으며 모듈화를 통해 코드 재사용성을 높이고 유지보수를 쉽게 할 수 있다는 특징이 있습니다. 네임스패이스 패턴 중 즉시실행함수(IIFE, Immediately Invoked Function Expression)
패턴은 코드의 모듈화를 구현할 수 있을 뿐만 아니라 Closure를 이용하여 모듈 내부에서만 사용하는 변수나 함수를 구분할 수 있어 대표적으로 사용되었습니다.
var MyModule = (function () {
var privateVar = "비공개 변수";
function privateFunction() {
console.log("비공개 함수");
}
return {
publicFunction: function () {
console.log("공개 함수");
}
};
})();
이러한 방식은 코드의 모듈화는 가능하지만 파일간 의존관계에 대한 파악은 여전히 어렵다는 문제점이 남아있었습니다.
2009년 Node.js가 등장하면서 CommonJS 모듈 시스템이 도입되었습니다. CommonJS는 Node.js의 기본 모듈 시스템으로 사용되며, require
와 module.exports
, exports
키워드를 사용하여 모듈을 정의하고 가져옵니다. CommonJS는 동기 방식으로 모듈을 로드하며, Node.js 환경에 최적화되어 있습니다.
// utils.js
module.exports = {
add: function(a, b) {
return a + b;
}
};
CommonJS는 Node.js 런타임에 적합한 방식으로 브라우저 환경에서는 적합하지 않습니다. 우선 CommonJS는 모듈을 동기적으로 불러오기 때문에 비동기적으로 Javascript 파일을 호출하여 가져와야 하는 브라우저 환경에서는 어울리지 않습니다. 더군다나 require()
문은 브라우저에서 지원하지 않기 때문에, 트랜스파일러를 통해 변환해주어야 하는 점도 신경써야하는 부분 중 하나입니다. 그리고 CommonJS는 module.exports
를 통해 객체 전체를 내보내는 구조이기 때문에, 사용되지 않는 코드도 로드되어 트리쉐이킹이 불가능하다는 단점을 가지고 있습니다.
CommonJS가 브라우저 환경에서 사용하기 어려웠기 때문에, 비동기 로딩을 지원하는 AMD(Asynchronous Module Definition)가 등장했습니다. AMD의 대표적인 구현체로는 RequireJS 가 있습니다.
AMD에서 모듈을 정의할 때는 define()
함수를 사용합니다. define()
은 인자에 따라 다양한 방식으로 호출 될 수 있는데요. 살펴볼 예시는 의존성을 가지고 있는 모듈을 선언할 때이며 아래와 같은 방식으로 구성할 수 있습니다.
define-module
이라는 모듈은 dependency1
, dependency2
두 개의 모듈을 의존성으로 갖고 콜백함수의 인자로 전달되어 실행하고자 하는 코드를 구성할 수 있습니다.
define(['./dependency1', './dependency2'], function(module1, module2) {
const result = module1.doSomething() + module2.doSomething();
return {
getResult: function() {
return result;
}
}
})
단순히 외부 모듈을 사용하고자 한다면, require()
를 통해 모듈을 의존성 배열에 명시해준 뒤, 콜백함수를 통해 모듈을 받아 사용하면 됩니다.
// requirejs 설정을 통해 jquery 모듈 로드
requirejs.config({
enforceDefine: true,
paths: {
jquery: 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min'
}
});
// 의존성 이름을 의존성 배열에 추가하여 모듈 로드
require(['jquery'], function ($) {
// jquery 모듈 로드 후 실행할 코드
}, function (error) {
// 모듈 로드 실패 시 실행할 코드
})
AMD 모듈 방식은 브라우저 환경에서 사용할 수 있지만, 코드가 복잡해지고 가독성이 떨어진다는 단점이 있습니다.
Node.js에서는 CommonJS를 주로 사용하게 되고, 브라우저에서는 AMD를 사용하게 되면서 모듈 시스템이 분열되는 시점이 다가오자 두가지 환경에서 범용적으로 사용할 수 있도록 UMD 방식의 모듈 시스템이 탄생했습니다. UMD는 AMD와 CommonJS를 모두 지원하는 방식으로, 브라우저 환경에서는 전역 객체에 모듈을 할당하고, Node.js 환경에서는 CommonJS 방식으로 모듈을 정의합니다.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 환경
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 환경
module.exports = factory();
} else {
// 브라우저 환경 (전역 변수)
root.MyModule = factory();
}
}(this, function () {
return {
add: function (a, b) {
return a + b;
}
};
}));
하나의 코드 베이스로 두가지 환경에서 사용할 수 있다는 장점이 있지만, 코드의 양이 많아지면서 복잡도가 증가하는 형태가 되었습니다.
2015년 ES6(ECMAScript 2015)에서 공식적으로 ES Modules(ESM)가 도입되었습니다. 브라우저와 Node.js에서 모두 지원되며 현재 가장 많이 사용되는 모듈 시스템입니다. ES Modules는 import
와 export
키워드를 사용하여 모듈을 정의하고 가져옵니다. 또한 파일 단위의 모듈 스코프를 가지며, 비동기 로딩을 지원하고 브라우저에서 HTTP/2와 잘 통합됩니다.
// utils.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './utils.js';
console.log(add(2, 3));
ESM은 Javascript 진영에서 공식적으로 지원하는 모듈 시스템이지만, Can I Use 에서 찾아본 ESM을 지원하는 브라우저 버전에 따르면, 2017년 이전 브라우저를 대상으로 웹 어플리케이션을 개발해야 하는 경우 바벨 같은 트랜스파일러를 통해 CommonJS나 UMD로 변환해야 합니다.
Node.js 환경에서는 아직까지 CommonJS가 사용되는 경우가 많지만 점차 ESM으로 넘어오는 추세이며, 프로젝트에서 ESM을 사용하려면 package.json 파일에 "type": "module"
을 추가하면 됩니다.
Cautionpackage.json에
"type": "module"
을 추가하면 Node.js는 프로젝트 내.js
파일을 ESM으로 해석합니다. 변경 이후 CommonJS 방식을 사용하려면 명시적으로.cjs
확장자로 파일을 구성해야합니다.
또한 ESM은 import.meta 객체를 통해 모듈의 메타데이터를 제공하며, 동적 import를 지원하여 필요한 모듈을 필요한 시점에 동적으로 로드할 수 있습니다.
특징 | ESM | CommonJS | UMD | AMD |
---|---|---|---|---|
로딩 방식 | 비동기 | 동기 | 동기 및 비동기 | 비동기 |
환경 | 브라우저, Node.js | Node.js | 브라우저, Node.js | 브라우저 |
키워드 | import /export | require /exports | 환경에 따라 다름 | define /require |
현재 사용성 | 표준으로 널리 사용 | 레거시 코드에서 사용 | 점차 사용 감소 | 거의 사용되지 않음 |
typescript
May 1, 2023
6 min read
궁금했던 typescript의 moduleResolution 옵션. bundler는 처음보는데? 그래서 알아봤습니다!
javascript
Oct 4, 2021
5 min read
ESM을 지원하는 브라우저와 지원하지 않는 브라우저를 위한 코드를 나누어주는 module/nomodule 패턴에 대해 알아봅니다.
javascript
May 2, 2021
8 min read
다수의 비동기 함수를 효율적으로 실행하기 위해 사용하는 Promise의 정적 메소드에 대해 알아보았습니다.