제네릭 클래스와 인터페이스
제네릭 클래스와 인터페이스는 타입스크립트에서 재사용 가능하고 타입 안전한 코드를 작성하는 데 핵심적인 역할을 합니다.
이들은 다양한 타입에 대해 동작하는 컴포넌트를 만들 수 있게 해줍니다.
제네릭 클래스의 기본 개념과 문법
제네릭 클래스는 하나 이상의 타입 매개변수를 가진 클래스입니다.
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
- 단순성 유지 : 필요한 경우에만 제네릭을 사용하고, 가능한 한 간단하게 유지하세요.
- 명확한 명명 : 타입 매개변수에 의미 있는 이름을 사용하세요 (예 : T 대신 TElement).
- 제약 조건 활용 : 필요한 경우 extends를 사용하여 타입 매개변수에 제약 조건을 설정하세요.
- 기본 타입 제공 : 적절한 경우 타입 매개변수에 기본값을 제공하여 사용성을 높이세요.
- 인터페이스 선호 : 가능한 경우 제네릭 클래스보다 제네릭 인터페이스를 사용하여 유연성을 높이세요.
- 타입 추론 활용 : 명시적 타입 인수를 과도하게 사용하지 말고, 타입스크립트의 추론 기능을 활용하세요.
- 테스트 작성 : 제네릭 컴포넌트에 대한 철저한 테스트를 작성하여 다양한 타입에서의 동작을 확인하세요.
- 문서화 : 복잡한 제네릭 타입이나 클래스는 주석을 통해 사용 방법과 의도를 명확히 설명하세요.
- 과도한 중첩 피하기 : 너무 많은 중첩된 제네릭 타입은 가독성을 해칠 수 있으므로 주의하세요.
- 실제 사용 사례 기반 : 실제 문제 해결을 위해 제네릭을 사용하고, 불필요한 추상화는 피하세요.
제네릭 클래스와 인터페이스는 타입스크립트에서 재사용 가능하고 타입 안전한 코드를 작성하는 데 필수적인 도구입니다.
이들을 효과적으로 활용하면 더 유연하고 강력한 API를 설계할 수 있습니다.
그러나 과도하게 복잡한 제네릭 사용은 코드의 가독성을 해치므로, 항상 명확성과 단순성을 염두에 두고 설계해야 합니다.