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 프로젝트 구조를 만들 수 있습니다.