네임스페이스와 모듈

네임스페이스와 모듈 (Namespaces and Modules)

TypeScript에서 코드를 조직화하는 두 가지 축, 모듈(modules)과 네임스페이스(namespaces)는 언뜻 비슷해 보여서 언제 무엇을 써야 할지 헷갈리기 쉬워요. 이 문서는 두 방식을 정리하고, 각각을 쓸 때 흔히 저지르는 실수와 피하는 방법까지 다룹니다. ES 모듈에 대한 자세한 내용은 Modules, 네임스페이스에 대한 자세한 내용은 Namespaces 문서를 참고하면 돼요.

참고: 아주 오래된 TypeScript 버전에서는 네임스페이스를 '내부 모듈(Internal Modules)'이라고 불렀는데, 이는 JavaScript 모듈 시스템이 등장하기 전의 용어예요.

출처: TypeScript 핸드북

모듈 사용하기

모듈은 코드와 선언을 모두 담을 수 있어요. 모듈은 또한 CommonJS/Require.js 같은 모듈 로더나 ES 모듈을 지원하는 런타임에 의존합니다. 모듈은 더 나은 코드 재사용, 더 강한 격리, 번들링에 더 나은 도구 지원을 제공해요.

특히 Node.js 애플리케이션에서는 모듈이 기본이고, 현대적인 코드에서는 네임스페이스보다 모듈을 권장합니다. ECMAScript 2015부터 모듈은 언어의 고유한 부분이 되어, 모든 표준 준수 엔진이 지원해야 합니다. 그래서 새 프로젝트에는 모듈이 권장되는 코드 조직화 방식이에요.

네임스페이스 사용하기

네임스페이스는 TypeScript만의 코드 조직화 방식입니다. 네임스페이스는 그냥 전역 네임스페이스에 있는 이름 붙은 JavaScript 객체일 뿐이라, 쓰기에 매우 단순한 구조예요. 모듈과 달리 여러 파일에 걸칠 수 있고, outFile로 합칠 수도 있습니다. 모든 의존성을 HTML 페이지에 <script> 태그로 담는 웹 애플리케이션에서는 네임스페이스가 코드를 구조화하는 좋은 방법이 될 수 있어요.

다만 모든 전역 네임스페이스 오염이 그렇듯, 특히 큰 애플리케이션에서는 컴포넌트 간 의존성을 식별하기 어려울 수 있습니다.

네임스페이스와 모듈의 함정

이 섹션에서는 네임스페이스와 모듈을 사용할 때 자주 겪는 함정들과, 이를 피하는 방법을 차례대로 살펴볼게요.

모듈에 /// <reference> 하기

흔한 실수는 import 문 대신 /// <reference ... /> 문법으로 모듈 파일을 참조하려는 것입니다. 이 차이를 이해하려면 먼저 컴파일러가 import의 경로(예: import x from "...";, import x = require("...");...)를 바탕으로 모듈의 타입 정보를 어떻게 찾는지 알아야 해요.

컴파일러는 가장 먼저 적절한 경로의 .ts, .tsx, 그리고 .d.ts를 찾으려고 합니다. 특정 파일을 찾지 못하면 앰비언트 모듈 선언 을 찾게 되는데, 이 선언은 .d.ts 파일에 선언되어야 한다는 점을 기억하세요.

  • myModules.d.ts

    // In a .d.ts file or .ts file that is not a module:
    declare module "SomeModule" {
      export function fn(): string;
    }
    
  • myOtherModule.ts

    /// <reference path="myModules.d.ts" />
    import * as m from "SomeModule";
    

여기서 reference 태그는 앰비언트 모듈에 대한 선언이 담긴 선언 파일을 찾을 수 있게 해줘요. 여러 TypeScript 예시에서 쓰는 node.d.ts 파일이 바로 이런 방식으로 소비됩니다.

불필요한 네임스페이싱

프로그램을 네임스페이스에서 모듈로 변환하다 보면, 다음과 같은 파일이 되기 쉽습니다:

  • shapes.ts

    export namespace Shapes {
      export class Triangle {
        /* ... */
      }
      export class Square {
        /* ... */
      }
    }
    

여기서 최상위 네임스페이스 ShapesTriangleSquare를 아무 이유 없이 감싸고 있어요. 이는 모듈의 소비자에게 혼란스럽고 짜증나는 일입니다:

  • shapeConsumer.ts

    import * as shapes from "./shapes";
    let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
    

TypeScript 모듈의 핵심 특징은, 두 개의 서로 다른 모듈이 절대 같은 스코프에 이름을 기여하지 않는 다는 점이에요. 모듈의 소비자가 그 이름을 무엇으로 정할지 결정하기 때문에, 내보낸 심볼을 네임스페이스로 적극적으로 감쌀 필요가 없습니다.

모듈 내용을 네임스페이스로 묶지 말아야 하는 이유를 다시 말하면, 네임스페이싱의 일반적인 목적은 구조의 논리적 그룹화를 제공하고 이름 충돌을 막는 것이기 때문이에요. 모듈 파일 자체가 이미 논리적 그룹이고, 최상위 이름은 가져오는 코드가 정의하므로, 내보낸 객체에 추가적인 모듈 레이어를 쓸 필요가 없습니다.

수정된 예시를 볼게요:

  • shapes.ts

    export class Triangle {
      /* ... */
    }
    export class Square {
      /* ... */
    }
    
  • shapeConsumer.ts

    import * as shapes from "./shapes";
    let t = new shapes.Triangle();
    

모듈의 트레이드오프

JS 파일과 모듈이 일대일 대응하듯, TypeScript도 모듈 소스 파일과 그에 해당하는 방출된 JS 파일이 일대일로 대응해요. 이 때문에 대상 모듈 시스템에 따라 여러 모듈 소스 파일을 합치는 것이 불가능해질 수 있습니다. 예를 들어 commonjsumd를 대상으로 할 때는 outFile 옵션을 쓸 수 없지만, TypeScript 1.8 이상에서는 amdsystem을 대상으로 할 때 outFile을 쓸 수 있습니다.

더 알아보기 (Learn more)