29
TypeScript - Modules
이 포스트는 TypeScript - Modules를 번역 및 학습한 내용입니다.
ECMAScript 2015(ES6)에 추가된 모듈 기능을 TypeScript에서도 똑같이 사용 할 수 있다. 모듈은 자신만의 스코프를 가지고 있으며, 모듈 내부에 선언된 변수, 함수, 클래스 등은 export 되지 않는 한 외부에서 접근 할 수 없다.
export된 모듈은 다른 모듈에서 import
키워드를 통해 불러올 수 있다. 이를 가능하게 하는 것은 모듈 로더이다. 모듈 로더는 런타임에 import된 모듈(디펜던시)의 위치를 확인한다. 자바스크립트에서 사용되는 모듈 로더의 종류는 크게 두 가지이다.
- CommonJS 모듈을 위한 Node.js의 로더
- AMD 모듈을 위한 RequireJS 로더
import
, 혹은 export
키워드를 포함한 파일은 모듈로 처리된다. 그 외(import
, 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
키워드를 사용하여 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 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"
타입스크립트는 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");
}
자바스크립트로 작성된 라이브러리의 구조를 나타내기 위해서는, 해당 라이브러리에서 제공하는 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이다.
일부 라이브러리는 다양한 모듈 로더, 혹은 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);
- 모듈을 네임스페이스로 래핑하여 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를 사용한다.
모듈의 기능을 확장할 경우, 기존의 모듈을 변경하지 않고 새로운 기능을 제공하는 개체를 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";
모듈은 자신만의 스코프를 가지고 있으며, 오직 export된 모듈만 외부에서 접근 할 수 있다. 이 사실 자체만으로 namespce는 큰 의미가 없다. namespace는 전역 스코프에서 이름 충돌의 위험이 있는 식별자를 계층적으로 구분하기 위해 사용된다. 하지만 모듈은 경로와 파일 이름을 resolve하여 사용되므로 이미 파일 시스템에 의해 계층이 구분되어 있다.
- namespace만을 top-level export하는 모듈(ex.
export namespace Foo {...}
)은 해당 namespace를 제거한 후 하위에 선언된 모든 것을 1레벨 올린다. - 여러 파일에서 작성된 top-level
export namespace Foo {...}
는 하나의Foo
로 병합되지 않는다.
29