icon안동민 개발노트

제네릭 클래스와 인터페이스


 제네릭 클래스와 인터페이스는 타입스크립트에서 재사용 가능하고 타입 안전한 코드를 작성하는 데 핵심적인 역할을 합니다.

 이들은 다양한 타입에 대해 동작하는 컴포넌트를 만들 수 있게 해줍니다.

제네릭 클래스의 기본 개념과 문법

 제네릭 클래스는 하나 이상의 타입 매개변수를 가진 클래스입니다.

class Box<T> {
    private content: T;
 
    constructor(value: T) {
        this.content = value;
    }
 
    getValue(): T {
        return this.content;
    }
}
 
const numberBox = new Box<number>(42);
const stringBox = new Box<string>("Hello");
 
console.log(numberBox.getValue()); // 42
console.log(stringBox.getValue()); // "Hello"

 이 접근 방식은 코드 재사용성을 높이고, 타입 안정성을 보장합니다.

제네릭 인터페이스 정의와 구현

 제네릭 인터페이스는 다양한 타입에 대해 동일한 구조를 정의할 수 있게 해줍니다.

interface Pair<T, U> {
    first: T;
    second: U;
}
 
class OrderedPair<T, U> implements Pair<T, U> {
    constructor(public first: T, public second: U) {}
}
 
const pair1 = new OrderedPair<number, string>(1, "one");
const pair2: Pair<boolean, Date> = { first: true, second: new Date() };

Static 멤버와 인스턴스 멤버

 제네릭 클래스에서 static 멤버는 클래스의 타입 매개변수를 사용할 수 없습니다.

class GenericNumber<T> {
    static defaultValue: T; // 오류: static 멤버는 클래스의 타입 매개변수를 사용할 수 없음
    
    constructor(public value: T) {}
 
    static createInstance<U>(value: U): GenericNumber<U> {
        return new GenericNumber<U>(value);
    }
}
 
const num = GenericNumber.createInstance(42);

제네릭 클래스 상속

 제네릭 클래스를 상속할 때는 타입 매개변수를 적절히 처리해야 합니다.

class Animal<T> {
    constructor(public name: T) {}
}
 
class Dog<T> extends Animal<T> {
    bark() {
        console.log("Woof!");
    }
}
 
const dog = new Dog<string>("Buddy");

제네릭 메서드와 제네릭 클래스 조합

 제네릭 클래스 내에서 제네릭 메서드를 정의할 수 있습니다.

class DataContainer<T> {
    private data: T[];
 
    constructor() {
        this.data = [];
    }
 
    addItem(item: T) {
        this.data.push(item);
    }
 
    getItems(): T[] {
        return this.data;
    }
 
    getItemsOfType<U extends T>(type: new () => U): U[] {
        return this.data.filter(item => item instanceof type) as U[];
    }
}
 
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
const container = new DataContainer<Animal>();
container.addItem(new Dog());
container.addItem(new Cat());
 
const dogs = container.getItemsOfType(Dog);

타입 매개변수의 기본값

 타입 매개변수에 기본값을 설정하여 옵셔널 제네릭을 구현할 수 있습니다.

class DefaultMap<K, V = string> {
    private map: Map<K, V>;
 
    constructor() {
        this.map = new Map<K, V>();
    }
 
    set(key: K, value: V): void {
        this.map.set(key, value);
    }
 
    get(key: K): V | undefined {
        return this.map.get(key);
    }
}
 
const numberStringMap = new DefaultMap<number>();
const numberBooleanMap = new DefaultMap<number, boolean>();

디자인 패턴 구현

 제네릭 클래스와 인터페이스를 활용한 Repository 패턴 예시

interface Entity {
    id: number;
}
 
interface Repository<T extends Entity> {
    findById(id: number): T | undefined;
    save(item: T): void;
    delete(id: number): void;
}
 
class InMemoryRepository<T extends Entity> implements Repository<T> {
    private items: T[] = [];
 
    findById(id: number): T | undefined {
        return this.items.find(item => item.id === id);
    }
 
    save(item: T): void {
        const index = this.items.findIndex(i => i.id === item.id);
        if (index !== -1) {
            this.items[index] = item;
        } else {
            this.items.push(item);
        }
    }
 
    delete(id: number): void {
        this.items = this.items.filter(item => item.id !== id);
    }
}
 
interface User extends Entity {
    name: string;
}
 
const userRepository = new InMemoryRepository<User>();

타입 추론 메커니즘

 타입스크립트는 제네릭 클래스와 인터페이스 사용 시 타입을 추론할 수 있습니다.

function createPair<T, U>(first: T, second: U): Pair<T, U> {
    return { first, second };
}
 
const pair = createPair("hello", 42); // Pair<string, number> 타입으로 추론됨

성능 고려사항

 제네릭은 컴파일 시간에 처리되므로 런타임 성능에 직접적인 영향을 주지 않습니다.

 그러나 과도하게 복잡한 제네릭 타입은 컴파일 시간을 증가시킬 수 있습니다.

설계 원칙과 Best Practices

  1. 단순성 유지 : 필요한 경우에만 제네릭을 사용하고, 가능한 한 간단하게 유지하세요.
  2. 명확한 명명 : 타입 매개변수에 의미 있는 이름을 사용하세요 (예 : T 대신 TElement).
  3. 제약 조건 활용 : 필요한 경우 extends를 사용하여 타입 매개변수에 제약 조건을 설정하세요.
  4. 기본 타입 제공 : 적절한 경우 타입 매개변수에 기본값을 제공하여 사용성을 높이세요.
  5. 인터페이스 선호 : 가능한 경우 제네릭 클래스보다 제네릭 인터페이스를 사용하여 유연성을 높이세요.
  6. 타입 추론 활용 : 명시적 타입 인수를 과도하게 사용하지 말고, 타입스크립트의 추론 기능을 활용하세요.
  7. 테스트 작성 : 제네릭 컴포넌트에 대한 철저한 테스트를 작성하여 다양한 타입에서의 동작을 확인하세요.
  8. 문서화 : 복잡한 제네릭 타입이나 클래스는 주석을 통해 사용 방법과 의도를 명확히 설명하세요.
  9. 과도한 중첩 피하기 : 너무 많은 중첩된 제네릭 타입은 가독성을 해칠 수 있으므로 주의하세요.
  10. 실제 사용 사례 기반 : 실제 문제 해결을 위해 제네릭을 사용하고, 불필요한 추상화는 피하세요.

 제네릭 클래스와 인터페이스는 타입스크립트에서 재사용 가능하고 타입 안전한 코드를 작성하는 데 필수적인 도구입니다.

 이들을 효과적으로 활용하면 더 유연하고 강력한 API를 설계할 수 있습니다.

 그러나 과도하게 복잡한 제네릭 사용은 코드의 가독성을 해치므로, 항상 명확성과 단순성을 염두에 두고 설계해야 합니다.