프로그래밍/JavaScript

[JS] Javascript 모듈 시스템

mhko411 2021. 9. 1. 22:27
728x90

모듈화가 필요한 이유

모듈은 관련된 기능을 하나의 파일에서 코드로 작성하여 실행시키는 것이다. 이렇게 관련된 기능끼리 나눠서 여러 개의 파일을 만들어 모듈화를 진행할 수 있다. 모듈화는 다른 코드와의 결합도를 줄이고 재사용성을 높일 수 있다는 장점이 존재한다.

 

Javascript의 모듈 시스템

Javascript는 본래 웹 페이지의 보조 작업을 수행하기 위해 만들어진 언어이며 모듈화 기능을 제공하지 않았다. 따라서 서로 다른 파일에서 작성한 코드이지만 스코프를 공유하여 서로의 파일에 영향을 주었다. 아래의 코드를 보자.

a.js이라는 파일에서는 x를 선언하고 5로 초기화하였다. 해당 파일에서 x를 출력하면 5가 출력된다. 그리고 b.js라는 파일에서는 x를 선언하기 전에 출력하고 x에 새로운 값인 3을 대입하였다. 해당 파일에서는 5가 출력되고 그 다음 3이 출력된다.

// a.js
let x = 5;
console.log(x); // 5
// b.js
console.log(x); // 5
x = 3;
console.log(x); // 3
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./a.js"></script>
    <script src="./b.js"></script>
  </body>
</html>

 

이처럼 Javascript는 파일로 분리했지만 모듈화를 지원하지 않기 때문에 Javascript 파일들이 하나의 스코프를 공유한다.

 

CommonJS와 AMD

Javascript의 모듈 시스템을 지원하기 위해 CommonJS와 AMD가 탄생하였다.

 

CommonJS

Node.js에서는 CommonJS의 키워드를 사용하여 모듈화 시스템을 지원하고 있다. CommonJS의 모듈화는 다음 세 가지로 정의할 수 있다.

1. 모든 모듈의 독립적인 실행 영역을 갖고있다.

2. 모듈의 정의를 위해 exports를 사용한다.

3. 모듈의 사용을 위해 require를 사용한다.

// a.js
exports.sum = function(a, b) {
	return a + b;
}
// b.js
const a = require('./a.js');
console.log(a.sum(2, 3));

하지만 CommonJS은 브라우저에서 사용할 때 단점이 존재한다. CommonJS는 동기적으로 실행되기 때문에 필요한 모듈을 모두 로드하기 전까지 다른 동작을 진행할 수 없다는 것이다. 따라서 <script>를 동적으로 삽입하는 방법으로 해결하고 있다.

 

CommonJS의 경우 현재 실질적인 표준(de factor)으로 자리잡고 있다. 따라서 모듈을 만들 때 CommonJS의 명세를 많이 따르며 CommonJS의 명세를 따라서 Node.js를 만들기도 하였다.

 

AMD

AMD는 Asynchronous Module Definition의 약자이며 비동기 모듈에 대한 표준을 명세하고 있다. AMD의 특징은 동적 로딩, 의존성 관리, 모듈화가 있다.

 

동적 로딩은 <script>를 동적으로 삽입하는 것이다. 동적으로 삽입하면 window.load와 같은 이벤트에 대한 처리를 통한 콜백 함수를 사용하는데 만약 로드해야 할 모듈이 많다면 콜백 헬에 빠질 수 있다. 따라서 AMD의 점진적인 방식의 동적 로딩으로 이를 해결할 수 있다.

 

Javascript는 명시적이고 강제적인 키워드(#include, import)가 존재하지 않기 때문에 의존성을 파악하기 힘들다. 따라서 AMD는 특정 기능을 하나의 단위로 묶어서 이름을 붙이고 이를 다른 스크립트에서 불러오기 위해 define()을 사용하여 의존성 관리를 한다. 

 

독립적인 스코프를 갖는 모듈화를 위해서 return으로 외부에서 접근할 변수와 함수만 선택해서 내보낼 수 있으며 외부에 내보내지 않아도 되는 변수와 함수는 클로저를 이용하여 전역 공간에 위치시키지 않고도 접근할 수 있도록한다.

아래의 코드는 아직 클로저를 완벽하게 이해하지 않아서 첨부하였다.

// https://d2.naver.com/helloworld/591319
var foo = (function () {  
    var i = 0;

    function init() {
        reset();
    }

    function reset() {
        i = 0;
    }

    function increase() {
        i++;
    }

    function decrease() {
        i--;
    }

    function get() {
      return i;
    }

    return {
        init: init,
        increase: increase,
        decrease: decrease,
        get: get
    };
}());

foo.increase();  
console.log(foo.get()); // 1  
foo.decrease();  
console.log(foo.get()); // 0

console.log(foo.i); // undefined  
foo.reset(); // Error

 

ES6 Module

모듈을 동기적으로 로드했을 때 브라우저에서 발생하는 문제를 해결하기 위해 2015년에는 표준 자바스크립트에서 독자적인 모듈 시스템을 구축하였다. 이를 사용하기 위해서 CommonJS의 require가 아닌 import를 사용한다. 또한 <script>에 type="module"이라는 속성을 추가하여 브라우저에서도 문제없이 모든 모듈이 로드되어 정상적으로 동작되도록 할 수 있다. 

ESM을 통해 동지거, 비동기적으로 모듈을 로딩할 수 있다. 하지만 브라우저와 노드에 완전히 추가된 것이 아니다. 이때는 Babel을 통해 해결할 수 있을 것이다.

'프로그래밍 > JavaScript' 카테고리의 다른 글

[ES6] async/await  (0) 2021.09.23
[ES6] forEach와 map의 차이점  (0) 2021.09.06
[JS] this 키워드는 어떻게 동작할까?  (0) 2021.08.30
[JS] null과 undefined의 차이  (0) 2021.08.23
[ES6] Promise  (0) 2021.08.12