TypeScript - Modules

   

이 포스트는 TypeScript - Modules를 번역 및 학습한 내용입니다.

  

ECMAScript 2015(ES6)에 추가된 모듈 기능을 TypeScript에서도 똑같이 사용 할 수 있다. 모듈은 자신만의 스코프를 가지고 있으며, 모듈 내부에 선언된 변수, 함수, 클래스 등은 export 되지 않는 한 외부에서 접근 할 수 없다.

export된 모듈은 다른 모듈에서 import 키워드를 통해 불러올 수 있다. 이를 가능하게 하는 것은 모듈 로더이다. 모듈 로더는 런타임에 import된 모듈(디펜던시)의 위치를 확인한다. 자바스크립트에서 사용되는 모듈 로더의 종류는 크게 두 가지이다.

  • CommonJS 모듈을 위한 Node.js의 로더
  • AMD 모듈을 위한 RequireJS 로더

import, 혹은 export 키워드를 포함한 파일은 모듈로 처리된다. 그 외(import, export 키워드가 없는 파일)는 일반 스크립트(글로벌 스코프를 공유하는)로 처리된다.

 

Export

export 키워드를 사용하면, 선언된 모든 식별자(변수, 함수, 클래스, 타입, 인터페이스 등)를 export 할 수 있다.

// StringValidator.ts
export interface StringValidator {
  isAcceptable(s: string): boolean;
}
// ZipCodeValidator.ts
import { StringValidator } from './StringValidator';

export const numberRegex = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegex.test(s);
  }
}

  

export 문 작성 시 export 대상의 이름을 변경 할 수 있다. 위 예제를 아래와 같이 작성 할 수 있다.

// ZipCodeValidator.ts
import { StringValidator } from './StringValidator';

export const numberRegex = /^[0-9]+$/;

class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string) {
    return s.legnth === 5 && numberRegex.test(s);
  }
}

// mainValidator로 이름 변경 후 export
export { ZipCodeValidator as mainValidator };

   

특정 모듈을 extend하여, 해당 모듈의 일부 기능을 부분적으로 re-export 할 수 있다. 예를 들어, ParseIntBasedZipCodeValidator.ts에서 ZipCodeValidator.ts에 작성된 ZipCodeValidator 클래스를 re-export 할 수 있다. 이때 ZipCodeValidator를 import하지 않는다는 것에 주의해야한다.

// ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && parseInt(s).toString() === s;
  }
}

// ZipCodeValidator를 rename하여 re-export
export { ZipCodeValidator as RegExpBasedZipCodeValidator } from "./ZipCodeValidator";

  

선택적으로, 하나의 모듈에서 여러 모듈을 한꺼번에 export 할 수 있다. 이때 export * from 'module' 문법을 사용한다.

// AllValidators.ts

// StringValidator 인터페이스 export
export * from './StringValidator';

// ZipCodeValidator 클래스, numberRegexp 변수 export
export * from './ZipCodeValidator';

// ParseIntBasedZipCodeValidator 클래스 export 
// RegExpBasedZipCodeValidator 클래스 export (ZipCodeValidator.ts의 ZipCodeValidator 클래스를 rename하여 re-export)
export * from "./ParseIntBasedZipCodeValidator";

 

export * as namespace 문법을 사용하여 export 대상을 네임스페이스로 랩핑하여 re-export 할 수 있다. 이를 적용하여 위 예제를 일부 수정하면 아래와 같다.

// AllValidators.ts

// ZipCodeValidator 클래스, numberRegexp 변수를 validator 네임스페이스로 래핑하여 export
export * as validator from './ZipCodeValidator';

  

Import

import 키워드를 사용하여 export된 모듈을 로드 할 수 있다.

import { ZipCodeValidator } from "./ZipCodeValidator";

const myValidator = new ZipCodeValidator();

  

import 시 모듈의 이름을 rename 할 수 있다. 위의 예제를 아래와 같이 작성 할 수 있다.

// ZipCodeValidator를 ZCV로 rename
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";

const myValidator = new ZCV();

  

만약 특정 모듈에서 export되는 모든 대상을 하나의 네임스페이스로 import 하고 싶다면 아래와 같이 import 할 수 있다.

import * as validator from './ZipCodeValidator';

const myVlidator = new validator.ZipCodeValidator();

  

일부 모듈은 side-effect만을 위해 사용된다 (ex. polyfill, core-js 등). 이러한 모듈은 export 문을 포함하지 않을 수도 있고, 혹은 해당 모듈을 사용하는 입장에서 무엇이 export되는지 알 필요가 없을 수도 있다. 이러한 모듈은 아래와 같이 import 한다. (좋은 방법은 아니다.)

import './my-module.js';

  

타입스크립트에서 type을 import하기 위해서는 import type 문법을 사용했다. 하지만 3.8버전부터 import 키워드로 type을 import 할 수 있다.

// import 키워드 사용
import { APIResponseType } from "./api";

// import type 사용
import type { APIResponseType } from "./api";

import type 문은 컴파일 시 제거된다는 것이 보장된다.

   

Default exports

모듈은 선택적으로 default export 할 수 있다. Default export는 default 키워드를 사용하며, 하나의 모듈에서 한 번만 사용 가능하다. default export된 모듈을 import 할 때는 이전에 사용했던 문법과 다른 문법을 사용한다.

// JQuery.d.ts
declare let $: JQuery;
export default $;
// App.ts
import $ from 'jquery';

// 꼭 같은 이름으로 import 할 필요는 없다. 원하는 이름으로 import 할 수 있다.
// import jquery from 'jquery';

$("button.continue").html("Next Step...");

   

클래스 혹은 함수 선언 시 default 키워드를 바로 사용 할 수 있다. 이때 클래스 혹은 함수 이름 작성을 생략할 수 있다.

// ZipCodeValidator.ts

// with name
export default class ZipCodeValidator {
  static numberRegexp = /^[0-9]+$/;
  isAcceptable(s: string) {
    return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
  }
}
// ZipCodeValidator.ts

// without name
export default class {
  static numberRegexp = /^[0-9]+$/;
  isAcceptable(s: string) {
    return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
  }
}
// Tests.ts
import validator from "./ZipCodeValidator";

let myValidator = new validator();

  

함수, 클래스 뿐만 아닌 자바스크립트에서 값으로 평가되는 모든 것은 default export 할 수 있다.

// OneTwoThree.ts
export default '123';
// Log.ts
import num from "./OneTwoThree";

console.log(num); // "123"

   

export =, import = require()

타입스크립트는 CommonJS와 AMD를 모두 사용 할 수 있도록 export = 문법을 지원한다. export = 문법 사용 시 하나의 객체만을 export 할 수 있다. 이때 export 대상은 클래스, 인터페이스, 네임스페이스, 함수, 혹은 열거형(Enum)이 될 수 있다.

타입스크립트에서 export = 문법을 사용하여 export된 모듈을 import 할 때 import module = require("module") 문법을 사용해야한다.

// ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;

class ZipCodeValidator {
  isAcceptable(s: string) {
    return s.length === 5 && numberRegexp.test(s);
  }
}
export = ZipCodeValidator;
// Test.ts
import zip = require("./ZipCodeValidator");

let validator = new zip();

 

모듈 코드 생성

모듈 타겟이 무엇인지에 따라 컴파일된 코드가 달라진다. 아래는 각 타겟 별 SimpleModule 모듈을 컴파일한 결과이다.

// SimpleModule.ts
import m = require("mod");
export let t = m.something + 1;

   

Target: AMD (RequireJS)

// SimpleModule.js
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
  exports.t = mod_1.something + 1;
});

   

Target: CommonJS (Node)

// SimpleModule.js
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;

  

Target: UMD

// SimpleModule.js
(function (factory) {
  if (typeof module === "object" && typeof module.exports === "object") {
    var v = factory(require, exports);
    if (v !== undefined) module.exports = v;
  } else if (typeof define === "function" && define.amd) {
    define(["require", "exports", "./mod"], factory);
  }
})(function (require, exports) {
  var mod_1 = require("./mod");
  exports.t = mod_1.something + 1;
});

 

Target: System

// SimpleModule.js
System.register(["./mod"], function (exports_1) {
  var mod_1;
  var t;
  return {
    setters: [
      function (mod_1_1) {
        mod_1 = mod_1_1;
      },
    ],
    execute: function () {
      exports_1("t", (t = mod_1.something + 1));
    },
  };
});

   

Target: ES6

// SimpleModule.js
import { something } from "./mod";
export var t = something + 1;

  

선택적 모듈 로딩

컴파일러는 import된 모듈이 emit된 자바스크립트 파일에서 사용되는지 검사한다. 만약 모듈 식별자가 표현식이 아닌 타입 표기로만 사용된다면, 해당 모듈의 require 호출문은 emit된 자바스크립트 파일에 포함되지 않는다.

import id = require("...")문을 사용하면 해당 모듈의 타입에 접근 할 수 있다. 아래 코드는 Node.js에서 Dynamic 모듈 로딩을 구현한 예제이다.

declare function require(moduleName: string): any;

// 1. Zip은 타입 표기로만 사용된다. 즉, emit된 JS 파일에 require("./ZipCodeValidator")문이 포함되지 않는다.
import { ZipCodeValidator as Zip } from './ZipCodeValidator';

if (needZipValidation) {
  // 2. ZipCodeValidator가 필요한 경우, require문으로 import한다.
  let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
}

 

자바스크립트 라이브러리 사용하기 - Ambient 모듈

자바스크립트로 작성된 라이브러리의 구조를 나타내기 위해서는, 해당 라이브러리에서 제공하는 API를 선언(declare) 할 필요가 있다. implementation을 정의하지 않은 선언을 "Ambient"라고 한다. Ambient 선언은 보통 .d.ts 파일에 작성되어 있다.

// node.d.ts
declare module "url" {
  export interface Url {
    protocol?: string;
    hostname?: string;
    pathname?: string;
  }

  export function parse(
    urlStr: string,
    parseQueryString?: string,
    slashesDenoteHost?: string
  ): Url;
}

declare module "path" {
  export function normalize(p: string): string;
  export function join(...paths: any[]): string;
  export var sep: string;
}

   

위에서 작성한 Ambient 모듈을 사용하기 위해서는 node.d.ts 파일을 /// <reference>로 추가하면된다.

/// <reference path="node.d.ts"/>
import * as URL from 'url';
let myUrl = URL.parse("http://www.typescriptlang.org");

  

만약 위 예제에서 라이브러리 API를 선언하지 않고, 모듈을 바로 사용하고 싶다면 단축 선언(shorthand declaration)을 작성하면된다.

declare module "url";
import { parse } from 'url';

parse("...");
// 주의: shorthand declaration으로 작성된 모듈은 any type이다.

  

UMD 모듈

일부 라이브러리는 다양한 모듈 로더, 혹은 0개의 모듈 로더를 사용할 수 있도록 작성되어있다. 이러한 모듈을 UMD(Universal Module Definition) 모듈이라고 한다. UMD 모듈은 import하여 사용하거나, 전역 변수로서 사용한다. 아래 예제를 살펴보자.

// math-lib.d.ts
export function isPrime(x: number): boolean;
export as namespace mathLib;

math-lib 라이브러리를 모듈에서 사용 할 경우 import 하면된다.

import { isPrime } from "math-lib";

isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

math-lib 라이브러리를 전역 변수로 사용하기 위해서는 모듈 파일이 아닌 일반 스크립트 파일에서 사용해야한다.

mathLib.isPrime(2);

   

모듈 구조화 가이드

1. Export as close to top-level as possible

  • 모듈을 네임스페이스로 래핑하여 export하는 것은 불필요한 레이어를 추가하는 것일 수 있다. 모듈 사용자로 하여금 실수를 유발 할 수 있게 한다.
  • export된 클래스의 static 메소드를 사용할 때, 클래스 자체가 불필요한 레이어가 될 수 있다. 클래스를 네임스페이스로 사용하는 것이 작성된 코드의 의도를 더 뚜렷히 나타낼 수 있는 것이 아니라면 개별 함수를 export 하는 것이 바람직하다.
  • 만약 1개의 클래스 혹은 함수만을 export 한다면 export default 문법을 사용한다. default export된 모듈을 import 할 때 원하는 이름으로 rename 할 수 있고, 불필요한 . 체이닝을 줄일 수 있다.
  • 여러 모듈을 export 할 경우 top-level에 작성한다. (export * as namespace X)
  • 여러 모듈을 import 할 경우 import된 모듈 이름을 명시적으로 작성한다 (import * as namespace X). 단, import 하는 모듈이 너무 많을 경우 namespace import를 사용한다.

   

2. Re-export to extend

모듈의 기능을 확장할 경우, 기존의 모듈을 변경하지 않고 새로운 기능을 제공하는 개체를 export한다. 예를 들어, Calculator 클래스를 확장한 ProgrammerCalculator 클래스를 export할 때 아래와 같이 작성 할 수 있다.

// Calculator.ts
export class Calculator {
  private current = 0;
  private memory = 0;
  private operator: string;

  protected processDigit(digit: string, currentValue: number) {
    // ...
  }

  protected processOperator(operator: string) {
    // ...
  }

  protected evaluateOperator(
    operator: string,
    left: number,
    right: number
  ): number {
    // ...
  }

  private evaluate() {
    // ...
  }

  public handleChar(char: string) {
    // ...
  }

  public getResult() {
    // ...
  }
}

export function test(c: Calculator, input: string) {
  // ...
}
// ProgrammerCalculator.ts
import { Calculator } from "./Calculator";

class ProgrammerCalculator extends Calculator {
  static digits = [ /* ... */ ];

  constructor(public base: number) {
    super();
    // ...
  }

  protected processDigit(digit: string, currentValue: number) {
    // ...
  }
}

// 기존 Calculator를 변경하지 않고 확장하여 export
export { ProgrammerCalculator as Calculator };

// 기존 test를 re-export
export { test } from "./Calculator";

  

3. Do not use namespaces in modules

모듈은 자신만의 스코프를 가지고 있으며, 오직 export된 모듈만 외부에서 접근 할 수 있다. 이 사실 자체만으로 namespce는 큰 의미가 없다. namespace는 전역 스코프에서 이름 충돌의 위험이 있는 식별자를 계층적으로 구분하기 위해 사용된다. 하지만 모듈은 경로와 파일 이름을 resolve하여 사용되므로 이미 파일 시스템에 의해 계층이 구분되어 있다.

 

4. 주의사항

  • namespace만을 top-level export하는 모듈(ex. export namespace Foo {...})은 해당 namespace를 제거한 후 하위에 선언된 모든 것을 1레벨 올린다.
  • 여러 파일에서 작성된 top-level export namespace Foo {...}는 하나의 Foo로 병합되지 않는다.

   

출처

29