네임스페이스

네임스페이스 (Namespaces)

코드를 조직화하는 방법은 여러 가지가 있어요. 그중 네임스페이스(namespace)는 이름 충돌을 막으면서 타입과 코드를 논리적으로 묶는 TypeScript의 전통적인 방식입니다. 다만 지금은 이 개념을 잘 아는 것도 중요하지만, 새 프로젝트에서는 모듈을 쓰는 게 권장된다는 점을 함께 기억해 두면 좋아요.

참고(용어): TypeScript 1.5에서 명칭이 바뀌었다는 점을 알아둬야 해요. "내부 모듈(internal modules)"이 이제 "네임스페이스(namespaces)"이고, "외부 모듈(external modules)"은 그냥 "모듈(modules)"입니다. 이는 ECMAScript 2015의 용어에 맞춘 건데요, module X {와 지금 선호되는 namespace X {가 동일하다는 뜻입니다.

이 페이지에서는 TypeScript에서 네임스페이스(이전의 "내부 모듈")로 코드를 조직화하는 다양한 방법을 다룰게요. 앞서 용어 설명에서 언급했듯 "내부 모듈"은 이제 "네임스페이스"라고 불러요. 내부 모듈을 선언할 때 module 키워드를 쓰던 자리에는 앞으로 namespace 키워드를 쓸 수 있고, 써야 합니다. 비슷한 이름의 용어가 겹쳐 신규 사용자를 헷갈리게 하지 않으려는 조치예요.

출처: TypeScript 핸드북

첫 단계

이 페이지 전체에서 예시로 쓸 프로그램부터 시작할게요. 웹페이지 폼에서 사용자 입력을 검사하거나, 외부에서 제공된 데이터 파일의 형식을 검사할 때 쓸 법한, 단순한 문자열 검증기(StringValidator)들을 조금 작성해 볼게요.

단일 파일 안의 검증기들

interface StringValidator {
  isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
  isAcceptable(s: string) {
    return lettersRegexp.test(s);
  }
}

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

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    let isMatch = validators[name].isAcceptable(s);
    console.log(`'${s}' ${isMatch ? "matches" : "does not match"} '${name}'.`);
  }
}

네임스페이싱

검증기가 늘어날수록 타입들을 정리하고 다른 객체와의 이름 충돌을 걱정하지 않도록, 어떤 조직화 체계가 필요해지기 시작해요. 수많은 서로 다른 이름을 전역 네임스페이스에 넣는 대신, 우리 객체들을 하나의 네임스페이스로 감싸 볼게요.

이 예시에서는 검증기 관련 엔티티 전부를 Validation이라는 네임스페이스 안으로 옮기겠습니다. 여기 있는 인터페이스와 클래스가 네임스페이스 밖에서도 보여야 하므로 export를 앞에 붙여줘요. 반대로 변수 lettersRegexpnumberRegexp는 구현 세부 사항이므로 내보내지 않고, 네임스페이스 바깥의 코드에서는 보이지 않게 둡니다. 파일 아래쪽의 테스트 코드에서는 이제 네임스페이스 밖에서 타입 이름을 한정해 줘야 해요. 예를 들어 Validation.LettersOnlyValidator처럼요.

네임스페이스가 적용된 검증기들

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;
  const numberRegexp = /^[0-9]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }

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

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
}

여러 파일로 쪼개기

애플리케이션이 커지면 코드를 여러 파일로 쪼개 관리하기 쉽게 만들고 싶어질 거예요.

다중 파일 네임스페이스

여기서는 Validation 네임스페이스를 여러 파일로 쪼개 볼게요. 파일이 분리되어 있어도 각 파일이 같은 네임스페이스에 기여할 수 있고, 마치 한 곳에 모두 정의된 것처럼 사용할 수 있어요. 파일 사이에 의존성이 있으므로, 컴파일러에게 파일 간의 관계를 알려주는 reference 태그를 추가할게요. 테스트 코드는 그 외에는 변함이 없습니다.

Validation.ts
namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;
  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
  const numberRegexp = /^[0-9]+$/;
  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}
Test.ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${
        validators[name].isAcceptable(s) ? "matches" : "does not match"
      } ${name}`
    );
  }
}

파일이 여러 개가 되면 컴파일된 코드가 전부 로드되도록 신경 써야 해요. 방법은 두 가지입니다.

첫째, outFile 옵션으로 모든 입력 파일을 하나의 JavaScript 출력 파일로 합쳐 컴파일할 수 있어요:

tsc --outFile sample.js Test.ts

컴파일러는 파일 안에 있는 reference 태그를 기준으로 출력 파일의 순서를 자동으로 정렬합니다. 각 파일을 개별적으로 지정할 수도 있어요:

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

둘째, 파일별 컴파일(기본값)을 써서 입력 파일 하나마다 JavaScript 파일 하나를 만들 수도 있어요. JS 파일이 여러 개 만들어지면 웹페이지에서 <script> 태그로 각 파일을 적절한 순서대로 로드해야 합니다. 예를 들면:

MyTestPage.html (일부)
<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" />

별칭 (Aliases)

네임스페이스 작업을 단순화하는 또 다른 방법으로, import q = x.y.z를 써서 자주 쓰는 객체에 짧은 이름을 붙일 수 있어요. 모듈을 로드할 때 쓰는 import x = require("name") 문법과 혼동하지 말아야 하는데, 이 문법은 단지 지정된 심볼에 대한 별칭(alias)을 만들어 줄 뿐입니다. 모듈 임포트로 만든 객체를 포함해 어떤 종류의 식별자에도 이런 형태의 import(보통 별칭이라 함)를 쓸 수 있어요.

namespace Shapes {
  export namespace Polygons {
    export class Triangle {}
    export class Square {}
  }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as 'new Shapes.Polygons.Square()'

require 키워드는 쓰지 않는다는 점에 주목하세요. 대신 가져오는 심볼의 한정된 이름에서 직접 할당해요. 이것은 var를 쓰는 것과 비슷하지만, 가져온 심볼의 타입 의미와 네임스페이스 의미에도 적용된다는 차이가 있습니다. 중요한 점은 값의 경우 import는 원래 심볼과 별개의 참조라는 것인데, 별칭을 만든 var의 변경이 원래 변수에는 반영되지 않아요.

다른 JavaScript 라이브러리와 함께 작업하기

TypeScript로 작성되지 않은 라이브러리의 형태(shape)를 기술하려면, 그 라이브러리가 노출하는 API를 선언해야 해요. 대부분의 JavaScript 라이브러리는 최상위 객체를 몇 개만 노출하므로, 네임스페이스가 이를 나타내기에 좋은 방법입니다.

구현을 정의하지 않는 선언을 우리는 "앰비언트(ambient)" 선언이라고 불러요. 보통 이런 선언은 .d.ts 파일에 정의됩니다. C/C++을 안다면 .h 파일로 생각하면 됩니다. 몇 가지 예를 볼게요.

앰비언트 네임스페이스

널리 쓰이는 라이브러리 D3는 d3라는 전역 객체에 기능을 정의해요. 이 라이브러리는 모듈 로더 대신 <script> 태그로 로드되므로, 형태를 정의하는 선언이 네임스페이스를 사용합니다. TypeScript 컴파일러가 이 형태를 보게 하려면 앰비언트 네임스페이스 선언을 써야 해요. 다음과 같이 작성하면 됩니다.

D3.d.ts (단순화된 일부)
declare namespace D3 {
  export interface Selectors {
    select: {
      (selector: string): Selection;
      (element: EventTarget): Selection;
    };
  }

  export interface Event {
    x: number;
    y: number;
  }

  export interface Base extends Selectors {
    event: Event;
  }
}

declare var d3: D3.Base;

더 알아보기 (Learn more)