TypeScript의 제네릭(Generics)

원도훈·2024년 12월 8일
1

안녕하세요, 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 인터페이스는 두 개의 제네릭 타입 KV를 사용합니다. 이를 통해 keyvalue의 타입을 자유롭게 지정할 수 있습니다.


제네릭 클래스

클래스에서도 제네릭을 사용할 수 있습니다. 제네릭 클래스를 사용하면 다양한 데이터 타입을 다룰 수 있는 유연한 객체를 생성할 수 있습니다.

예제

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로 정의되어 있어, 문자열, 숫자 등 다양한 타입의 데이터를 처리할 수 있습니다.


제네릭 제약 조건 (Constraints)

제네릭은 때로 특정 타입만 허용하도록 제약 조건을 걸 필요가 있습니다. 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

Partial<T>는 특정 타입의 모든 속성을 선택적으로 만들어줍니다.

interface User {
  id: number;
  name: string;
  age: number;
}

const partialUser: Partial<User> = {
  name: 'John'
};

Readonly

Readonly<T>는 특정 타입의 모든 속성을 읽기 전용으로 만듭니다.

const readonlyUser: Readonly<User> = {
  id: 1,
  name: 'John',
  age: 30
};

// readonlyUser.age = 31; // 오류: 읽기 전용 속성은 수정할 수 없습니다.

Record

Record<K, T>는 특정 키 타입 K와 값 타입 T를 가지는 객체를 생성합니다.

const userRoles: Record<string, string> = {
  admin: 'John',
  user: 'Jane'
};

결론

제네릭은 TypeScript의 핵심 기능으로, 다양한 타입에 대해 안전하고 유연한 코드를 작성할 수 있게 해줍니다.

  • 제네릭 함수는 다양한 데이터 타입을 처리하는 데 유용합니다.
  • 제네릭 클래스인터페이스는 객체 지향적인 설계에서 강력한 도구가 됩니다.
  • 제네릭 제약 조건을 통해 타입 안정성을 더욱 강화할 수 있습니다.
  • 유틸리티 타입은 제네릭의 활용도를 극대화합니다.

TypeScript의 제네릭을 적극적으로 활용하여 코드의 재사용성과 안정성을 높여보세요! 감사합니다.


참고 자료

profile
개발

0개의 댓글