제네릭의 개념과 필요성
제네릭(Generics)는 TypeScript에서 코드 재사용성을 극대화하고 타입 안정성을 보장하는 데 매우 유용한 도구입니다. 제네릭을 사용하면 함수, 클래스, 인터페이스를 다양한 타입으로 처리할 수 있습니다.
필요성
일반적으로 함수나 클래스에서 특정 타입을 다루게 되면, 그 타입이 고정적이어서 재사용성이 떨어집니다. 제네릭은 이러한 문제를 해결하고 다양한 타입을 지원할 수 있도록 설계되었습니다.
예를 들어, 배열의 첫 번째 요소를 반환하는 함수를 생각해 봅시다.
// 문자열 배열에 대해 동작하는 함수
function getFirstElementString(arr: string[]): string {
return arr[0];
}
// 숫자 배열에 대해 동작하는 함수
function getFirstElementNumber(arr: number[]): number {
return arr[0];
}
위의 코드는 문자열과 숫자 배열 각각에 대해 별도의 함수를 작성해야 하므로 비효율적입니다. 제네릭을 사용하면 이러한 중복 코드를 줄일 수 있습니다.
제네릭 함수와 클래스 정의
제네릭 함수
제네릭 함수는 타입을 함수 호출 시점에 정의할 수 있습니다. 타입 매개변수는 대개 T로 표기되며, 이는 관습적인 이름일 뿐 다른 이름을 사용해도 무방합니다.
예제: 배열의 첫 번째 요소 반환
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
const stringArray = ['apple', 'banana', 'cherry'];
const numberArray = [1, 2, 3, 4];
const firstString = getFirstElement(stringArray); // 'apple'
const firstNumber = getFirstElement(numberArray); // 1
이 함수는 배열의 타입에 관계없이 재사용할 수 있으며, 타입 추론 덕분에 호출 시 타입을 명시하지 않아도 됩니다.
제네릭 클래스
제네릭은 클래스에서도 사용 가능합니다. 예를 들어, 데이터를 저장하고 관리하는 상자를 만드는 클래스를 생각해봅시다.
예제: 제네릭 클래스를 활용한 데이터 저장소
class DataStorage<T> {
private items: T[] = [];
addItem(item: T): void {
this.items.push(item);
}
removeItem(item: T): void {
this.items = this.items.filter(storedItem => storedItem !== item);
}
getItems(): T[] {
return this.items;
}
}
const stringStorage = new DataStorage<string>();
stringStorage.addItem('Book');
stringStorage.addItem('Pen');
stringStorage.removeItem('Pen');
console.log(stringStorage.getItems()); // ['Book']
const numberStorage = new DataStorage<number>();
numberStorage.addItem(42);
numberStorage.addItem(7);
numberStorage.removeItem(42);
console.log(numberStorage.getItems()); // [7]
제네릭 제약조건(Constraints) 설정
제네릭은 기본적으로 어떤 타입이든 사용할 수 있지만, 때로는 특정 타입이나 타입의 특징을 강제해야 할 필요가 있습니다. 이때 extends 키워드를 사용하여 제약조건을 설정할 수 있습니다.
예제: 객체의 특정 속성에 접근
다음은 객체 배열에서 특정 키의 값을 추출하는 함수입니다.
interface Person {
name: string;
age: number;
}
function extractProperty<T extends object, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const people: Person[] = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
];
const names = extractProperty(people, 'name'); // ['Alice', 'Bob']
const ages = extractProperty(people, 'age'); // [30, 25]
설명
T extends object:T는 객체 타입이어야 합니다.K extends keyof T:K는T의 키 중 하나여야 합니다.
제약조건을 사용한 정렬 함수
객체 배열을 특정 키를 기준으로 정렬하는 예제입니다.
function sortBy<T extends object, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => {
if (a[key] < b[key]) return -1;
if (a[key] > b[key]) return 1;
return 0;
});
}
const sortedPeople = sortBy(people, 'age');
console.log(sortedPeople);
// [{ name: 'Bob', age: 25 }, { name: 'Alice', age: 30 }]
기본 타입 설정
제네릭 타입 매개변수에 기본 타입을 설정할 수도 있습니다.
예제: 기본 타입이 string인 함수
function logItem<T = string>(item: T): void {
console.log(item);
}
logItem('Hello'); // 'Hello'
logItem(123); // 123
결론
제네릭(Generics)은 타입 안정성과 코드 재사용성을 크게 향상시킵니다. 제네릭 함수와 클래스를 활용하면 타입에 독립적인 코드를 작성할 수 있으며, 제약조건을 통해 필요한 범위 내에서 유연성을 유지할 수 있습니다. TypeScript의 제네릭은 코드를 보다 안전하고 효율적으로 작성할 수 있는 강력한 도구입니다.

'Language > TypeScript' 카테고리의 다른 글
| TypeScript 타입 가드(Type Guards) (0) | 2023.10.17 |
|---|---|
| Typescript 유니언(Union), 교차(Intersection) 타입 (0) | 2023.10.16 |
| TypeScript interface와 type aliasing (0) | 2023.10.16 |
| TypeScript 함수 타입 정의 (0) | 2023.10.16 |
| TypeScript 객체(Object)와 배열(Array)에서의 타입 (0) | 2023.10.16 |