Typescript의 모듈 시스템

sehan·2020년 4월 5일
18

node_modules에서 namespace와 module이 혼동되어 쓰이는 것을 보고 둘의 차이점이 무엇인가에 대해 찾아보며 쓰게 된 글입니다. Typescript Handbook과 같은 여러 영문자료들을 찾아보면서 정리한 것이기 때문에 오역과 잘못된 내용이 있을 수 있습니다.

내부 모듈과 외부 모듈

Typescript의 모듈 방식은 Internal Module(내부 모듈)External Module(외부 모듈) 두가지의 모듈이 존재한다. 두 모듈화 방식의 차이는 module-loader의 의존성 여부이다.

Internal Module은 TS만의 특유한 모듈 방법으로써 다른 module-loader에 의존하지 않고 TS를 컴파일 할 때 이름이 명명된 JS 오브젝트를 생성함으로써 모듈화한다. 말 그대로 이름을 붙이는 네임스페이스를 생성한다고 볼 수 있다.

반면에 External Module은 다른 module-loader에 의존하여 모듈화하는 방법이다. External Module에는 ES Module, CommonJS(Node), Require.js(AMD)와 같이 따로 module-loader를 사용하는 모듈방법이 해당된다.

TS는 파일의 top-level(아무것으로도 감싸지지 않은 최상위 레벨)export가 존재하면 해당 ts파일을 모듈 파일로 생각한다. 파일의 top-level에 아무런 import나 export가 존재하지 않는다면 TS는 파일을 모듈이 아닌 스크립트 파일로 생각하고 이는 일반적인 JS파일과 같이 파일 내에 생성된 변수는 window, global과 같은 전역 스코프에 영향을 미치게 된다.

내부 모듈과 외부 모듈의 차이?

외부 모듈은 CommonJS/Require.js/ES Module과 같은 module-loaer에 의존성을 가지게 된다. 내부 모듈은 TS파일을 컴파일하고 난 후의 JS파일로 의존성을 해소한다.

module-loader를 이용하는 외부모듈

/* 컴파일 된 js, ESModule에 의존하고 있는 것을 볼 수 있다.*/
import * as express from "express";

script 태그로 의존성을 해소하는 내부모듈 (--outFile로 컴파일 시 한 파일로 합칠 수도 있다)

/* namespace를 참조하는 TS 파일 */
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
<!-- something.html 컴파일된 Javascript에 의존하고 있음 -->
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

또한 내부 모듈은 컴파일 된 JS에서 module-loader에 의존하지 않기 때문에 전역 스코프에 오브젝트로 명명된다. 이는 글로벌 네임스페이스를 망칠 수도 있으며 규모가 큰 프로젝트일수록 네임스페이스를 식별하기 어려워질 수 있다.

/* JS로 컴파일 된 내부모듈, 스크립트 파일이기 때문에 글로벌 스코프에 선언된다. */
var MyConsole;

(function (MyConsole) {
    function log(msg) {
        console.log(msg);
    }
    MyConsole.log = log;
})(MyConsole || (MyConsole = {}));

MyConsole.log("MyConsole");

따라서 간단한 수준의 프로그램이거나 ES2015(ES6)이하 레벨의 JS를 사용하는 경우를 제외하고는 ES Module을 이용하는 것을 추천한다. ES2015부터는 module 시스템이 네이티브 언어에 탑재되어있고 Typescript에서 외부모듈은 기본으로 ES2015의 모듈 컨셉을 사용한다.
Node Application의 경우에는 네임스페이스보다 module이 권고사항이고 이 방법이 기본이기 때문에 여태껏 Node에서 써왔던 모듈 방법에서 import/export로 키워드만 적절하게 바꿔주면 된다. 하지만 module-loader를 사용하지 않는 경우엔 네임스페이스를 이용한 모듈 방법이 좋은 방법이 될 수도 있다.

Typescript의 Namespace

Typescript 1.5부터 Internal Module의 명칭이 Namespace로 변경되었다.

Typescript의 Namespace는 네임스페이스를 이용하는 모듈 방법(Internal Module)을 제공한다.

Namespace는 다음과 같이 사용한다.

namespace MyConsole {
  export function log(msg) {
    console.log(msg);
  }
}

MyConsole.log("MyConsole");

/// <reference path="MyConsole.ts" />와 같이 접근할 수 있다.
Triple-Slash Directives

declare module과 declare namespace

declare 키워드는 타입스크립트 컴파일러에게 특정한 변수가 있다고 선언하는 키워드로 전역변수를 사용하거나 .d.ts 파일을 만들때 사용한다.

TS에는 두 가지의 모듈 선언 방법이 있다.
1. declare module "buffer" {}
2. declare module buffer {}

전자는 외부 모듈(External Module)을 정의하는 방법이며 후자는 내부 모듈(Internal Module)을 정의하는 방법이다. 후자는 namespace가 생기고나서부터declare namespace buffer {}로 교체되었다. 모듈명이 문자열로 감싸져있다면 외부 모듈(ES6)에 대해 정의하는 것이고 문자열로 감싸져있지않다면 내부모듈을 정의하는 것이다.

결론

ts파일의 top-level에 export나 import가 있으면 모듈 파일(외부 모듈)이고 export나 import가 존재하지 않는다면 그것은 모듈파일이 아니며 파일 내의 변수에 대한 독립성을 지켜주지 않는다. export나 import 없이 namespace만 쓴다면 이는 모듈기능을 제공받지 않는 것과 동일하므로 컴파일 된 js에서 글로벌 환경을 오염시킬 수 있다는 뜻이 된다. 하지만 외부 모듈 파일 안에서 따로 namespace를 사용하는 것은 독립성이 지켜진다고 볼 수 있다. 외부 모듈 파일 안에서 선언한 namespace는 컴파일 된 js에서 그저 모듈화된 js파일 안에 또 모듈화된 변수라고 생각할 수 있다. 개인적으로 생각하기에는 외부 모듈과 내부 모듈의 차이는 모듈의 독립성 여부인 것 같다. module-loader를 사용하는 경우엔 어차피 module-loader가 모듈의 독립성을 지켜주지만 namespace는 typescript의 모듈 방식으로 모듈의 독립성을 따로 지켜주지 않기 때문이다. 참조할 때 사용했던 /// <reference path="Validation.ts" />는 컴파일 시 그냥 주석으로 처리된다.

profile
필요한 공부를 찾아가며 현재를 열심히 살아가려 노력하고 있습니다.

0개의 댓글