안녕하세요, TypeScript의 제네릭(Generics)에 대해 깊이 알아보는 시간을 가져보겠습니다. 제네릭은 TypeScript의 강력한 기능 중 하나로, 다양한 타입에 대해 재사용 가능한 컴포넌트나 함수를 작성할 수 있게 해줍니다. 이 글에서는 제네릭의 기본 개념부터 실무에서 자주 사용되는 패턴까지 다뤄보겠습니다.
제네릭(Generics)은 코드의 재사용성을 높이기 위해 만들어진 TypeScript의 문법입니다. 제네릭을 사용하면 특정 타입에 의존하지 않고 다양한 타입에서 동작할 수 있는 함수, 클래스, 인터페이스를 정의할 수 있습니다. 이는 데이터 타입에 대한 강력한 타입 안전성을 제공하면서도 유연성을 유지할 수 있도록 도와줍니다.
제네릭은 보통 <T>
라는 형태로 사용됩니다. 여기서 T
는 제네릭 타입을 나타내며, 이름은 임의로 지정할 수 있습니다. T
외에도 K
, V
등 다양한 이름을 사용할 수 있습니다.
function identity<T>(value: T): T {
return value;
}
const stringIdentity = identity<string>('Hello'); // 'Hello'
const numberIdentity = identity<number>(42); // 42
위 코드에서 identity
함수는 제네릭 타입 T
를 사용하여 입력값과 같은 타입의 값을 반환합니다. 이 함수는 호출될 때 전달되는 타입에 따라 T
의 타입이 결정됩니다.
제네릭 함수는 입력값과 출력값의 타입을 호출 시점에 지정할 수 있습니다. 이를 통해 코드의 재사용성과 타입 안정성을 모두 만족시킬 수 있습니다.
function reverseArray<T>(items: T[]): T[] {
return items.reverse();
}
const reversedNumbers = reverseArray<number>([1, 2, 3, 4]); // [4, 3, 2, 1]
const reversedStrings = reverseArray<string>(['a', 'b', 'c']); // ['c', 'b', 'a']
위 함수는 배열을 뒤집는 기능을 제공합니다. 입력된 배열의 타입이 number
이든 string
이든 상관없이, 타입 안전성을 유지하면서 동작합니다.
제네릭은 인터페이스에서도 사용될 수 있습니다. 이를 통해 특정 데이터 타입에 종속되지 않는 구조를 정의할 수 있습니다.
interface KeyValuePair<K, V> {
key: K;
value: V;
}
const pair: KeyValuePair<string, number> = {
key: 'age',
value: 30
};
위 예제에서 KeyValuePair
인터페이스는 두 개의 제네릭 타입 K
와 V
를 사용합니다. 이를 통해 key
와 value
의 타입을 자유롭게 지정할 수 있습니다.
클래스에서도 제네릭을 사용할 수 있습니다. 제네릭 클래스를 사용하면 다양한 데이터 타입을 다룰 수 있는 유연한 객체를 생성할 수 있습니다.
class DataManager<T> {
private items: T[] = [];
addItem(item: T): void {
this.items.push(item);
}
removeItem(index: number): void {
this.items.splice(index, 1);
}
getItems(): T[] {
return this.items;
}
}
const stringManager = new DataManager<string>();
stringManager.addItem('Hello');
stringManager.addItem('World');
console.log(stringManager.getItems()); // ['Hello', 'World']
const numberManager = new DataManager<number>();
numberManager.addItem(1);
numberManager.addItem(2);
console.log(numberManager.getItems()); // [1, 2]
위 클래스는 데이터를 관리하는 기능을 제공합니다. 데이터 타입은 제네릭 타입 T
로 정의되어 있어, 문자열, 숫자 등 다양한 타입의 데이터를 처리할 수 있습니다.
제네릭은 때로 특정 타입만 허용하도록 제약 조건을 걸 필요가 있습니다. TypeScript에서는 extends
키워드를 사용하여 제약 조건을 지정할 수 있습니다.
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(item: T): void {
console.log(item.length);
}
logLength({ length: 10, name: 'Test' }); // 10
// logLength(42); // 오류: 'number' 타입에는 'length' 속성이 없습니다.
위 예제에서 logLength
함수는 입력값이 Lengthwise
인터페이스를 구현하는 타입만 받을 수 있도록 제약 조건을 설정하였습니다. 이를 통해 불필요한 타입 오류를 방지할 수 있습니다.
TypeScript는 제네릭을 기반으로 한 여러 유틸리티 타입을 제공합니다. 대표적인 예로 Partial
, Readonly
, Record
등이 있습니다.
Partial<T>
는 특정 타입의 모든 속성을 선택적으로 만들어줍니다.
interface User {
id: number;
name: string;
age: number;
}
const partialUser: Partial<User> = {
name: 'John'
};
Readonly<T>
는 특정 타입의 모든 속성을 읽기 전용으로 만듭니다.
const readonlyUser: Readonly<User> = {
id: 1,
name: 'John',
age: 30
};
// readonlyUser.age = 31; // 오류: 읽기 전용 속성은 수정할 수 없습니다.
Record<K, T>
는 특정 키 타입 K
와 값 타입 T
를 가지는 객체를 생성합니다.
const userRoles: Record<string, string> = {
admin: 'John',
user: 'Jane'
};
제네릭은 TypeScript의 핵심 기능으로, 다양한 타입에 대해 안전하고 유연한 코드를 작성할 수 있게 해줍니다.
TypeScript의 제네릭을 적극적으로 활용하여 코드의 재사용성과 안정성을 높여보세요! 감사합니다.