Javascript의 모듈 시스템

September 3, 2021 ˑ 11min read

thumbnail

개요#

Javascript는 언어가 발전하면서 여러 방식의 모듈 시스템을 거쳤습니다. 아주 초기에는 모듈 시스템을 지원하지 않았지만, Node.js의 등장과 ES6의 도입에 따라 점차 발전된 형태의 모듈 시스템 또한 등장하게 되었는데요. 이번 글에서는 Javascript의 모듈 시스템이 어떻게 변모해 왔는지 알아보려고 합니다.

1. 전역 스코프 사용 (1995~2000년대 초)#

Javascript가 처음 등장(1995년)했을 때는 모듈이라는 개념이 없었고, <script /> 태그를 사용하여 Javascript를 작성하거나, 파일을 로드하였습니다. 모든 코드가 전역 변수 및 함수로 선언되었기 때문에 전역스코프 오염이 발생하여 변수의 충돌이 발생하고 유지 보수에 어려움을 겪었습니다.

Html
<script src="file1.js"></script> <script src="file2.js"></script>

이러한 방식은 전역 스코프 오염으로부터 발생하는 문제 뿐만 아니라 코드의 의존 관계 파악에도 어려움이 있었습니다. 문제를 해결하기 위해 네임스페이스 패턴을 사용하는 즉시실행함수(IIFE, Immediately Invoked Function Expression) 패턴이 등장하였습니다.

Note

네임스페이스 패턴이란?

네임스페이스 패턴(Namespace Pattern)은 변수, 함수, 클래스 등의 이름 충돌을 방지하고 코드의 모듈성을 높이기 위해 특정한 네임스페이스(공간)를 정의하여 사용하는 프로그래밍 기법을 의미합니다. 대규모 어플리케이션이나 라이브러리 개발에 유용하며, 전역 스코프 오염을 방지할 수 있습니다.

2. 네임스페이스 패턴과 IIFE (2000년대 중반)#

이 시점에도 Javascript는 공식적으로 지원하는 모듈 시스템이 없었습니다. 따라서, 전역 스코프 오염과 같은 문제를 회피하기 위해 네임스페이스 패턴을 통해 문제를 해결하고자 했습니다. 네임스페이스 패턴은 관련된 기능을 한데 모아 코드 관리를 용이하게 하고, 동일한 이름의 변수나 함수가 있어도 네임스페이스가 다르다면 충돌을 회피할 수 있으며 모듈화를 통해 코드 재사용성을 높이고 유지보수를 쉽게 할 수 있다는 특징이 있습니다. 네임스패이스 패턴 중 즉시실행함수(IIFE, Immediately Invoked Function Expression) 패턴은 코드의 모듈화를 구현할 수 있을 뿐만 아니라 Closure를 이용하여 모듈 내부에서만 사용하는 변수나 함수를 구분할 수 있어 대표적으로 사용되었습니다.

Javascript
var MyModule = (function () { var privateVar = "비공개 변수"; function privateFunction() { console.log("비공개 함수"); } return { publicFunction: function () { console.log("공개 함수"); } }; })();

이러한 방식은 코드의 모듈화는 가능하지만 파일간 의존관계에 대한 파악은 여전히 어렵다는 문제점이 남아있었습니다.

3. Node.js의 등장과 CommonJS (2009년)#

2009년 Node.js가 등장하면서 CommonJS 모듈 시스템이 도입되었습니다. CommonJS는 Node.js의 기본 모듈 시스템으로 사용되며, requiremodule.exports, exports 키워드를 사용하여 모듈을 정의하고 가져옵니다. CommonJS는 동기 방식으로 모듈을 로드하며, Node.js 환경에 최적화되어 있습니다.

Javascript
// utils.js module.exports = { add: function(a, b) { return a + b; } };

CommonJS는 Node.js 런타임에 적합한 방식으로 브라우저 환경에서는 적합하지 않습니다. 우선 CommonJS는 모듈을 동기적으로 불러오기 때문에 비동기적으로 Javascript 파일을 호출하여 가져와야 하는 브라우저 환경에서는 어울리지 않습니다. 더군다나 require()문은 브라우저에서 지원하지 않기 때문에, 트랜스파일러를 통해 변환해주어야 하는 점도 신경써야하는 부분 중 하나입니다. 그리고 CommonJS는 module.exports를 통해 객체 전체를 내보내는 구조이기 때문에, 사용되지 않는 코드도 로드되어 트리쉐이킹이 불가능하다는 단점을 가지고 있습니다.

4. AMD (Asynchronous Module Definition) (2010년대 초)#

CommonJS가 브라우저 환경에서 사용하기 어려웠기 때문에, 비동기 로딩을 지원하는 AMD(Asynchronous Module Definition)가 등장했습니다. AMD의 대표적인 구현체로는 RequireJS가 있습니다. AMD에서 모듈을 정의할 때는 define() 함수를 사용합니다. define()은 인자에 따라 다양한 방식으로 호출 될 수 있는데요. 살펴볼 예시는 의존성을 가지고 있는 모듈을 선언할 때이며 아래와 같은 방식으로 구성할 수 있습니다. define-module이라는 모듈은 dependency1, dependency2 두 개의 모듈을 의존성으로 갖고 콜백함수의 인자로 전달되어 실행하고자 하는 코드를 구성할 수 있습니다.

define-module.js
define(['./dependency1', './dependency2'], function(module1, module2) { const result = module1.doSomething() + module2.doSomething(); return { getResult: function() { return result; } } })

단순히 외부 모듈을 사용하고자 한다면, require()를 통해 모듈을 의존성 배열에 명시해준 뒤, 콜백함수를 통해 모듈을 받아 사용하면 됩니다.

require-module.js
// 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 모듈 방식은 브라우저 환경에서 사용할 수 있지만, 코드가 복잡해지고 가독성이 떨어진다는 단점이 있습니다.

5. UMD (Universal Module Definition) (2010년대 중반)#

Node.js에서는 CommonJS를 주로 사용하게 되고, 브라우저에서는 AMD를 사용하게 되면서 모듈 시스템이 분열되는 시점이 다가오자 두가지 환경에서 범용적으로 사용할 수 있도록 UMD 방식의 모듈 시스템이 탄생했습니다. UMD는 AMD와 CommonJS를 모두 지원하는 방식으로, 브라우저 환경에서는 전역 객체에 모듈을 할당하고, Node.js 환경에서는 CommonJS 방식으로 모듈을 정의합니다.

Javascript
(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; } }; }));

하나의 코드 베이스로 두가지 환경에서 사용할 수 있다는 장점이 있지만, 코드의 양이 많아지면서 복잡도가 증가하는 형태가 되었습니다.

6. ES Modules(ESM, ECMAScript Modules) (2015년)#

2015년 ES6(ECMAScript 2015)에서 공식적으로 ES Modules(ESM)가 도입되었습니다. 브라우저와 Node.js에서 모두 지원되며 현재 가장 많이 사용되는 모듈 시스템입니다. ES Modules는 importexport 키워드를 사용하여 모듈을 정의하고 가져옵니다. 또한 파일 단위의 모듈 스코프를 가지며, 비동기 로딩을 지원하고 브라우저에서 HTTP/2와 잘 통합됩니다.

Javascript
// 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"을 추가하면 됩니다.

Caution

package.json에 "type": "module"을 추가하면 Node.js는 프로젝트 내 .js 파일을 ESM으로 해석합니다. 변경 이후 CommonJS 방식을 사용하려면 명시적으로 .cjs 확장자로 파일을 구성해야합니다.

또한 ESM은 import.meta 객체를 통해 모듈의 메타데이터를 제공하며, 동적 import를 지원하여 필요한 모듈을 필요한 시점에 동적으로 로드할 수 있습니다.

모듈 시스템 비교#

특징ESMCommonJSUMDAMD
로딩 방식비동기동기동기 및 비동기비동기
환경브라우저, Node.jsNode.js브라우저, Node.js브라우저
키워드import/exportrequire/exports환경에 따라 다름define/require
현재 사용성표준으로 널리 사용레거시 코드에서 사용점차 사용 감소거의 사용되지 않음

Related Articles

Github

Linkedin

Instagram