[JS 톺아보기] Javascript에서 module이란? 모듈 시스템의 비교까지(CommonJS, AMD, UMD, ESM) (with. 모던 JS Deep Dive)

2025. 2. 4. 18:07·📝 공부 기록/Javascript

 

모던 자바스크립트 Deep Dive 48.2 자바스크립트와 모듈 & 48.3 ES6 모듈(ESM) (P. 894)


“(…생략) 이로써 자바스크립트의 모듈 시스템은 크게 CommonJS와 AMD 진영으로 나뉘게 되었고 브라우저 환경에서 모듈을 사용하기 위해서는 CommonJS 또는 AMD를 구현한 모듈 로더 라이브러리를 사용해야 하는 상황이 되었다. (…생략) 이러한 상황에서 ES6에서는 클라이언트 사이드 자바스크립트에서도 동작하는 모듈 기능을 추가했다.”

 

모던 자바스크립트 Deep Dive 교재에 따르면, 자바스크립트의 모듈 시스템은 CommonJS와 AMD이 먼저 등장하여 사용되어왔었고, 뒤이어 ES6에서 Module 기능이 추가된 것을 알 수 있다. CommonJS와 AMD 방법에 대해서는 자세히 나와있지 않았기에 이에 대해 알아보면서 javascript에서 module이 필요한 이유 즉, 등장할 수 밖에 없었던 배경과 이를 다루기 위해 나타난 모듈 시스템, 그리고 모듈 시스템이 등장하기 전에 개발자들이 시도했던 방법들까지 알아보려고 한다.

 

1. 모듈은 왜 필요할까?

내가 hello world와 <html></html>를 처음 작성해본 것은 2020년이었기에 그 이후 배워가며 접했던 Javascript 코드들은 이미 ES6 문법이 널리 쓰이고도 남는 시기였다. 따라서 나는 javascript에서 왜 그러한 코드들이 쓰이기 시작했는지 흐름을 거꾸로 올라가며 공부하곤 했다. javascript의 탄생은 많은 사람들이 알고있다시피 웹 페이지의 간단한 기능을 구현해보기위함이었다. 따라서 다음과 같이 .html 파일 내에서 javascript 코드를 작성하거나, 그 코드가 길어질 때면 따로 .js 파일을 만들어서 .html 파일에서 불러오는 것으로 시작하였다.

 

.html 파일에서 .js 파일 불러오기

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>hello world!</title>
		<script src='a.js'></script>
		<script src='b.js'></script>
		<script>
			console.log('number: ', number); // 20 || 10
		</script>
	</head>
</html>
// a.js
var number = 10
// b.js
var number = 20

 

 

다음과 같은 환경에서의 장점이자 단점은 모두 다 단 하나의 전역 스코프를 공유한다는 점이다. 즉, js 파일을 아무리 만들어도 불러오기만 한다면 .html 파일에서도 그 변수를 사용할 수 있는 것이다. 하지만 언제나 그렇듯! 프로젝트가 복잡해지면서 이는 곧 큰 재앙이 된다. Naver 포털 사이트만 예시로 들더라도 뉴스, 메일, 블로그, 카페 등 수많은 기능이 있고 이를 모두 전역 스코프에서 다룬다면? 끔찍한 상황이 벌어질 것이다. 수많은 변수와 함수 등의 코드들이 서로에게 영향을 주고 받는 상황이 생기기도 할 것이다. 혹여나 협업을 하는 경우에는 이것이 더 심했을 것이다. 따라서 서로 다른 스코프를 가지는 환경을 만들기 위해 모듈이라는 개념이 나왔다. 좀 더 자세히 말하자면, 전역 스코프와 분리된 스코프를 사용하려고 했다! 지금은 export/import를 너무 당연하게 사용하고 있지만 이러한 모듈 시스템이 등장하기 전, 개발자들은 어떠한 방법들을 썼을까?

 

2. 모듈 시스템이 등장하기 전

자바스크립트에서 전역 스코프와 분리된 개별 스코프는 어떤 것들이 있을까? 바로 객체이다. 함수 또한 객체이기에 개발자들은 객체나 함수에 독자적으로 사용하고 싶은 변수나 함수 코드들을 넣고 사용하였다. 여기서 여러가지 유명한 패턴들이 등장한다.

 

네임 스페이스 패턴(NameSpace Pattern)

전역 스코프를 피하기 위해 한 객체에 온갖 변수, 함수, 객체들을 넣어놓는 방법이다. 각 변수, 함수, 객체 이름은 곧 하나의 프로퍼티, 즉 공간이 되므로 충돌되지도 않고 전역 스코프와는 다른 스코프를 사용하기에 전역 공간을 사용하지 않는다는 장점이 있다.

let Naver = {}
Naver.news = {}
Naver.news.view = function(){}
Naver.mails = {}
Naver.blog = {}

 

 

즉시 실행 함수 패턴(IIFE Pattern, Immediately Invoked Function Expression Pattern)

함수 또한 객체이다. 따라서 함수 스코프는 전역 스코프와는 다른 스코프로 작동되기에 즉시 실행 함수에 코드들을 넣어서 전역 스코프로부터 보호하려는 코드도 많이 쓰여졌다. 이 또한 전역 공간을 전혀 사용하지 않기에 서로 다른 js 코드들에 의해 영향을 받지 않았다. 즉시 실행 함수를 module이라는 변수에 할당하여 module처럼 접근하는 패턴을 사용하였다.

const module = (function (){
    let News = {}
    News.view = function (){
    	console.log('뉴스 보는중...');
    }
})()

 

 

전역 스코프와 완벽하게 분리는 되었지만, 다음과 같은 코드들은 문제가 하나 있다. 바로 반대로 전역공간에서 사용하고 싶은 변수나 함수가 있을 경우에는 전혀 접근할 수가 없다는 것이다. 따라서 개발자들은 return문에 공유하고싶은 변수나 함수를 넣은 객체를 넣어 클로저를 사용하여 마치 독자적인 스코프를 갖고 있지만 외부에서도 접근이 가능한 module처럼 사용했었다.

 

const module = (function (){
    let News = {}
    News.view = function (){
    	console.log('뉴스 보는중...');
    }
    return {viewNews: News.view}
})()

module.viewNews()

 

 

이렇게 점점 더 복잡한 기능을 위한 자바스크립트 코드를 작성하던 중에 2008년, 구글에서 개발한 자바스크립트 엔진인 V8이 등장하였다. 이를 사용하여 2009년에 브라우저 밖에서도 동작이 가능한 Javascript인 NodeJS가 등장하였다. 

 

 

 

3. 모듈 시스템의 등장

이렇게 서버사이드에서도 작동되는 Javascript 런타임 환경 NodeJS가 등장하면서 드디어 module를 사용할 수 있는 첫 모듈 시스템이 나왔다. 그것은 바로 CommonJS이다.

 

CommonJS(2009년) : NodeJS 환경에서 사용할 수 있는 모듈 시스템 (서버사이드 JS)

실제 서버의 폴더에 접근하여 파일을 읽어오고 또 내보낼 때 사용하고 있는 모듈 시스템이다. module을 정의하고 이를 위해 파일(module)별로 독자적인 스코프를 제공하는 프로젝트? 스펙?이다. NodeJS에서 채택하여 사용중이다. 

큰 특징이 있다면 동기적이라는 것이다. 서버에서는 직접 로컬 디스크을 통해 파일에 접근할 수 있기때문에 그 실행 시간이 짧기 때문에 동기적으로 동작하여도 문제 없다. 브라우저 환경에서는 많은 이벤트 처리와 렌더링을 위해 비동기로 동작하는 경우가 많지만, 서버에서는 동기적으로 동작하여도 실행 시간이 짧기에 사용자 경험을 염두에 두고 비동기로 작성하지 않아도 된다. 하지만 클라이언트사이드(브라우저)에서는 서버의 폴더에 직접 접근할 수 없기때문에 서버사이드 Javascript를 위한 모듈 시스템이다. CommonJS를 사용하고 있는 대표적인 구현체는 NodeJS가 있다.

 

사용법은 module.exports와 require를 사용한다.

// news.js
fuction viewNews() {
	console.log('뉴스 보는 중..');
}

module.exports = {
	viewNews: viewNews
}

// main.js
const viewNews = require('./news');
viewNews()

 

 

아쉽게도 브라우저에서는 '유용하게' 사용할 수 없는 모듈 시스템이다. 그 당시 브라우저에서도 사용할 수 있는 모듈 시스템이 없었기에 CommonJS를 브라우저에서도 사용했던 개발자들이 있었지만 확실하게 무리였다.

 

?? : 브라우저에서도 사용하게끔 최적화 해주는 건 어때.....?

CommonJS : Nope.

 

 

 

아직 브라우저에 최적화된 비동기형 모듈 시스템이 등장하지않아서 개발자들이 아쉬워하던 도중, CommonJS 그룹 내부에서도 비슷한 얘기들이 나왔었다. 모듈 시스템을 비동기적으로, 브라우저에서 네트워크로 파일을 받을 수 있도록 구현할 수는 없을까? 라는 고민을 똑같이 하고 있던 CommonJS에서 일부 개발자들이 나와서 만들어진 그룹이 바로 AMD그룹이다.


AMD(Asynchronous Module Definition, 2009) : 브라우저에서도 사용할 수 있는 모듈 시스템 (클라이언트사이드 JS)

브라우저에서는 동기적으로 모듈들의 로딩을 기다릴 수 없기에 비동기적으로 모듈을 로딩해와야했고, 직접 로컬 디스크에 접근할 수 있는 서버사이드 JS와 달리 네트워크를 통해 원격으로 리소스에 접근할 수 있도록 나와 클라이언트사이드 JS에 적합하다. (물론 서버사이드에서도 호환이 되지만 최적화된 API는 아니다.) 브라우저에서도 모듈을 사용할 수 있게끔 하기 위해 나온 스펙이자 라이브러리이다. 

 

AMD API Spec

 

amdjs-api/AMD.md at master · amdjs/amdjs-api

Houses the Asynchronous Module Definition API. Contribute to amdjs/amdjs-api development by creating an account on GitHub.

github.com

 

 

CommonJS에서 탄생하여 분리된 그룹인만큼 문법 또한 비슷한 점이 많다. require를 사용하는 것은 동일하나 CommonJS와 다르게 define 함수를 사용한다는 차이점이 있다. 브라우저 환경에서는 파일 스코프가 따로 존재하지 않기에 파일 스코프의 역할을 위하여 등장한 함수이다. 일종의 NameSpace 역할을 하여 모듈 스코프와 전역 스코프를 분리할 수 있다. 

 

<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>AMD Spec</title>
        <!-- data-main 에는 require.js가 로드된 후 실행할 자바스크립트 파일 경로를 넣어준다. -->
        <script data-main="js/app.js" src="scripts/require.js"></script>
    </head>
    <body>
        <h1>AMD Project</h1>
    </body>
</html>
// module.js
define(function() {
  return {
    viewNews() {
    	console.log('뉴스 보는 중..');
    }
    clippingNews(){
    	conosle.log('뉴스 스크랩하는 중..')
    }
  };
});

// app.js
require(['module'], function (module) {
  module.viewNews()
  module.clippingNews()
});

 

 

AMD를 사용한 가장 대표적인 구현체가 require.js이다. require.js 공식사이트에서는 왜 AMD 스펙을 선택하였는지에 대해 다루고 있다.

requirejs.org

 

이렇게 Javascript만으로 클라이언트뿐만 아니라 서버까지 모두 구현이 가능해지면서 생태계가 더더욱 커지고 있었다. 따라서 CommonJS와 AMD를 모두 사용할 수 있는 스펙이 필요해지면서 등장한 것이 UMD이다.


UMD(Universal Module Definition, 2009) : 서버사이드, 클라이언트사이드 JS 모두 지원하는 모듈 시스템

CommonJS와 AMD를 둘 다 사용할 수 있는 호환성을 제공해주는 모듈 시스템이다. API 명세 사이트에서는 AMD with simple Node/CommonJS adapter로 표현하고 있다. 

 

UMD API Spec

 

 

GitHub - umdjs/umd: UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere.

UMD (Universal Module Definition) patterns for JavaScript modules that work everywhere. - umdjs/umd

github.com

 

UMD는 모듈을 정의하기 위해 IIFE 패턴을 사용하고 있다. 

IIFE 코드에는 root와 factory, 두 개의 파라미터가 있는데 root 파라미터는 실행 환경에 따라 window(브라우저) 또는 global(node.js)를 가리킨다. factory 파라미터는 모듈 코드를 감싸고 있는 함수를 가리킨다. 아래 코드는 rhostem 블로그를 참고하였다.

// 이 예제에서는 의존 모듈을 표현하기 위해 임의로 모듈 'b'를 사용한다
(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 {};
}));

 

 

그리고 드디어 ECMAScript2015(ES6)에서 modules를 내놓았다. 이를 ES Module이라고 하며 줄여서 ESM으로 일컫는다.


ESM(ES Module, 2015) : javascript 자체의 모듈 시스템

프론트엔드 개발자라면 한 번쯤은 사용해봤을 export/import 문이 바로 ESM의 대표 문법이다. 클라이언트 사이드 js와 서버 사이드 js 모두 사용할 수 있기에 비동기적으로 동작한다. 또한 자바스크립트 문법이기에 작성할 때 올바른 코드인지 정적 분석이 가능하다. 독자적인 모듈 스코프를 갖고 있으며 다음과 같이 사용하여 외부에 공개하고 싶은 변수, 함수, 코드들을 내보내고 불러올 수 있다.

 

 

export/import 문 사용

/* News.js */
export const viewNews = '뉴스 보는 중..'

/* index.js */
import {viewNews, test} from './News.js';
console.log(viewNews) // '뉴스 보는 중..'

 

 

모듈 스크립트 (type="module") 사용

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>hello world!</title>
		<script type="module" src='a.js'></script>
		<script src='b.js'></script>
		<script>
			console.log('number: ', number); // 10
			// 더이상 a.js는 같은 전역 스코프를 공유하고 있지 않는다.
		</script>
	</head>
</html>

 

위에서 나온 코드(이하 모듈 스크립트)는 모듈 스코프를 독자적으로 만들어 사용할 수 있지만 여러 문제점이 존재한다. 대표적인 문제는 CORS 오류가 발생한다는 것이다. 모듈 스크립트를 사용할 경우 동일 출처 정책(SOP)이 적용되지 않는다고 한다. 이는 자바스크립트 모듈 보안 요구사항으로 인해 발생한 것이며, 모듈 스크립트를 사용한 경우 /경로/index.html 에서 null/js/app.js 를 불러온 것처럼 그 출처가 null로 설정되기때문이다. 따라서 모듈 스크립트를 사용할 때에는 반드시 CORS를 적용하여 동일한 출처임을 밝혀야 한다.

 

 

이후에도 많은 브라우저에서 모듈 스크립트를 지원하고, 개발자들은 ESM과 CommonJS를 선택하여 사용하면서 점점 더 모듈 사용이 복잡해짐에 따라 번들러가 등장하였다. 수많은 파일과 모듈들을 정리하여 하나의 파일로 번들링해주는 번들러의 필요성이 대두되었다. 따라서 Webpack, Rollup 등의 번들러 도구들이 ESM을 지원하고 있으며 CommonJS와 AMD 또한 처리할 수 있도록 발전하였다.

 

 

 

출처

모듈 시스템의 역사

https://deemmun.tistory.com/86

https://witch.work/ko/posts/import-and-require

https://leetrue.hashnode.dev/javascript-module-system?t

 

commonJS, AMD https://velog.io/@qhflrnfl4324/Module-system-with-CommonJs-AMD-JavaScript

 

'📝 공부 기록/Javascript' 카테고리의 다른 글
  • [JS 톺아보기] 실행 컨텍스트와 함께 알아본 '스코프 체인' (with. 모던 JS Deep Dive)
  • [JS 톺아보기] 반복문과 재귀함수의 비교 (with. 모던 JS Deep Dive)
상심한 개발자
상심한 개발자
  • 상심한 개발자
    상심한 개발자
    상심한 개발자
  • 전체
    오늘
    어제
    • 상심한 개발자 (36)
      • 📝 공부 기록 (4)
        • Javascript (3)
        • CS (1)
        • NodeJS (0)
      • 💻 개발 기록 (1)
        • Sring, 스터디 모집 및 관리 기능 통합 서비.. (1)
      • 💾 자료구조 & 알고리즘 (26)
        • 이론 정리 (13)
        • 문제 풀이 (13)
      • 📝 후기 및 회고록 (4)
      • etc. (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    삽질기록
    배열
    블로그
    array
    자료구조
    JavaScript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
상심한 개발자
[JS 톺아보기] Javascript에서 module이란? 모듈 시스템의 비교까지(CommonJS, AMD, UMD, ESM) (with. 모던 JS Deep Dive)
상단으로

티스토리툴바