갓태희

[JavaScript] CommonJS 본문

카테고리 없음

[JavaScript] CommonJS

갓태희 2021. 5. 24. 11:33

원문을 제가 공부한 내용과 함께 쓴 글입니다. 잘못된 부분이 있으면 적극적으로 피드백부탁드립니다!

범용적인 목적으로 JavaScript를 사용하기 위해 필요한 선결 조건은 모듈화입니다.

Node.js도 이런 모듈화 작업때문에 탄생할 수 있었는데, JavaScript 모듈화 작업의 선두 주자는 CommonJS와 AMD입니다.

이 글에서는 CommonJS와 AMD의 JavaScript 모듈화에 대해 간략하게 설명합니다. (AMD는 다른 글에서 설명합니다.)

Common JS

CommonJS링크는 JavaScript를 브라우저에서 뿐만 아니라, 서버사이드 애플리케이션이나 데스크톱 애플리케이션에서도 사용하려고 조직한 자발적 워킹 그룹입니다. CommonJS의 'Common'은 JavaScript를 브라우저에서만 사용하는 언어가 아닌 일반적인 범용 언어로 사용할 수 있도록 하겠다는 의지를 나타내고 있는 것이라고 이해할 수 있다.

이 그룹은 JavaScript를 범용적으로 사용하기 위해 필요한 '명세(Specification)'를 만드는 일을 하며 Module 명세로는 2021년 현재 Modules/1.0 ,Modules/1.1 ,Modules/1.1 ,Modules/1.1.1 가 있다.

탄생 배경

1996년 자바스크립트가 탄생한 후, JavaScript를 브라우저 밖에서도 사용하려는 노력이 끊임없이 이어져 왔다. Heloma, ApJet, Jaxer 등등이 있지만 큰 성공을 거두진 못했는데 2005년 Ajax가 부상하면서 JavaScript의 중요성이 그전보다 더 부각되었다. Ajax의 활성화와 함께 JavaScript 연산이 증가했고, 자연스레 더 빠른 JavaScript 엔진이 필요하게 되었다.

이런 맥락에서 2008년 구글이 공개한 V8 JavaScript 엔진은 많은 주목을 받았다. V8엔진은 기존의 JavaScript엔진보다 월등히 빨랐을 뿐만 아니라, 브라우저 밖에서도 충분히 쓸만한 성능을 자랑했다.

V8 엔진의 등장은 서버사이드 JavaScript 진영에도 활기를 불어넣었다. 2009년 1월 Kevin Dangoor는 서버사이드 JavaScript가 성공하려면 기술적인 맥락보다는 공동으로 표준을 정하고 표준을 지켜나가는 활동이 필요하다고 보았고 그렇게 CommonJS가 시작된지 3개월만에 CommonJS API 0.1을 발표한다.

Kevin은 JavaScript가 브라우저용 언어를 넘어 범용적으로 쓰이려면, Ruby나 Python과 같은 체계가 필요하다고 주장했고 kevin의 핵심문제 제기는 다음과 같다.

  • 서로 호환되는 표준 라이브러리가 없다.
    • 브라우저 저마다 정적인 HTML을 동적으로 조작할수있는 언어를 만들려하다보니 표준이 없다.
  • 데이터베이스에 연결할 수 있는 표준 인터페이스가 없다.
    • IE5에서 1999년 Window 98 SE와 함께 XMLHttpRequest를 출시했으며 한 예로 Oracle과 연동해서 데이터베이스에 저장된 정보를 가져올수 있다.
  • 다른 모듈을 삽입하는 표준적인 방법이 없다.
    • require()를 이용한 모듈을 import및 export할수있다.
    • 현재는 import, export 구문이 있으며 babel을 통해 사용하거나 package.json에 "type":"module"을 적어주면 사용 가능하다
  • 코드를 패키징해서 배포하고 설치하는 방법이 필요하다.
    • 결국 이것이 모듈화가 필요하다는 말이다.
  • 의존성 문제까지 해결하는 모듈 저장소가 필요하다.
    • Node.js의 경우 npm Manager를 통해 Node 중앙저장소에 있는 수많은 패키지들을 쉽게 설치 및 배포 할수있다.

핵심은 모듈화

앞에서 언급한 문제들은 결국 모듈화로 귀결되며, CommonJS의 주요 명세는 바로 이 모듈을 어떻게 정의하고, 어떻게 사용할 것인가에 대한 것이다.

모듈화는 3가지로 이루어진다.

  • 스코프(Scope): 모든 모듈은 자신만의 독립적인 실행 영역이 있어야 한다.
  • 정의(Definition): 모듈 정의는 exports 객체를 이용한다.
    • 실제 Node의 REPL을 이용해 module의 안에 보면 빈객체({})인 exports가 존재한다. 이 객체를 이용해서 모듈을 정의한다.
  • 사용(Usage): 모듈 사용은 require 함수를 이용한다.

모듈은 자신만의 독립적인 실행 영역이 있어야 한다. 따라서 전역변수와 지역변수를 분리하는 것이 매우 중요한데 서버사이드 JavaScript의 경우에는 파일마다 독립적인 파일 스코프가 있기 때문에 하나에 모듈 하나를 작성하면 간단히 해결된다. 즉 서버사이드 JavaScript는 아래와 같이 작성하더라도 전역변수가 겹치지 않는다.

fileA.js

exports.a = "fileA";
exports.b = "fileA";

console.log("fileA 시작");
console.log(exports);
console.log("fileA에 a,b가 담겼습니다.");
console.log("fileA 끝\n");

fileB.js

const fileA = require('./fileA');

console.log("fileB 시작");
console.log("fileB에서 불러온 fileA", fileA);

exports.a = "fileB";
exports.b = "fileB";

console.log("fileB 끝\n");

fileC.js

const fileB = require('./fileB');

console.log("fileC 시작");
console.log("fileC에서 불러온 fileB", fileB);

console.log("각 모듈마다 exports에 값을 넣기전에는 빈 객체인걸 알수있다. ",exports);

exports.a = "fileC";
exports.b = "fileC";

console.log("fileC에서 공유한 a,b",exports);
console.log("fileC 끝");

다음 세 파일을 만들고 fileC.js를 실행시켜보면 결과는 다음과 같다.

  • fileA.js부터 차근차근 살펴보면 fileA에서는 단순히 exports객체를 이용해 a,b를 공유하려고 하고있는데 각각에는 "fileA"라는 문자열이 담겨있다.
  • fileB.js 에서 바로 fileA.js에서 공유했던 exports객체 전체를 require함수를 통해서 가지고 오는것을 볼수있다. 가져온 객체의 a,b값을 출력해 보면 fileA에서 export했던 값들이 잘 출력되는것을 볼수 있다. 그 후에 똑같이 exports.a, exports.b에 "fileB"라는 문자열을 넣었고 일부러 fileA와 똑같은 변수명에 대입을 해보았다.
  • fileC.js에서는 fileA.js와 fileB.js를 require 해왔고 여기서 눈여겨볼점은 fileA, fileB 둘다 exports에 a와 b를 속성으로 넣었으므로 fileA의 값이 fileB의 값으로 덮어 씌워지는것이 아닌 각각 독립적으로 값이 들어가있다는점이다. 실제로 각 모듈마다 exports가 처음에는 빈객체임을 알수있다. 이 말은 이전에 exports에 값을 대입한적이 있다고 해서 새로운 모듈(자바스크립트 파일)의 exports가 그 대입을 한 exports객체가 아니라는 점이다.이 점이 위의 강조표시한 내용을 아주 잘 설명해 주고있다.

위의 예에서 CommonJS의 모듈 명세는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 한다. 다시 말해 서버사이드 JavaScript환경을 전제로 한다.

하지만 이러한 방식은 브라우저에서는 결정적인 단점이 있는데 바로 필요한 모듈을 모두 내려받을 때까지 아무것도 할 수 없게 되는 것이다. 앞서 제시한 예에서 3개의 모듈은 그 길이가 상당히 짧은 모듈이다 하지만 모듈하나에 1억줄이고 그 모듈이 1억개가 있다고 하면 이 모듈을 내려받는 때까지 아주많은 시간이 걸릴것이며 사실상 다른작업은 아무것도 못하는 상황이 된것이다. (너무 극단적인 예이지만 생활코딩에서 프로그래밍을 할때는 항상 극단적인 생각을 하라는 가르침을 받아서..) 이러한 단점을 극복하려는 여러 방법이 CommonJS에서 논의 되었지만, 결국 동적으로 태그를 삽입하는 방법으로 가닥을 잡는다. script 태그를 동적으로 삽입하는 방법은 JavaScript 로더들이 사용하는 가장 일반적인 방법이기도 하다.

비동기 모듈로드 문제

JavaScript가 브라우저에서 동작할 때는 서버 사이드 JavaScript와 달리 파일 단위의 스코프가 없다. 즉 앞의 예시를 태그를 이용해 로드하면 앞서 원래 예상했던 결과인 fileB가 fileA의 변수를 모두 덮어쓰고 fileC역시 fileB의 변수를 모두 덮는 전역변수 문제가 발생한다. 이런 문제를 해결하려고 CommonJSS는 서버 모듈을 비동기적으로 클라이언트에 전송할 수 있는 모듈 전송 포멧(module transport format)을 추가로 정의했다. 이 명세에 따라 서버사이드에서 사용하는 모듈을 다음 예의 브라우저에 사용하는ㄴ 모듈과 같이 전송 포멧으로 감싸면 서버 모듈을 비동기적으로 로드할 수 있게 된다.

서버사이드에서 사용하는 모듈

// complex-numbers/plus-two.js

var sum = require("./math").sum;  
exports.plusTwo = function(a){  
return sum(a, 2);  
};

plus-two.js모듈에서 같은 폴더위치에 있는 math.js의 sum함수를 reqiure해오고 있다. 그런다음 plusTwo라는 함수를 exports 해주었다.
서버사이드에서는 이렇게 각각의 모듈이 파일단위의 스코프를 가지므로 밑의 브라우저에서 사용하는 모듈 코드보다는 상대적으로 쉽고 짧게 코드를 작성할수 있다.

브라우저에서 사용하는 모듈

// complex-numbers/plus-two.js

require.define({"complex-numbers/plus-two": function(require, exports){

//콜백 함수 안에 모듈을 정의한다.
var sum = require("./complex-number").sum;  
exports.plusTwo = function(a){  
return sum(a, 2);  
};
},["complex-numbers/math"]);
//먼저 로드되어야 할 모듈을 기술한다.

하지만 위와 달리 브라우저에서 사용하는 모듈은 똑같은 상황임에도 불구하고 맨 밑의 문자열 배열 ["complex-numbers/math"]를 통해서 먼저 로드되어야할 모듈을 기술하고 require.define안에 plus-two라는 모듈을 콜백 함수안에 정의 하고있다. 모듈을 파일 스코프 단위로 나눌수 있고없고의 차이를 잘 드러내고 있다. 또한 define함수 안에서 sum(a,2)를 반환했으므로 클로저를 통해 전역변수 통제까지 하고있다는 사실을 유심히 보면된다.

위의 더 자세한 사항은 참고를 하면된다.

CommonJS를 따르는 사람들

CommonJS는 현재 실질적인 표준(de fact standard)역할을 하고있으며 많은 서드파티 벤덛들이 CommonJS 모듈 명세에 따라 모듈을 만들거나 모듈 로드 시스템을 만들고 있다. 이 명세를 따르는 대표적인 프로젝트가 Node.js이다. 그 밖에도 다음과 같은 로더와 프레임워크가 CommonJS 모듈 명세를 따르고 있다.

브라우저용

위의 목록만 보더라도 CommonJS가 꼭 서버사이드에 국한된 이야기가 아니라는 사실을 알 수 있다. 하지만 CommonJS를 만든 목적이 서버사이드에서 JavaScript를 사용하는 것이었기 때문에 서버사이드 용으로 사용할 때에 장점이 많다.

Comments