네임스페이스 사용법
namespace로 이름을 묶고 export로 공개 범위를 정하는 방법과, 여러 블록의 병합, import 별칭, 출력되는 코드를 예제로 확인하고, ES 모듈이 표준이 된 지금 네임스페이스를 쓰는 자리와 TypeScript 6.0에서 바뀐 점, 모듈로 옮길 때 확인할 것을 정리합니다.
ES 모듈이 자바스크립트의 표준이 되기 전, 타입스크립트는 관련된 코드를 하나의 이름 아래 묶기 위해 네임스페이스(namespace)를 제공했습니다. 브라우저가 여러 스크립트 파일을 차례로 읽어 하나의 전역 스코프를 함께 쓰던 시절에는, 전역에 이름 하나만 두고 그 안에 나머지를 넣어 이름이 부딪히는 일을 줄이는 방법이 필요했기 때문입니다.
지금 새로 만드는 프로젝트라면 파일을 나누고 묶는 일은 앞의 두 글에서 본 ES 모듈이 맡습니다. 그래도 오래된 코드를 읽거나, 8장에서 다룰 선언 파일에서 전역 라이브러리의 모양을 적을 때는 여전히 네임스페이스를 만납니다. 이 글은 네임스페이스의 문법과 출력을 확인하고, 지금 네임스페이스를 쓰는 자리와 모듈로 옮길 때 살필 점을 정리합니다.
TYPE / RUNTIME / CONTRACT
namespace의 문맥과 공개 범위를 먼저 구분한다
본문 Validators는 전역 스크립트의 실행 코드입니다. 구현이 있는 namespace와 선언만 있는 namespace, 모듈 안의 namespace는 역할과 범위가 다릅니다.
이 그림은 원래 단일 파일 Validators의 실행 클래스와 함수 범위입니다. app은 공개 클래스를 사용하고 LettersOnlyValidator는 같은 블록의 내부 함수를 사용합니다. interface는 타입 선언이므로 별도 실행 노드로 그리지 않았습니다.
이 영역은 import/export 없는 스크립트의 선언 범위를 설명합니다. Node의 CommonJS 파일에서 실행하는 별도 실습은 파일 실행 범위 안에 머물며 브라우저 일반 script의 전역 객체 등록을 뜻하지 않습니다.
실행 스크립트
Validators의 공개 멤버
ZipCodeValidator와 LettersOnlyValidator는 export되어 밖에서 사용합니다. "12345"와 "Hello" 검사 결과는 각각 true입니다.
블록 내부
isLetter는 밖에서 보이지 않는다
isLetter는 선언한 namespace 블록 내부에서만 보입니다. 같은 이름으로 병합된 다른 블록에서도 이 함수에 접근할 수 없습니다.
다중 파일
같은 선언 스코프에서 병합
분할한 Validators.ZipCode.ts와 Validators.LettersOnly.ts를 함께 컴파일합니다. 실행 시에도 구현이 사용 코드보다 먼저 준비되어야 합니다. 단일 파일 예제와 중복 구현을 함께 넣지 않습니다.
별칭
기존 이름에 짧은 이름을 붙인다
import Validator = MyUtility.DataValidation은 모듈을 가져오는 import가 아닙니다. 기존 namespace에 별칭을 붙여 PhoneNumberValidator를 사용합니다.
선언 / 모듈 · 보충
값 생성과 파일 범위를 구분
declare namespace는 기존 API를 타입으로 설명하며 값을 만들지 않습니다. 외부 모듈 안의 namespace는 그 모듈의 범위에 머뭅니다.
ESM으로 전환
공개 API와 실행 순서를 보존
전역 실행 코드를 옮길 때 루트 이름, 공개 멤버, 숨겨진 상태, 사용 파일과 초기화 순서를 확인합니다. 기존 전역 API의 타입 선언은 별도로 검토합니다.
namespace는 export하지 않은 내부 멤버를 숨기지만 전역 스크립트의 루트 이름까지 없애지는 않습니다. 새 파일 의존성은 import/export 경계로 표현할 수 있습니다.
위 그림은 네임스페이스가 어떤 문맥에 쓰였는지부터 나누어 봅니다. 전역 스크립트에서 실제로 실행되는 코드를 묶은 네임스페이스와, 선언 파일에서 모양만 적은 네임스페이스, 모듈 안에 들어 있는 네임스페이스는 역할과 범위가 서로 다릅니다.
선언과 공개 범위
네임스페이스는 namespace 이름 { ... }으로 선언합니다. 블록 안의 선언은 그 블록의 스코프에 속하며, export를 붙인 것만 이름.멤버처럼 바깥에서 쓸 수 있습니다.
namespace Validators {
export interface StringValidator {
isValid(s: string): boolean;
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return s.length === 5 && /^\d+$/.test(s);
}
}
function isLetter(char: string): boolean {
return /[a-zA-Z]/.test(char);
}
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
for (const char of s) {
if (!isLetter(char)) {
return false;
}
}
return true;
}
}
}
const zip = new Validators.ZipCodeValidator();
const letters = new Validators.LettersOnlyValidator();
console.log(zip.isValid("12345"), zip.isValid("1234"));
console.log(letters.isValid("Hello"), letters.isValid(""), letters.isValid("A1"));isLetter에는 export가 없으므로 블록 안의 LettersOnlyValidator는 쓸 수 있지만, 바깥에서 Validators.isLetter("a")를 부르면 Property 'isLetter' does not exist on type 'typeof Validators'.로 진단됩니다. 그런데 LettersOnlyValidator는 글자를 하나씩 검사하므로, 빈 문자열에서는 반복이 한 번도 돌지 않아 true를 돌려줍니다. 빈 입력을 허용할지는 네임스페이스와 관계없는 검증의 규칙이므로, 이런 코드를 옮길 때 함께 정해야 합니다.
이 파일을 실행하려면 새 폴더 ts-namespace에 package.json을 { "type": "commonjs" }로 두고, 1장의 ts-first와 같은 모양의 tsconfig.json, 곧 rootDir 없이 outDir만 dist로 둔 설정에서 include를 ["legacy-demo.ts"]로 정합니다. tsc -p .으로 컴파일하고 node dist/legacy-demo.js를 실행하면 다음이 출력됩니다.
true false
true true false우편번호 검사는 다섯 자리 숫자인 "12345"만 통과하고, 글자 검사는 "Hello"와 빈 문자열을 통과시키며 숫자가 섞인 "A1"은 거절합니다.
출력되는 코드
네임스페이스는 타입 표기와 달리 실행되는 코드를 만듭니다. Validators를 컴파일하면, 같은 이름의 변수에 객체를 만들고 즉시 실행하는 함수 안에서 export한 멤버를 그 객체의 속성으로 붙이는 코드가 됩니다. 다음은 출력의 앞부분입니다.
"use strict";
var Validators;
(function (Validators) {
class ZipCodeValidator {
isValid(s) {
return s.length === 5 && /^\d+$/.test(s);
}
}
Validators.ZipCodeValidator = ZipCodeValidator;
// isLetter와 LettersOnlyValidator도 같은 방식으로 이어진다
})(Validators || (Validators = {}));인터페이스 StringValidator는 타입이어서 출력에 남지 않고, export하지 않은 isLetter는 함수 안의 지역 함수로만 남습니다. 이렇게 새 코드를 만들어 내는 문법이어서, 1장에서 본 Node.js의 타입 제거 실행은 실행 코드가 들어 있는 네임스페이스를 ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX 오류로 거부하고, erasableSyntaxOnly를 켜면 컴파일러도 진단합니다.
여러 블록의 병합과 별칭
같은 이름의 네임스페이스를 여러 번 선언하면, 같은 스코프 안에서 하나로 합쳐집니다. 전역 스크립트라면 다른 파일에 나누어 적은 블록도 합쳐지므로, 예전에는 큰 네임스페이스를 여러 파일로 나누어 관리했습니다.
namespace Validators {
export class EmailValidator {
isValid(s: string): boolean {
return /^[^@\s]+@[^@\s]+$/.test(s);
}
}
}이 블록을 앞의 파일에 더하면 Validators.EmailValidator도 쓸 수 있습니다. 다만 합쳐지는 것은 export한 멤버뿐이어서, 새 블록 안에서는 앞 블록의 isLetter를 부를 수 없습니다. 여러 파일로 나눈 네임스페이스를 실행하려면 브라우저에서 스크립트를 올바른 순서로 불러오거나 출력을 한 파일로 합쳐야 했는데, 합치는 데 쓰던 outFile 옵션은 TypeScript 6.0에서 폐기 예정으로 지정되었습니다.
중첩된 네임스페이스의 긴 이름에는 import 별칭 = 이름;으로 짧은 이름을 붙일 수 있습니다.
namespace MyUtility.DataValidation {
export class PhoneNumberValidator {
isValid(phone: string): boolean {
return phone.length > 7;
}
}
}
import Validator = MyUtility.DataValidation;
const phoneValidator = new Validator.PhoneNumberValidator();
console.log(phoneValidator.isValid("123-456-7890"));trueimport Validator = MyUtility.DataValidation;은 다른 파일을 불러오는 ES 모듈의 import가 아니라, 이미 있는 이름에 별칭을 붙이는 타입스크립트 전용 문법입니다. 이 별칭도 출력에서 변수가 되므로 타입 제거 실행에서는 지원하지 않습니다. 또 아주 오래된 코드에는 namespace 대신 module Validators { }라고 적은 것이 있는데, TypeScript 6.0에서 이 module 키워드의 사용은 폐기 예정으로 지정되어 namespace로 바꾸어야 합니다.
더 깊이: 클래스와 함수, 열거형에 합쳐지는 네임스페이스
네임스페이스는 같은 이름의 네임스페이스끼리만 합쳐지는 것이 아닙니다. 공식 Handbook의 선언 병합 글에 따르면, 네임스페이스는 같은 이름의 클래스와 함수, 열거형 뒤에 선언하면 그 선언과도 합쳐집니다. 클래스와 합치면 네임스페이스에서 export한 멤버가 클래스의 정적 멤버처럼 붙어, Album.AlbumLabel 같은 안쪽 클래스를 나타낼 수 있습니다. 함수와 합치면 buildLabel.prefix처럼 함수에 속성을 단 모양을, 열거형과 합치면 Color.mixColor 같은 함수를 더한 모양을 타입 안전하게 적을 수 있습니다. 이때 네임스페이스는 합쳐질 선언보다 뒤에 와야 하며, 클래스끼리나 클래스와 변수는 합칠 수 없습니다.
이 가운데 함수에 속성을 다는 경우는 지금은 네임스페이스 없이도 됩니다. TypeScript 3.1부터는 함수 선언이나 const로 선언한 함수에, 같은 스코프에서 readImage.sync = ...처럼 속성을 대입하기만 하면 컴파일러가 그 속성을 함수의 타입에 넣어 줍니다. 릴리스 노트는 이것을 네임스페이스로 우회하지 않고 보통의 자바스크립트로 적을 수 있게 한 변화라고 소개합니다. 그래서 선언 병합으로 함수에 속성을 붙인 오래된 코드를 만나면, 모듈로 옮길 때 이 대입 형태로 바꾸는 것도 방법입니다.
지금 네임스페이스를 쓰는 자리
새 코드에서 파일을 나누고 이어 주는 일은 ES 모듈이 맡습니다. 모듈은 파일마다 스코프가 따로 있어 전역에 이름을 남기지 않고, 가져오는 관계가 코드에 그대로 적혀 있어 번들러가 쓰지 않는 코드를 빼내기도 쉬우며, 무엇보다 자바스크립트의 표준입니다. 네임스페이스는 타입스크립트만의 기능이고, 전역 스크립트에 쓰면 루트 이름 하나는 결국 전역에 남습니다.
그래도 네임스페이스가 쓰이는 자리는 남아 있습니다. 대표적인 것이 선언 파일에서 declare namespace로 전역 라이브러리의 모양을 적는 경우로, 이것은 실행 코드를 만들지 않고 이미 있는 전역 객체의 타입만 설명합니다. 또 모듈 안에서 관련된 타입을 한 이름 아래 묶는 데 쓰기도 하는데, 이때는 모듈 스코프 안에 머물므로 전역을 더럽히지 않습니다.
오래된 네임스페이스 코드를 모듈로 옮길 때는 먼저 전역에 남는 루트 이름과 export한 멤버의 목록을 찾아, 그것을 파일 단위의 export로 옮깁니다. 그다음 스크립트를 불러오던 순서에 기대던 의존 관계를 import로 바꾸어 적고, 앞에서 본 빈 문자열처럼 검증의 규칙이 옮기는 동안 바뀌지 않았는지 테스트로 확인합니다. 네임스페이스의 문법은 공식 Handbook의 네임스페이스에 있습니다.
정리
- 네임스페이스는
namespace 이름 { }으로 코드를 묶고,export한 멤버만이름.멤버로 바깥에서 쓸 수 있습니다. - 네임스페이스는 즉시 실행하는 함수로 객체를 만드는 코드를 출력하므로, Node.js의 타입 제거 실행이나
erasableSyntaxOnly와 함께 쓸 수 없습니다. - 같은 이름의 블록은 같은 스코프에서 합쳐지지만, 합쳐지는 것은
export한 멤버뿐입니다. import 별칭 = 이름은 네임스페이스에 별칭을 붙이는 전용 문법이며, TypeScript 6.0에서outFile과module키워드의 네임스페이스는 폐기 예정입니다.- 새 코드의 파일 구성은 ES 모듈로 하고, 네임스페이스는 선언 파일의
declare namespace처럼 모양을 적는 자리에 주로 씁니다.
연습 문제
1. 정규식으로 바꾼 검사
LettersOnlyValidator의 본문을 return /^[A-Za-z]+$/.test(s);로 바꾸면 "AB", "", "A1"의 결과는 각각 무엇인가요? 빈 문자열도 통과시키려면 정규식을 어떻게 바꾸어야 하나요?
해설 보기
결과는 차례로 true, false, false입니다. +는 글자가 하나 이상 있어야 한다는 뜻이어서 빈 문자열은 통과하지 못하며, 원래 반복문과 결과가 다릅니다. 빈 문자열도 통과시키려면 +를 0개 이상을 뜻하는 *로 바꾸는데, 이것은 공개 범위와 관계없는 검증 규칙의 변경이므로 의도적으로 고릅니다.
2. 다른 블록에서 내부 함수 쓰기
앞에서 더한 EmailValidator 블록 안에서 isLetter("a")를 부르면 어떻게 되나요? 그 함수를 두 블록에서 함께 쓰려면 어떻게 해야 하나요?
해설 보기
isLetter는 처음 블록에서 export하지 않은 지역 함수여서, 같은 이름으로 합쳐진 다른 블록에서도 보이지 않으므로 Cannot find name 'isLetter'.로 진단됩니다. 두 블록에서 함께 쓰려면 export function isLetter로 공개해야 하는데, 그러면 바깥에서도 Validators.isLetter로 쓸 수 있게 됩니다.
3. 모듈로 옮기기
Validators 네임스페이스를 validators.ts라는 ES 모듈로 옮긴다면, 무엇을 export하고 사용하는 쪽은 어떻게 가져와야 하나요?
해설 보기
namespace 블록을 없애고, 블록에서 export했던 StringValidator, ZipCodeValidator, LettersOnlyValidator에 파일 단위의 export를 붙입니다. 공개하지 않던 isLetter는 export 없이 두면 모듈 안에서만 쓰입니다. 사용하는 쪽은 import { ZipCodeValidator, LettersOnlyValidator } from "./validators.js";로 필요한 것만 가져오고, 기존처럼 Validators. 접두사를 유지하고 싶다면 import * as Validators from "./validators.js";로 네임스페이스 가져오기를 씁니다.
다음 글 모듈 해석 전략에서는 import에 적은 경로를 컴파일러가 실제 파일로 찾아가는 규칙과, moduleResolution과 paths가 그 과정에 어떻게 끼어드는지 다룹니다.