본문 바로가기
4주차

타입스크립트 제네릭(Generic), 제대로 이해하기

by 김채으니 2025. 5. 9.

안녕하세요, 웹 OB 김채은입니다 -!

 

타입스크립트를 사용해 개발을 하다보면, 어느 순간 제네릭(Generic)이라는 문법을 접하게 되는데요..! 처음에는 any와 비슷해 보일 수 있지만, 실제로는 타입 추론과 코드 재사용성 면에서 훨씬 강력합니다. 이 글에서는 책 "우아한 타입스크립트 with 리액트"의 내용을 바탕으로 제네릭의 개념, 사용 방법, 그리고 주의할 점을 정리해보려 합니다.


📌  제네릭이란?

제네릭(Generic)은 C, Java 같은 정적 타입 언어에서 다양한 타입에 대해 동일한 코드를 재사용할 수 있도록 고안된 문법입니다. 타입스크립트도 정적 타입 언어이기 때문에 제네릭을 지원합니다.

 

예를 들어 다음과 같이 타입을 변수처럼 취급할 수 있어요:

function exampleFunc<T>(arg: T): T[] {
  return new Array(3).fill(arg);
}

exampleFunc("hello"); // T는 string으로 추론됨

 

여기서 T는 타입 변수로, 사용할 때 외부에서 지정해주는 타입입니다. T 외에도 E, K, V 등 한 글자 약어가 자주 쓰입니다.


🆚  any와의 차이

any는 타입 체크를 하지 않고 모든 타입을 허용하지만, 제네릭은 특정 타입으로 한정하면서도 다양한 타입을 다룰 수 있게 해줍니다.

type ExampleArrayType<T> = T[];
const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"];

// any를 사용할 경우
const array2: any[] = [
  "치킨",
  { id: 0, name: "치킨", price: 20000, quantity: 1 },
  99,
  true,
];

 

any는 타입 정보를 잃어버리지만, 제네릭은 배열 요소들이 동일한 타입임을 보장합니다.


✅  타입 추론 및 기본값

제네릭 함수 호출 시 타입을 명시하지 않아도, 타입스크립트가 인자를 통해 추론합니다.

function exampleFunc<T>(arg: T): T[] {
  return new Array(3).fill(arg);
}

exampleFunc("hello"); // string으로 추론

 

필요할 경우 기본값도 지정할 수 있어요:

interface SubmitEvent<T = HTMLElement> extends SyntheticEvent<T> {
  submitter: T;
}

📛 제약 조건 (extends)

모든 타입이 특정 속성을 가진 것은 아니기 때문에 제약을 걸어줄 수 있습니다:

function exampleFunc2<T extends { length: number }>(arg: T): number {
  return arg.length;
}

 

이렇게 하면 length 속성을 가진 타입만 사용할 수 있게 됩니다.


🧨 TSX 환경에서의 제네릭

파일 확장자가 .tsx일 때는 화살표 함수에서 제네릭을 사용할 경우 JSX의 태그 문법과 혼동되어 오류가 날 수 있는데요:

// 오류 발생
const arrowExampleFunc = <T>(arg: T): T[] => {
  return new Array(3).fill(arg);
};

// 오류 회피 (extends 사용)
const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
  return new Array(3).fill(arg);
};

 

보통 이럴 땐 function 키워드를 사용한 선언형 함수로 대체하는 것이 좋습니다.


📦 실전 예시: API 응답 타입

export interface MobileApiResponse<Data> {
  data: Data;
  statusCode: string;
  statusMessage?: string;
}

export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
  return request({ method: "GET", url: "/price" });
};

export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
  return request({ method: "GET", url: "/order" });
};

 

이처럼 다양한 API 응답에서 반복되는 타입을 제네릭으로 추상화하면, 코드의 재사용성과 가독성이 대폭 향상됩니다 ! !


🧱 클래스에서의 제네릭

class LocalDB<T> {
  async put(table: string, row: T): Promise<T> {
    return new Promise<T>((resolve, reject) => { /* 저장 로직 */ });
  }

  async get(table: string, key: any): Promise<T> {
    return new Promise<T>((resolve, reject) => { /* 조회 로직 */ });
  }

  async getTable(table: string): Promise<T[]> {
    return new Promise<T[]>((resolve, reject) => { /* 테이블 전체 조회 */ });
  }
}

 


❌ 제네릭 오남용 예시

다만 불필요한 제네릭 사용은 코드 가독성을 해칩니다:

type GType<T> = T;
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";

interface Order {
  getRequirement(): GType<RequirementType>; // ❌ 의미 없는 추상화
}

// 아래가 훨씬 낫다
interface Order {
  getRequirement(): RequirementType;
}

🧠 마무리

제네릭은 타입스크립트에서 타입 안정성 코드 재사용성을 동시에 잡을 수 있는 강력한 기능입니다. 특히 API 응답 처리, 유틸 함수, DB 핸들러 등에서 큰 효과를 발휘합니다. 하지만 무분별한 제네릭 사용은 오히려 혼란을 유발할 수 있으므로, 필요한 곳에 명확하게 적용하는 것이 중요할 것 같습니다ㅎ.,ㅎ