iBetter Books
수정

Ch 04. 네임스페이스와 모듈 패턴

TypeScript에는 코드를 조직화하는 두 가지 방법이 있습니다. 하나는 ES 모듈 시스템(파일 기반), 다른 하나는 네임스페이스(namespace 키워드)입니다. 현대 TypeScript 프로젝트에서는 ES 모듈이 표준이지만, 네임스페이스는 타입 선언과 대규모 SDK에서 여전히 의미 있는 역할을 합니다.

네임스페이스 기본 문법

// 파일: src/namespace/geometry.tsnamespace Geometry {  export interface Point {    x: number;    y: number;  }  export interface Circle {    center: Point;    radius: number;  }  export function distance(a: Point, b: Point): number {    return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);  }  export function circleArea(circle: Circle): number {    return Math.PI * circle.radius ** 2;  }  // 중첩 네임스페이스  export namespace ThreeD {    export interface Point3D {      x: number;      y: number;      z: number;    }    export function distance3D(a: Point3D, b: Point3D): number {      return Math.sqrt(        (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2      );    }  }}// 사용const p1: Geometry.Point = { x: 0, y: 0 };const p2: Geometry.Point = { x: 3, y: 4 };console.log(Geometry.distance(p1, p2)); // 5const p3d: Geometry.ThreeD.Point3D = { x: 0, y: 0, z: 0 };

네임스페이스 vs ES 모듈

// 네임스페이스 방식namespace Api {  export namespace Users {    export interface User { id: number; name: string; }    export function getUser(id: number): User { /* ... */ return { id, name: "" }; }  }  export namespace Products {    export interface Product { id: number; title: string; }    export function getProduct(id: number): Product { /* ... */ return { id, title: "" }; }  }}// 사용const user = Api.Users.getUser(1);const product = Api.Products.getProduct(1);
// ES 모듈 방식 (현대적, 권장)// users.tsexport interface User { id: number; name: string; }export function getUser(id: number): User { /* ... */ return { id, name: "" }; }// products.tsexport interface Product { id: number; title: string; }export function getProduct(id: number): Product { /* ... */ return { id, title: "" }; }// app.tsimport { User, getUser } from "./users";import { Product, getProduct } from "./products";

현대 TypeScript 프로젝트에서는 ES 모듈이 표준입니다. 네임스페이스를 새로 만드는 것은 권장하지 않습니다. 다만 다음 경우에는 네임스페이스가 여전히 유용합니다.

  • .d.ts 파일에서 라이브러리 API를 구조화할 때
  • 기존 레거시 코드와의 호환성이 필요할 때
  • 단일 번들 파일로 배포하는 브라우저 라이브러리

대규모 프로젝트 모듈 구조화

ES 모듈 기반으로 대규모 프로젝트를 구조화하는 실전 패턴입니다.

src/
  domain/            # 비즈니스 로직
    user/
      user.model.ts
      user.service.ts
      user.repository.ts
      index.ts        # 배럴 파일
    product/
      product.model.ts
      product.service.ts
      index.ts
  infrastructure/    # 외부 시스템 연동
    database/
      connection.ts
      query-builder.ts
      index.ts
    http/
      client.ts
      interceptors.ts
      index.ts
  api/               # API 라우트
    v1/
      users.ts
      products.ts
      index.ts
  shared/            # 공통 유틸리티
    types/
    utils/
    constants/
    index.ts
// 파일: src/domain/user/index.tsexport type { User, CreateUserDTO, UpdateUserDTO } from "./user.model";export { UserService } from "./user.service";// repository는 내부 구현이므로 export하지 않음
// 파일: src/shared/types/index.tsexport type { ApiResponse, ApiError, PaginatedResponse } from "./api.types";export type { DeepPartial, DeepReadonly, Optional } from "./utility.types";

모듈 경계와 접근 제어

TypeScript에는 Java나 C#의 접근 제어자(패키지 전용 등)가 없습니다. 배럴 파일로 공개 API를 명시적으로 제한하는 방식으로 모듈 경계를 만듭니다.

// 파일: src/domain/order/order.repository.ts// 이 파일은 직접 import하면 안 됨 (index.ts를 통해서만)export class OrderRepository {  private db: unknown;  async findById(id: number) {    // 내부 구현  }  async save(order: unknown) {    // 내부 구현  }}
// 파일: src/domain/order/index.ts// OrderRepository는 외부에 노출하지 않음export type { Order, CreateOrderDTO } from "./order.model";export { OrderService } from "./order.service";// OrderRepository는 의도적으로 export하지 않음

순환 의존성 방지

모듈 간 순환 참조는 런타임 오류나 예기치 않은 동작을 유발합니다.

// 나쁜 예시 — 순환 의존성// user.tsimport { Post } from "./post"; // post.ts에 의존export interface User { id: number; posts: Post[]; }// post.tsimport { User } from "./user"; // user.ts에 의존 — 순환!export interface Post { id: number; author: User; }
// 좋은 예시 — 공통 타입을 분리// 파일: src/types/entities.tsexport interface User {  id: number;  name: string;}export interface Post {  id: number;  authorId: number; // User 대신 ID만 참조  title: string;}// 파일: src/domain/user/user.service.tsimport { User } from "@types/entities"; // 공통 타입 참조// 파일: src/domain/post/post.service.tsimport { Post } from "@types/entities"; // 공통 타입 참조

앰비언트 모듈로 비 JS 파일 임포트

CSS, SVG, 이미지 파일 등을 TypeScript에서 import할 때 타입 선언이 필요합니다.

// 파일: src/types/assets.d.ts// CSS 모듈declare module "*.module.css" {  const styles: { readonly [className: string]: string };  export default styles;}// SVG 파일declare module "*.svg" {  const content: string;  export default content;}// SVG를 React 컴포넌트로declare module "*.svg?react" {  import { FC, SVGProps } from "react";  const component: FC<SVGProps<SVGSVGElement>>;  export default component;}// JSON 파일 (--resolveJsonModule으로 해결되지 않는 특수 경우)declare module "*.json" {  const value: unknown;  export default value;}

모듈과 선언의 올바른 설계는 코드베이스가 커질수록 더 중요해집니다. ES 모듈로 파일 단위 응집도를 높이고, 배럴 파일로 공개 API를 명확히 정의하고, 선언 파일로 타입 정보를 보강하는 세 가지 원칙을 지키면 확장 가능한 TypeScript 프로젝트 구조를 만들 수 있습니다.