일상 코드에 필요한 TypeScript 제네릭: 실용 가이드

Frontend2026-10-04TryQuickToolBox

여러분은 모든 타입에서 작동하는 함수를 작성하고 있지만, TypeScript는 계속 any에 대해 불평합니다. 아니면 서로 다른 타입을 처리하기 위해 같은 함수를 세 번 복사했을 수도 있습니다. 제네릭이 이 문제를 해결합니다. 제네릭을 사용하면 타입 안전성을 유지하면서 여러 타입에서 작동하는 코드를 작성할 수 있습니다. 하지만 많은 개발자가 제네릭을 피하거나 과도하게 사용합니다. 이 가이드는 골치 아픈 일 없이 일상 코드에서 제네릭을 사용하는 방법을 보여줍니다.

제네릭이란 무엇이며 왜 신경 써야 할까요?

제네릭은 타입을 매개변수화하는 방법입니다. string이나 number와 같은 타입을 하드코딩하는 대신, 타입 변수(종종 T)를 사용하여 함수나 클래스가 사용될 때 구체적인 타입으로 대체됩니다.

왜 중요할까요? 재사용 가능한 코드를 타입 안전하게 유지할 수 있기 때문입니다. 제네릭이 없다면 코드를 중복하거나 any를 사용해야 하는데, 이는 TypeScript의 목적을 무색하게 만듭니다.

제네릭 함수: 기본 구성 요소

간단한 예제부터 시작해 보겠습니다. 배열의 첫 번째 요소를 반환하는 함수를 원한다고 가정해 봅시다.

function firstElement(arr: any[]): any {
  return arr[0];
}

이것은 작동하지만 타입 정보를 잃게 됩니다. 문자열 배열을 전달하면 반환 타입은 string이 아니라 any입니다. 이제 제네릭을 사용해 보겠습니다:

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

이제 TypeScript는 string[]을 전달하면 string | undefined를 반환한다는 것을 알 수 있습니다. 타입이 유지됩니다.

여러 타입 매개변수를 사용할 수도 있습니다:

function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

제네릭 인터페이스와 타입 별칭

제네릭은 함수에만 국한되지 않습니다. 유연한 데이터 구조를 설명하기 위해 제네릭 인터페이스와 타입 별칭을 만들 수 있습니다.

interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

이것은 API 응답에 특히 유용합니다. 모든 엔드포인트에 대해 새 인터페이스를 정의하는 대신, 제네릭 ApiResponse<T>를 정의할 수 있습니다:

interface ApiResponse<T> {
  data: T;
  status: number;
}

type User = { id: number; name: string };

const response: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
};

제네릭 제약: 규칙 추가하기

때로는 제네릭과 함께 사용할 수 있는 타입을 제한하고 싶을 수 있습니다. 여기서 제약이 필요합니다. extends 키워드를 사용하여 타입이 특정 속성을 가져야 함을 요구하세요.

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

getLength("hello"); // OK
getLength([1, 2, 3]); // OK
getLength(42); // Error: number doesn't have length

제약은 제네릭을 더 예측 가능하게 만들고 런타임 오류를 방지합니다.

제네릭 클래스

클래스도 제네릭이 될 수 있습니다. 이는 스택, 큐 또는 리포지토리와 같은 데이터 구조에 유용합니다.

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

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

  pop(): T | undefined {
    return this.items.pop();
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const top = numberStack.pop(); // number | undefined

제네릭을 사용해야 할 때 (그리고 사용하지 말아야 할 때)

제네릭은 강력하지만 과도하게 사용하면 코드를 읽기 어렵게 만들 수 있습니다. 다음은 빠른 가이드입니다:

제네릭을 사용해야 할 때 제네릭을 피해야 할 때
타입 관계를 유지해야 할 때 단순한 유니온 타입이나 any로 충분할 때
재사용 가능한 유틸리티를 작성할 때 타입이 항상 동일할 때
코드 중복을 피하고 싶을 때 코드를 이해하기 어렵게 만들 때

좋은 경험 법칙: 여러 타입에 대해 동일한 로직을 작성하고 있다면 제네릭을 고려하세요. 세 번째 타입 매개변수를 추가하고 아무도 이해하지 못한다면 한 걸음 물러서세요.

일반적인 패턴과 모범 사례

실제 예제: 제네릭 API 클라이언트

실용적인 예제로 마무리해 보겠습니다: 데이터를 가져와 타입이 지정된 응답을 반환하는 간단한 API 클라이언트입니다.

async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json() as Promise<T>;
}

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

async function main() {
  const post = await fetchData<Post>("https://jsonplaceholder.typicode.com/posts/1");
  console.log(post.title); // string
}

이 패턴은 프론트엔드 코드에서 흔히 사용됩니다. 모든 엔드포인트에 대해 반복하지 않고도 타입 안전성을 얻을 수 있습니다.

FAQ

TypeScript에서 <T> 구문은 무엇을 의미하나요?

<T>는 타입 매개변수를 선언합니다. 함수, 인터페이스 또는 클래스가 사용될 때 지정될 타입의 자리 표시자입니다. 어떤 이름으로든 지정할 수 있지만, 단일 타입에는 T가 관례입니다.

여러 제네릭 타입 매개변수를 사용할 수 있나요?

네. 필요한 만큼 쉼표로 구분하여 정의할 수 있습니다: <T, U, V>. 코드를 읽기 쉽게 유지하려면 설명적인 이름을 사용하세요.

제네릭이 객체만 허용하도록 제한하려면 어떻게 해야 하나요?

T extends object를 사용하세요. 이렇게 하면 T가 string이나 number와 같은 원시 타입이 아닌 객체 타입임을 보장합니다. T extends { id: number }와 같이 특정 형태로 제한할 수도 있습니다.

제네릭은 유지 관리 가능한 TypeScript를 작성하는 핵심 부분입니다. JSON 데이터로 작업하고 빠르게 유효성을 검사하거나 형식을 지정하고 싶다면, JSON Formatter를 사용하여 API 응답을 예쁘게 출력하고 검사해 보세요.