September 3, 2021 ˑ 14min read
JavaScript는 언어가 발전하면서 여러 방식의 모듈 시스템을 거쳤습니다. 아주 초기에는 모듈 시스템을 지원하지 않았지만 Node.js의 등장은 서버사이드 JavaScript 환경을 위한 CommonJS 모듈 시스템을 탄생시켰고, 이후 ES6 표준에서는 브라우저와 서버 모두를 아우르는 공식 모듈 시스템인 ESM이 도입되었습니다. 이번 글에서는 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를 활용하여 모듈 외부에서는 접근할 수 없는 비공개 멤버(변수, 함수)를 만들어 정보 은닉(Information Hiding)을 구현할 수 있었기 때문에 대표적으로 사용되었습니다.
var MyModule = (function () {
var privateVar = "비공개 변수";
function privateFunction() {
console.log("비공개 함수");
}
return {
publicFunction: function () {
privateFunction(); // 내부에서 비공개 함수 호출 가능
console.log("공개 함수 호출됨: " + privateVar);
}
};
})();
MyModule.publicFunction(); // 공개된 함수만 호출 가능
// MyModule.privateFunction(); // 오류: 접근 불가능
이러한 방식은 코드의 모듈화는 가능하지만 파일간 의존관계에 대한 파악은 여전히 어렵다는 문제점이 남아있었습니다.
2009년 Node.js가 등장하면서 CommonJS 모듈 시스템이 도입되었습니다. CommonJS는 Node.js의 기본 모듈 시스템으로 사용되며, require 함수로 다른 모듈을 불러오고 module.exports 객체를 통해 모듈을 외부로 내보냅니다. CommonJS는 동기 방식으로 모듈을 로드하며, 서버 환경인 Node.js에 최적화되어 있습니다.
// utils.js
const PI = 3.14;
function add(a, b) {
return a + b;
}
module.exports = {
add: add,
PI: PI
};
// main.js
const utils = require('./utils.js'); // 동기적으로 utils.js 로드
console.log(utils.add(2, 3)); // 5
console.log(utils.PI); // 3.14
NoteNode.js는
module.exports
객체 외에도 편의를 위해exports
라는 변수를 제공합니다.exports
는module.exports
를 가리키는 참조 변수이므로,exports.add = ...
와 같이 속성을 추가하는 것은 가능하지만,exports = ...
와 같이exports
자체에 새 객체를 할당하면 더 이상 모듈을 내보낼 수 없게 되므로 주의해야 합니다.
CommonJS는 Node.js 런타임에 적합한 방식이지만 브라우저 환경에서는 몇 가지 문제점이 있습니다.
우선 브라우저는 require
함수를 기본적으로 지원하지 않으므로, Babel 같은 트랜스파일러를 통해 변환 과정을 거쳐야 합니다.
CommonJS가 브라우저에서 사용 가능하더라도 모듈을 동기적으로 불러오기 때문에, 네트워크를 통해 비동기적으로 JavaScript 파일을 가져와야 하는 브라우저 환경에서는 적절하지 않을 수 있습니다.
게다가 CommonJS는 module.exports
를 통해 객체 전체를 내보내는 구조이기 때문에, 사용되지 않는 코드도 로드되어 트리쉐이킹 같은 최적화가 어렵다는 문제점도 가지고 있습니다.
CommonJS가 브라우저 환경에서 사용하기 어려웠기 때문에, 비동기 로딩을 지원하는 AMD(Asynchronous Module Definition)가 등장했습니다. AMD는 특히 브라우저 환경에서의 비동기적 요구사항을 만족시키기 위해 설계되었습니다. 대표적인 구현체로는 RequireJS 가 있습니다.
모듈을 정의하기 위해 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 모듈 방식은 브라우저 환경에서 비동기 로딩 문제를 해결했지만, CommonJS에 비해 문법이 다소 장황하고 코드 가독성이 떨어진다는 평가를 받기도 했습니다.
Node.js에서는 CommonJS를, 브라우저에서는 AMD를 주로 사용하게 되면서 라이브러리 개발자들은 두 환경 모두에서 동작하는 코드를 작성해야 하는 어려움에 직면했습니다. 이 문제를 해결하기 위해 두 환경 모두에서 범용적으로 사용할 수 있는 UMD(Universal Module Definition) 패턴이 등장했습니다.
UMD는 기본적으로 코드를 즉시실행함수(IIFE)로 감싸고, 내부에서 현재 환경이 AMD인지, CommonJS인지 확인하여 각 환경에 맞는 방식으로 모듈을 정의하거나 내보냅니다. 둘 다 아니라면 전역 객체(브라우저의 window)의 속성으로 모듈을 할당합니다.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 환경: define 함수를 사용하여 모듈 정의
define(['dependency'], factory); // 예시: 'dependency' 모듈에 의존
} else if (typeof module === 'object' && module.exports) {
// CommonJS 환경 (Node.js 등): module.exports에 모듈 할당
module.exports = factory(require('dependency')); // 예시: 'dependency' 모듈 필요
} else {
// 브라우저 전역 환경: root 객체(보통 window)에 모듈 할당
root.MyModule = factory(root.Dependency); // 예시: 전역 Dependency 객체 필요
}
}(typeof self !== 'undefined' ? self : this, function (Dependency) {
// 실제 모듈 내용 (factory 함수)
// Dependency를 사용하는 코드...
// 내보낼 객체 반환
return {
greet: function () {
return "Hello from MyModule!";
}
};
}));
UMD는 호환성 측면에서 장점이 있지만, 코드가 환경을 감지하기 위한 보일러플레이트로 인해 다소 복잡해진다는 단점이 있습니다.
2015년 ES6(ECMAScript 2015)에서 드디어 JavaScript 언어 표준으로 ES Modules(ESM)가 도입되었습니다. ESM은 브라우저와 Node.js 환경 모두에서 네이티브하게 지원하는 것을 목표로 설계되었으며, 현재 가장 널리 사용되는 모듈 시스템입니다.
ESM은 import
키워드로 다른 모듈을 가져오고, export
키워드로 모듈의 특정 기능(변수, 함수, 클래스 등)을 내보냅니다. 파일 자체가 독립적인 모듈 스코프를 가지며, 기본적으로 비동기 로딩 방식으로 동작합니다.
// utils.js
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
// main.js
import { add, PI } from './utils.js'; // 이름 지정하여 필요한 것만 가져옴
console.log(add(2, 3)); // 5
console.log(PI); // 3.14
ESM은 대부분의 모던 브라우저에서 지원됩니다(Can I Use 참고). 하지만 구형 브라우저를 지원해야 한다면 Babel과 같은 트랜스파일러를 사용하여 CommonJS나 UMD 형태로 변환하고, Webpack 등으로 번들링하는 과정이 필요할 수 있습니다.
Node.js 환경에서는 v13.2.0부터 정식으로 ESM을 지원하기 시작했습니다. 프로젝트에서 ESM을 기본 모듈 시스템으로 사용하려면 package.json 파일에 "type": "module"
설정을 추가해야 합니다.
Cautionpackage.json에
"type": "module"
을 추가하면 Node.js는 프로젝트 내.js
파일을 ESM으로 해석합니다. 변경 이후 CommonJS 방식을 사용하려면 명시적으로.cjs
확장자로 파일을 구성해야합니다.
또한 ESM은 모듈에 대한 메타 정보를 담고 있는 import.meta 객체를 제공하며, import()
함수를 이용한 동적 임포트(Dynamic Import)도 지원하여 필요할 때 모듈을 로드하는 코드 스플리팅(Code Splitting) 을 용이하게 합니다.
특징 | ESM | CommonJS | UMD | AMD |
---|---|---|---|---|
로딩 방식 | 비동기 | 동기 | 동기 및 비동기 | 비동기 |
환경 | 브라우저, Node.js | Node.js | 브라우저, Node.js | 브라우저 |
키워드 | import /export | require /exports | 환경에 따라 다름 | define /require |
지금까지 JavaScript 모듈 시스템이 초기의 전역 스코프 방식에서 IIFE 패턴을 거쳐 CommonJS, AMD, UMD, 그리고 마침내 표준으로 자리 잡은 ESM까지 발전해 온 과정을 살펴보았습니다. 각 시스템은 당시의 문제점을 해결하기 위해 등장했으며, 현재는 ESM이 브라우저와 Node.js 양쪽에서 모던 JavaScript 개발의 표준 모듈 시스템으로 널리 사용되고 있습니다. 비록 구형 환경 지원이나 번들 최적화를 위해 여전히 번들링 및 트랜스파일링 과정이 필요할 수 있지만, ESM의 도입은 JavaScript 생태계의 파편화를 줄이고 개발 경험을 크게 향상시켰다고 볼 수 있습니다.
Chrome Extension
Mar 4, 2024
20 min read
크롬, 파이어폭스, 엣지, 오페라와 같은 주요 브라우저는 기능을 확장하거나 수정할 수 있는 확장 프로그램을 개발할 수 있는 환경과 API를 제공합니다. 확장 프로그램은 웹페이지의 UI를 변경하거나, 브라우저 이벤트를 감지하여 필요한 동작을 수행하는 등 다양한 응용이 가능한데요. 이번 글에서는 크롬 브라우저의 확장 프로그램을 개발하기에 앞서 확장 프로그램의 구성에 필요한 파일들과 그 역할에 대해 알아보려고 합니다.
Chrome Extension
May 4, 2024
15 min read
Chrome, Firefox, Edge, Opera 등 주요 브라우저는 기능을 확장하거나 수정할 수 있는 확장 프로그램을 개발할 수 있는 환경과 API를 제공합니다. 확장 프로그램은 웹페이지의 UI를 변경하거나, 브라우저 이벤트를 감지하여 필요한 동작을 수행하는 등 다양한 응용이 가능한데요. 이번 글에서는 크롬 브라우저의 확장 프로그램을 개발하기에 앞서 확장 프로그램에 필요한 파일 구성과 그 역할에 대해 알아보려고 합니다.