[TYPESCRIPT] 제네릭 함수와 클래스 활용하기 - 재사용성과 타입 안정성을 모두 잡는 방법

제네릭(Generic)은 TypeScript에서 가장 강력한 문법 중 하나입니다. 특히 “함수”와 “클래스”에서 제네릭을 제대로 활용하면, 데이터 타입에 의존하지 않는 재사용 가능한 로직을 만들 수 있습니다. 

 

제네릭 함수 — 타입을 입력받는 함수

제네릭 함수는 “입력값의 타입과 반환값의 타입이 연결된 함수”를 만들 때 사용합니다. 즉, 타입 정보가 흐르는 함수라고 할 수 있습니다.

function identity<T>(value: T): T {
  return value;
}

identity(10);       // T = number
identity("hello");  // T = string

이 함수는 어떠한 타입이 와도 동일한 타입을 유지한 채 반환합니다. 중복 선언 없이 모든 타입을 처리할 수 있는 것이 강점입니다.

 

제네릭 함수 활용 예제

1. 배열에서 첫 번째 요소 반환하기

function first<T>(arr: T[]): T {
  return arr[0];
}

first([1, 2, 3]);       // number
first(["a", "b"]);      // string

입력된 배열 타입에 따라 반환 타입도 자동으로 결정됩니다.

2. 2가지 타입을 동시에 받는 함수

function mapValue<T, U>(value: T, fn: (x: T) => U): U {
  return fn(value);
}

mapValue(5, x => x.toString());   // string

두 개 이상의 타입을 처리해야 하는 상황에서 매우 유용합니다.

 

제네릭 제약(extends) 적용하기

제네릭을 아무 타입이나 허용하면 위험할 때가 있습니다. 이럴 때 제약 조건을 걸어 제한할 수 있습니다.

function getLength<T extends { length: number }>(value: T) {
  return value.length;
}

getLength("Hello");  // OK
getLength([1,2,3]);  // OK
getLength(10);       // 오류

실제로 length 속성이 없는 타입을 걸러낼 수 있기 때문에 런타임 오류 예방에 큰 효과가 있습니다.

 

제네릭 클래스 — 타입을 파라미터로 받는 클래스

클래스에 제네릭을 사용하면 다양한 타입을 저장하거나 처리하는 자료구조, 서비스 클래스를 손쉽게 구현할 수 있습니다.

class Box<T> {
  constructor(private value: T) {}

  getValue(): T {
    return this.value;
  }
}

const numBox = new Box(123);     // T = number
const strBox = new Box("hello"); // T = string

값을 어떤 타입으로 저장하든, 클래스는 타입을 기억하고 유지합니다.

 

제네릭 클래스 활용 예제

1. Repository 패턴 구현

API 또는 DB 데이터를 관리하는 Repository를 제네릭으로 설계하면 모든 도메인에 재사용할 수 있습니다.

class Repository<T> {
  private items: T[] = [];

  add(item: T) {
    this.items.push(item);
  }

  findAll(): T[] {
    return this.items;
  }
}

type User = { id: number; name: string };
const userRepo = new Repository<User>();

userRepo.add({ id: 1, name: "Alice" });
userRepo.findAll(); // User[]

이 방식은 Repository, Service, Cache 클래스 등 실무에서 매우 많이 쓰입니다.

2. Queue, Stack 같은 자료구조 만들기

class Queue<T> {
  private data: T[] = [];

  enqueue(item: T) {
    this.data.push(item);
  }

  dequeue(): T | undefined {
    return this.data.shift();
  }
}

const q = new Queue<number>();
q.enqueue(1);
q.enqueue(2);

자료구조를 타입 안정성 있게 구성할 수 있습니다.

 

제네릭 + keyof 조합

제네릭과 keyof를 결합하면 “객체의 키를 안전하게 처리하는 함수”를 만들 수 있습니다.

function getValue<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const person = { name: "Bob", age: 30 };
getValue(person, "name");  // OK
getValue(person, "email"); // 오류

이 패턴은 폼 검증, API 데이터 파싱, Config 관리 등 실무에서 매우 중요합니다.

 

제네릭을 활용한 API 응답 타입 최적화

API 구조가 비슷할 때 제네릭은 중복을 극적으로 줄여줍니다.

type ApiResponse<T> = {
  success: boolean;
  data: T;
};

const userRes: ApiResponse<{ id: number }> = {
  success: true,
  data: { id: 1 }
};

API 응답 타입을 매번 새로 만들지 않아도 되므로 유지보수성이 크게 올라갑니다.

 

제네릭 활용 시 주의해야 할 점

  • 기능이 필요 없으면 제네릭을 사용하지 말 것 (과한 제네릭 남용 금지)
  • 제약 조건을 적절히 걸어 타입 안전성 유지
  • 타입 단언(as) 남발 대신 제네릭 타입 흐름 설계
  • any를 넣으면 제네릭 의미가 사라짐

제네릭은 강력하지만 목적에 맞게 설계하지 않으면 오히려 복잡도만 증가할 수 있습니다.


 

제네릭 함수와 클래스는 타입 안정성, 재사용성, 도메인 확장을 모두 만족시키는 기능입니다. 특히 대규모 프로젝트에서 중복된 함수나 서비스를 만들지 않도록 도와주며, 타입 흐름을 일관되게 유지할 수 있도록 돕습니다.

  • 제네릭 함수 → 타입 흐름을 보존하는 유연한 함수
  • 제네릭 클래스 → 자료구조·서비스 클래스에 최적
  • extends 제약 → 안전한 타입 제한
  • keyof + 제네릭 → 안전한 객체 접근 패턴