본문 바로가기
Dev/TypeScript

TypeScript 모듈 심층 분석: 디자인 패턴, 번들러 연동, 그리고 고급 활용

by ZEROGOON 2024. 12. 18.

TypeScript는 JavaScript에 정적 타입을 추가하여 코드의 안정성과 유지 보수성을 향상시키는 강력한 도구입니다. TypeScript의 핵심 기능 중 하나는 바로 모듈 시스템입니다. 모듈을 사용하면 코드를 논리적인 단위로 분리하여 구성하고, 재사용성을 높이며, 이름 충돌을 방지할 수 있습니다. 이 블로그 게시글에서는 TypeScript 모듈을 심층적으로 분석하고, 디자인 패턴과의 연관성, 모듈 번들러와의 연동, 그리고 다른 TypeScript 기능과의 조합을 자세한 예시와 함께 살펴보겠습니다.

1. TypeScript 모듈과 디자인 패턴

모듈은 코드를 구성하는 기본적인 단위로서, 다양한 디자인 패턴을 구현하는 데 중요한 역할을 합니다. 몇 가지 대표적인 예시를 살펴보겠습니다.

  • 리포지토리 패턴 (Repository Pattern): 데이터 접근 로직을 추상화하여 비즈니스 로직과 분리하는 패턴입니다. 모듈을 사용하여 데이터 접근 로직을 캡슐화할 수 있습니다.
// data-source.ts (데이터 소스 모듈)
export interface User {
  id: number;
  name: string;
}

export interface DataSource {
  getUsers(): Promise<User[]>;
  getUser(id: number): Promise<User | undefined>;
}

export class MockDataSource implements DataSource {
  private users: User[] = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];

  async getUsers(): Promise<User[]> {
    return this.users;
  }

  async getUser(id: number): Promise<User | undefined> {
    return this.users.find(user => user.id === id);
  }
}

// user-repository.ts (리포지토리 모듈)
import { User, DataSource, MockDataSource } from './data-source';

export interface UserRepository {
    getUsers(): Promise<User[]>;
    getUser(id: number): Promise<User | undefined>;
}

export class UserRepositoryImpl implements UserRepository {
    private dataSource: DataSource;

    constructor(dataSource: DataSource) {
        this.dataSource = dataSource;
    }

    async getUsers(): Promise<User[]> {
        return this.dataSource.getUsers();
    }

    async getUser(id: number): Promise<User | undefined> {
        return this.dataSource.getUser(id);
    }
}

// main.ts
import { UserRepositoryImpl, MockDataSource } from './user-repository';

const userRepository = new UserRepositoryImpl(new MockDataSource());

userRepository.getUsers().then(users => console.log(users));
  • 서비스 로케이터 패턴 (Service Locator Pattern): 서비스들을 중앙 집중식으로 관리하고 제공하는 패턴입니다. 모듈을 사용하여 각 서비스를 캡슐화하고, 서비스 로케이터 모듈을 통해 접근할 수 있도록 구현할 수 있습니다. (이 패턴은 의존성 주입(Dependency Injection) 패턴으로 대체되는 추세입니다.)
// logger.ts
export interface Logger {
  log(message: string): void;
}

export class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

// service-locator.ts
import { Logger, ConsoleLogger } from './logger';

const services = new Map<string, any>();

export function registerService(name: string, service: any) {
  services.set(name, service);
}

export function getService<T>(name: string): T {
  return services.get(name) as T;
}

registerService('logger', new ConsoleLogger());

// main.ts
import { getService } from './service-locator';
import { Logger } from './logger';

const logger = getService<Logger>('logger');
logger.log('Hello, world!');

2. TypeScript 모듈과 모듈 번들러 (Webpack, Rollup)의 연동

모듈 번들러는 여러 개의 모듈 파일을 하나의 파일로 묶어주는 도구입니다. Webpack과 Rollup은 대표적인 모듈 번들러이며, TypeScript와 함께 사용하여 효율적인 웹 애플리케이션 개발을 가능하게 합니다.

  • Webpack: 다양한 기능을 제공하는 강력한 모듈 번들러입니다. TypeScript 로더를 사용하여 TypeScript 코드를 JavaScript로 변환하고, 다른 자원들과 함께 번들링할 수 있습니다.
  • Rollup: 작은 크기의 라이브러리 번들링에 특화된 모듈 번들러입니다. Webpack보다 설정이 간단하며, tree shaking (사용하지 않는 코드 제거) 기능을 통해 번들 크기를 최적화할 수 있습니다.

Webpack 또는 Rollup 설정 파일에서 TypeScript 컴파일러(tsc)를 직접 실행하거나, ts-loader (Webpack), @rollup/plugin-typescript (Rollup) 등의 플러그인을 사용하여 TypeScript 컴파일을 처리할 수 있습니다.

3. TypeScript 모듈과 다른 기능들 (인터페이스, 제네릭)과의 조합

TypeScript의 인터페이스와 제네릭은 모듈과 함께 사용하여 더욱 강력한 타입 안정성을 제공합니다.

  • 인터페이스: 모듈 내에서 인터페이스를 정의하여 모듈 간의 계약을 명확하게 정의할 수 있습니다.
  • 제네릭: 모듈 내의 함수나 클래스에서 제네릭을 사용하여 다양한 타입에 대응하는 유연한 코드를 작성할 수 있습니다.
// data-service.ts
export interface DataService<T> {
  fetchData(): Promise<T[]>;
}

export class UserDataService implements DataService<{id: number, name: string}> {
    async fetchData(): Promise<{id: number, name: string}[]> {
        // ... fetch user data
        return [{id: 1, name: 'test'}];
    }
}

// display-data.ts
import { DataService } from './data-service';

export async function displayData<T>(service: DataService<T>) {
    const data = await service.fetchData();
    console.log(data);
}

// main.ts
import { displayData } from './display-data';
import { UserDataService } from './data-service';

const userService = new UserDataService();
displayData(userService);