이 포스트는 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
로 병합되지 않는다.
Top comments (0)