# JavaScript Module System

### ✋🏻 들어가며,

얼마 전, 애플리케이션이 만들어지고 배포가 되고 사용이 되는 일련의 과정들에 대해 공부를 해보고자 그림을 그려가며 공부를 하고 있었는데, '번들러'라는 용어가 나왔고, '모듈화된 js 파일을 하나로 합친다.'라는 문장과 눈이 마주쳤다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699541830645/e067b8cb-06e4-4664-baaf-2e10914a2328.webp align="center")

React 로 개발을 하다보면 무수히 많은 함수 컴포넌트들을 작성하기도 하고, 작은 함수 유틸도 만들기도 하는데 머릿속에 어렴풋이 이런 조각조각들이 모여서 하나의 큰 애플리케이션을 만든다는 것에 대해서는 알겠는데 내가 정확하게 알고있지 않다는 생각이 들어 이 참에 공부를 해봤다.

공부한걸 정리하기에 앞서, 관련 자료들을 잘 정리해주신 분들께 감사하다고 말씀드리고 싶다. 하나 하나 공부하고 찾아볼 수 있는 길잡이가 되어주셔서 늘 감사합니다.

## 🧩 자바스크립트 모듈 시스템

초기 자바스크립트 프로그램은 규모가 크지 않아서 대부분의 스크립트들이 독립적으로 수행되었다고 한다. 하지만 지금 우리가 사용하고 있는 것들을 생각하면 그 규모가 어마어마하게 커졌다. 이렇게 시간이 지남에 따라 어플리케이션 규모가 커지며 스크립트 파일들이 나뉘었고, **필요한 것들만 가져올 수 있는 모듈 분할에 대한 필요성**이 생겼다. 그렇게 모듈 시스템이 나타났다.

**모듈 시스템**이란 플러그인 파일이나 잘게 쪼개져있는 JS 코드 조각들을 재사용하기 위해서 각각의 파일을 등록하고, 등록된 파일을 JS에서 불러와 사용할 수 있게 해주는 프로그램을 말한다. 여기서 “**모듈**”이란 어플리케이션을 구성하는 부품, 즉 **재활용 가능한 코드 단위**라고 할 수 있다.

> **모듈** : 재활용 가능한 + 코드 단위  
> → 어디에서 사용되어도 동작의 일관성이 보장됨  
> → 하나의 어플리케이션이 N개의 모듈 집합으로 구성됨

자바스크립트는 아래와 같은 모듈 시스템을 사용할 수 있는데, 각각이 어떤 것이고 어떤 특징이 있는지 살펴보자.

1. **CommonJS**
    
2. **AMD**
    
3. **UMD**
    
4. **ESM**
    

---

### 1️⃣ CJS, **CommonJS**

[CommonJS](https://wiki.commonjs.org/wiki/CommonJS) 는 정적 바인딩, 동기(synchronous) import 를 큰 특징으로 가지는 모듈 시스템이다.

> **정적 바인딩**  
> : `require` 를 통해 가져온 값의 **복사본**을 제공함. 즉 `module.exports` 를 수행한 곳에서 값의 변경이 있어도 최초 `require` 이후에는 변경된 값을 사용할 수 없음.

CJS 의 "**동기 import**" 라는 특징 덕분에 이는 서버 사이드에서 사용하기 용이하다. CommonJs라는 이름도 원래는 ServerJS였다고 하니..! Node.js가 CommonJS 를 채택해서 사용하고 있다.

그럼 CJS를 사용하는 예제 코드를 확인해볼까?

```jsx
var lib = require('package/lib');

function hello() {
	lib.log('hello world!');
};

exports.hello = hello;
```

위 코드와 같이 `require` 를 사용하면 `package/lib` 이라는 모듈을 변수에 담아서 사용할 수 있다. 그리고 위와 같이 만든 함수를 다른 파일에서 사용할 수 있도록 다른 모듈로 추출될 수도 있다.

### 2️⃣  **AMD, Asynchronous Module Definition**

**CommonJS**는 모든 파일이 로컬 디스크에 있어서 필요할 때 바로 불러올 수 있는 상황을 전제로 하는데, 이는 즉 **동기적인 동작이 가능한 서버사이드 JS 환경을 전제로 한다는 것**이다. 하지만 브라우저에서 이 방식은 **필요한 모듈이 모두 다운로드 되기 전까지는 아무것도 할 수가 없어 치명적인 단점**이 될 수 있다.

AMD는 비동기 상황에서 JS모듈을 사용하는 것에 대해 고민했고, 기존에 CommonJS에서 함께 논의하다 합의점을 찾지 못해 독립했다. 즉 CommonJS는 JS를 브라우저 밖으로 꺼내기 위해 탄생한 그룹인 반면 **AMD는 브라우저에 중점을 둔 그룹**이다.

아래 코드는 AMD에서 모듈로 추출하는 코드이며,

```jsx
// 종속성을 갖는 모듈인 'package/lib'를 모듈 선언부의 첫 번째 파라미터에 넣으면,
define(['package/lib'], function (lib) {
                        // 'package/lib'은 콜백 함수의 lib 파라미터 안에 담김
	function hello() {
		lib.log('hello world!');
	}

	return {
		hello: hello
	}
}
```

위와 같이 선언된 모듈들은 아래와 같이 `require` 로 사용가능하다.

```jsx
require(['package/myModule'], function(myModule){
	moModule.hello();
});
```

\*\*AMD의 주된 특징은 ‘비동기’\*\*이다. 브라우저는 네트워크를 통해 모듈을 내려받기 떄문에 비동기적으로 동작해야하므로 AMD의 비동기적인 특징으로 클라이언트 사이드 개발에 적합하다 볼 수 있겠다.

여기까지 딱 보고나면, 이 모두를 아우르는 뭔가가 나올 때가 되었다는 동물적인 직감이 든다. 개발자들은.. 이렇게 두지 않았을 것이기 때문에.. 이런 생각이 떠오르고 나니 그 다음으로 다뤄지는게 UMD 라는 녀석이다. (양반 아님)

### 3️⃣ **UMD**

AMD와 CommonJS 두 그룹으로 나누어지다 보니 서로 호환되지 않는 문제가 발생하게 되어 이를 해결하기 위해 UMD가 등장했다. **UMD는 모듈 시스템에 따라 다른 구현을 정의하고 있는 형태에 가깝다.**

AMD는 `define`을 사용하고, CommonJS는 `module.exports`를 사용하는데, 이 차이를 이용하여 UMD를 만들 수 있다. UMD는 두 부분으로 구성된다.

* 모듈 로더를 확인하는 즉시 실행 함수(IIFE)  
    → 이 함수는 `root`(전역 범위)와 `factory`(모듈을 선언하는 함수) 2개의 파라미터를 가짐
    
* 모듈을 생성하는 익명 함수  
    → 이 함수가 즉시 실행 함수의 2번째 파라미터로 전달됨
    

아래 코드를 보면, `exports`와 `module`이 존재하면 CommonJS 방식으로, `define`이 함수이고 `define.amd`가 존재할 경우 AMD 방식으로 그것도 아니라면 window 객체에 모듈을 내보낸다.

```jsx
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['exports', 'b'], factory);
  } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
    // CommonJS
    factory(exports, require('b'));
  } else {
    // Browser globals
    factory((root.commonJsStrict = {}), root.b);
  }
}(this, function (exports, b) {
  //use b in some fashion.

  // attach properties to the exports object to define
  // the exported module properties.
  exports.action = function () {};
}));
```

다시 정리하면, UMD의 주된 특징은 **여러 모듈 로더에서 사용이 가능**한 것이다. 즉 AMD와 CommonJS 모두 사용 가능하다.

### 4️⃣ **ESM, ECMAScript Module**

ESM은 언어 자체에 표준으로 탑재된 모듈 시스템으로 ES6에 추가되었다. 우리가 주로 사용하는 방법인데 예제 코드는 아래와 같다.

```jsx
import lib from 'package/lib';

fuction hello() {
	lib.log('hello world!');
}

export { hello: hello };
```

**ESM은 브라우저에서의 지원을 위해 번들러를 함께 사용**한다. 이 친구는 계속해서 조금 더 자세히 알아보도록 하자. (공부하려했던 주요 내용이었으니!)

---

## 🤝 **ESM, ECMAScript Module**

ESM은 ES6 부터 도입된 모듈 시스템으로 `export - import` **문을 사용해 분리되어 있는 자바스크립트 파일 간의 접근을 가능하게 만들어준다.** (다른 프레임워크는 아직 잘 모르는데, React 개발자들은.. 위 아래로 매일 쓰고 있는 그것..^^)

**ESM 등장 이전에는 각각의 script 파일을 전역 스코프처럼 사용**했는데, HTML 파일에서 보다 위에 있는 script 파일은 전역 스코프처럼 하위의 script 태그에서의 접근 또는 변경이 가능했다. 하지만 이 구조는 문제점이 있었다.

1. script 파일들의 순서가 중요했고, 순서가 섞이면 에러가 발생
    
2. 하위의 script 가 상위 script 상태를 쉽게 변경시키는 전역 오염이 발생
    
3. 모든 script 파일에서 전역 스코프에 있는 변수들에 접근할 수 있어서 하나의 script 가 어떤 script 를 의존하고 있는지 파악이 어려움
    
4. 위 문제들로 인해 유지보수가 어려움
    

말이 뭐가 많은데 대충 **정리하면** `script`**의 순서가 지켜지지 않는 등의 실수가 생기면서 오류가 발생할 수 있고 상태 변경에 있어서도 오염 가능성이 있고 파악이 점점 어려워진단 것이다. 이러한 문제들로 인해 ESM이 등장했다**.

그럼 이제 ESM이 어떻게 돌아가고 있는지 알아보자.

### ⚙️ ES Module 의 동작 방식

**ESM에서 의존성 간의 연결은** `import` **문이 작성된 코드에서 발생**한다. `import` 문은 브라우저 또는 Node가 어떤 코드를 불러와야하는지 인식하는 것에 사용되는데, `import` 문에서 지정한 파일이 **의존성 그래프의 진입점**이 되고 연결되어있는 `import` 문들을 따라가며 의존성 그래프가 그려진다.

**ESM 이 동작하기 위해서는 브라우저가 사용 가능한 Module Record(export, import 정보가 담긴 데이터 구조)로 변환 작업**이 필요하다. 그리고 이러한 모듈화 과정은 다음과 같이 세 단계를 거친다.

> **구성 → 인스턴스화 → 평가**

#### 1️⃣ 구성 Construction

구성 단계에서는 **모듈이 있는 파일을 어디서 다운로드 할 지 파악**하고, **파일을 가져온 뒤 이를 Module Record(export, import 정보가 담긴 데이터 구조)로 구문 분석**을 한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699542403233/fed0a5c7-db05-4882-b1c5-8e6d2dcdebbb.png align="center")

> **로더 loader**  
> : 파일을 불러오는 역할을 하며, 플랫폼에 따라 다른 로더를 가질 수도 있고 브라우저의 경우는 HTML 명세를 따른다. 로더는 script 태그에서 진입점 파일을 찾을 수 있는 단서를 얻고 import문은 모듈 지정자를 통해 다음 모듈의 의존성을 파악한다.

#### 2️⃣ 인스턴스화 Instantiation

이렇게 구문 분석이 끝나면 **Module Record 를 Module Instance로 변환**한다. 이는 import 할 모든 값을 할당할 메모리 공간을 찾는 과정으로, `export / import` 모두 해당 메모리를 가리키도록 한다.

> **모듈 인스턴스 Module Instance** ’code’와 ‘state’라는 두 가지를 결합한 상태

**하나의 모듈에 대한** `export / import` **는 같은 메모리 주소를 가리키는데**, 이것을 **라이브 바인딩**이라 한다. 이는 `import` 들이 각각의 `export` 에 연결되어 있다는 것을 보장함으로써 특정 모듈을 `import` 하는 또 다른 모듈에서는 가져오고 있는 모듈에서 발생하는 변경사항들을 알 수가 있게 된다.

그럼 `import` 해온 모듈은 `import` 해온 위치에서 변경할 수 있을까? 놉.. 변경할 수 없다. 하지만 모듈이 객체를 가져오는 경우라면 해당 객체의 프로퍼티 값 변경은 가능하긴 하다.

#### 3️⃣ 평가 Evaluation

평가 단계는 코드를 실행해 변수의 실제 값으로 메모리 공간을 채우는 과정이다.

### 👻 ESM의 특징

1. **ESM은 static 함**  
    : commonjs를 사용하거나, 리액트에서 webpack을 사용하는 경우에는 파일 확장자를 생략하는 경우도 있는데, ESM은 static 하기 때문에 `import - export` 가 컴파일 시점에 결정되어 파일 경로를 꼭 명시해야 함
    
2. **함수, 변수, 클래스를 export 하거나 import 할 수 있음**
    
3. **export문은 최상위 항목이어야 함**  
    : ESM에서 `import` 는 함수가 아닌 키워드이므로 변수에 할당하거나 중첩된 구조로 사용 X
    
4. 내보내거나 가져올 때는 중괄호 `{}` 로 묶을 수 있음
    
5. `script` 로 모듈을 선언할 때는 `<script>` 에 `type="module"` 을 포함시키면 됨
    

```jsx
<script type="module" src="module.mjs"></script>
```

### 📘 ES Module 사용 방법

#### 1\. 내보내기 - 가져오기

```jsx
// 1. 개별로 내보내기
export const name = 'square';
export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}
```

```jsx
// 2. 묶어서 내보내기
export { name, draw, reportArea, reportPerimeter };
```

```jsx
// 3. 가져오기
import { name, draw, reportArea, reportPerimeter }
                                            from './modules/square.js';
```

#### 2\. `as` 키워드와 **Renaming**

`import - export` 문에서 중괄호 `{}` 내부에 `as` 라는 키워드를 사용해 내보내거나 가져오려고 하는 모듈의 각 요소들에 대한 이름을 변경해줄 수 있는데, 이를 통해서 이름이 겹치는 경우에 생기는 에러를 해결할 수 있다.

```jsx
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};
```

```jsx
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from './modules/module.js';
```

#### 3\. 모듈 한 번에 가져오기 - **Module Object**

중괄호 `{}` 를 사용해서 비구조화해서 모듈을 가져오는 경우에는, 코드가 너무 길어져 지저분해질 수 있다. 이 때는 `as` 를 사용해서 모듈의 기능을 하나의 객체로 묶어 가져올 수 있다.

```jsx
import * as UI from './style.ts';

const Component = () => {
	return <UI.Layout></UI.Layout>
}
```

위와 같이 사용할 수 있는데, 이렇게 되면 `style.ts` 내에서 사용할 수 있는 모든 `export` 를 가져와 각각을 이 모듈의 프로퍼티처럼 사용할 수 있다.

#### 4\. **Module 집합**

모듈을 모아야 할 때는 여러 개의 서브 모듈들을 하나의 부모 모듈로 결합시킬 수 있다. 아래 예시를 보면, `modules` 하위의 세 파일에서 각각 모듈을 불러와서 사용하고 있는데 이것들을 하나로 만들어줄 수 있다는 것이다.

```jsx
// main.js
import { Square } from './modules/square.js';
import { Circle } from './modules/circle.js';
import { Triangle } from './modules/triangle.js';
```

```plaintext
modules/
  - shapes.js
  - shapes/
    - circle.js
    - square.js
    - triangle.js
```

위와 같이 구조를 잡아두고, 아래와 같이 `circle.js`, `square.jstriangle.js` 을 `shape.js` 파일에서 각각 내보내주자. 그러면 이 세 개의 모 듈은 `shape.js` 만 불러와서 간단하게 사용할 수 있게 된다.

```jsx
// shape.js
export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';
```

```jsx
import { Square, Circle, Triangle } from './modules/shapes.js';
```

#### 5\. Dynamic Imports **동적 모듈 로딩**

기본적으로 모듈들은 최상위에서 불러오게 되는데, 동적 모듈 로딩은 **필요할 때만 모듈을 동적으로 불러올 수 있다**. 기억할 키워드는 `import()` 이다.

`import()` 를 함수로 호출해서 모듈 경로를 파라미터로 전달한 뒤 모듈 객체를 사용해 `promise` 를 반환하면 모듈 객체가 가지고 있는 `export` 에 접근이 가능하다.

```jsx
import('/modules/myModule.js')
  .then((module) => {
		// Do something with the module.
});
```

음 뜬금없는 여담인데, 예전에 팀원분께서 옆에 스윽 오셔서 **“이렇게** `export` **하면 너무 귀찮지 않아요? 이름을 매번 지어줘야되서 나는 싫던데..”**라고 말씀을 하셨었는데 **‘어.. 원래 이렇게 하는게 아닌가..? 😳’ 하며 머쓱했던 기억**이 난다. 아마 아래 내용이 그 때 내가 제대로 알아듣지 못했던 이유가 될 것 같구나. 지금이라도 알아서 다행..!

#### **6\. Default export & Named export**

* **Named export** : `export` 로 내보내지는 함수, 변수, 클래스 등의 항목이 이름으로 참조됨
    
* **Default export** : 하나의 모듈에 하나만 존재할 수 있으며, `import` 할 때 해당 모듈이 `default` 값이 됨. 선언과 분리할 수도 있고, 선언과 동시에 내보낼 수도 있음
    
    * import 하라 때 어떤 이름으로든 import 가 가능함
        
    * named export 와 달리 export문과 import문에 중괄호가 없음
        
    * 함수나 클래스와 다르게 변수는 선언과 동시에 내보내기가 불가능하므로 반드시 선언과 내보내기를 분리하여 작성해야 함
        

default export 는 아래와 같이 사용할 수 있다.

```jsx
// 선언과 내보내기 분리
export default randomSquare;

// 선언과 동시에 내보내기
export default function(ctx) {
  ...
}
```

```jsx
// 기본형
import {default as randomSquare} from './modules/square.js';

// 단축형
import randomSquare from './modules/square.js';
```

음 팀원분께서 귀찮다고 느끼셨던건 아마 default export 에서 import를 할 때 이름을 지어줘야하는 점이 귀찮으셨던게 아닐까. 😶 그럼 뭘 쓰는게 더 좋을까.

Airbnb JS Style Guide에서는 하나만 export 하는 모듈이면 default export 를 사용한다고 한다. default를 쓰면 읽기 편하고, 유지보수성이 향상되고, Treeshaking이 가능하고, 개념적 이해 등등이 가능하다는 의견과 함께..? 물론 팀이 정하기 나름이 아니겠나.

### 👩🏻‍🌾 마무리

간단하다 생각해서 다이빙 뛰었는데, 정말 오랜 시간 공부를 하게 된 것 같다. 그 중에서 핵심만 집어서 정리를 해봤는데, 망각할 내 뇌 녀석을 생각하면 깊은 것들도 그냥 좀 더 정리해둘걸 싶기도 하고.. 나중에 시간이 되면 그 때 조금 더 딥하게 정리해봐야겠다.

그래도 이번에 정리를 하면서, 생각없이 `import - export` 하던 것들이 왜 그렇게 작성을 해야하고 이것들이 어떤식으로 서로를 참조하고 사용하게 되는지 전반적인 과정을 알게 되어서 좋았다. 이제 오늘 공부한 것들을 기반으로 해서 순환참조와 번들러 관련해서도 조금 살펴봐야겠다.

### 🙏🏻 참고

* ##### [https://d2.naver.com/helloworld/12864](https://d2.naver.com/helloworld/12864)
    
* [https://zubetcha.tistory.com/entry/Javascript-ES-Module](https://zubetcha.tistory.com/entry/Javascript-ES-Module)
    
* [https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/)
    
* [https://www.youdad.kr/js-module-system/](https://www.youdad.kr/js-module-system/)
    
* [https://webpack.kr/guides/tree-shaking/](https://webpack.kr/guides/tree-shaking/)
    
* [https://medium.com/@\_diana\_lee/default-export와-named-export-차이점-38fa5d7f57d4](https://medium.com/@_diana_lee/default-export%EC%99%80-named-export-%EC%B0%A8%EC%9D%B4%EC%A0%90-38fa5d7f57d4)
    
* [https://ui.toast.com/fe-guide/ko\_DEPENDENCY-MANAGE](https://ui.toast.com/fe-guide/ko_DEPENDENCY-MANAGE)
    
* [https://so-so.dev/web/tree-shaking-module-system/](https://so-so.dev/web/tree-shaking-module-system/)
