TypeScript 제네릭 쉽게 이해하기: props와 API 응답 타입으로 감 잡기

2026.07.13·수정 2026.07.19·약 14분

핵심 요약

TypeScript 제네릭은 어려운 고급 문법이라기보다, 나중에 타입을 넣기 위해 비워두는 자리입니다. React props나 API 응답처럼 구조는 같고 안에 들어가는 데이터 타입만 달라질 때 제네릭을 사용하면 any보다 안전하게 타입을 재사용할 수 있습니다.

TypeScript를 배우다 보면 어느 순간 <T> 같은 문법을 만나게 됩니다. 처음 보면 함수 같기도 하고, HTML 태그 같기도 해서 갑자기 코드가 어려워 보입니다. 하지만 제네릭을 이해할 때 가장 먼저 볼 것은 문법 자체가 아니라 “왜 이런 방식이 필요한가”입니다.

실무에서 자주 만나는 문제는 생각보다 단순합니다. 여러 컴포넌트나 API 응답에서 전체 구조는 비슷한데, 안에 들어가는 데이터 타입만 달라지는 경우가 많습니다. User 목록도 있고 Product 목록도 있고 Post 목록도 있지만, 목록을 렌더링하는 방식이나 서버 응답의 바깥 구조는 거의 같습니다. 이때 매번 타입을 새로 만들면 중복이 늘어나고, any를 쓰면 타입 안정성이 사라집니다.

TypeScript 제네릭 개념을 공통 박스와 타입 슬롯으로 표현한 이미지

제네릭이 처음에 어렵게 느껴지는 이유

제네릭이 낯선 가장 큰 이유는 타입을 값처럼 다루는 느낌을 주기 때문입니다. 일반적으로 함수에는 값이 들어갑니다. 예를 들어 getUser(id)처럼 호출하면 id라는 값이 들어갑니다. 그런데 제네릭에서는 ApiResponse<User>처럼 타입이 들어갑니다. 이 차이가 처음에는 어색하게 느껴집니다.

문법보다 먼저 봐야 할 핵심 개념

제네릭을 쉽게 말하면 “타입이 들어갈 빈자리”입니다. 어떤 타입이 들어올지 지금은 정하지 않고, 사용하는 시점에 정할 수 있도록 자리를 비워두는 방식입니다. 그래서 T라는 글자 자체가 중요한 것이 아닙니다. T는 관례적으로 많이 쓰는 이름일 뿐이고, 핵심은 타입을 나중에 끼워 넣을 수 있다는 점입니다.

예를 들어 선물 상자를 생각해볼 수 있습니다. 상자의 모양은 같지만 안에 책이 들어갈 수도 있고, 컵이 들어갈 수도 있고, 옷이 들어갈 수도 있습니다. 제네릭도 비슷합니다. 바깥 구조는 유지하면서 안에 들어가는 타입만 바꿔 끼울 수 있습니다.

제네릭은 타입을 나중에 넣는 자리다

TypeScript에서 제네릭은 함수, 타입, 인터페이스, 컴포넌트가 특정 타입 하나에 고정되지 않도록 도와줍니다. 대신 사용할 때 타입을 전달해서 그 타입에 맞게 동작하도록 만듭니다.

여기서 자주 비교되는 것이 any입니다. any도 어떤 값이든 받을 수 있습니다. 하지만 any는 타입 정보를 포기하는 방식입니다. 반면 제네릭은 다양한 타입을 받을 수 있으면서도, 한 번 들어온 타입 정보를 끝까지 유지합니다.

any와 generic의 타입 정보 유지 차이를 비교한 다이어그램

any와 제네릭의 결정적인 차이

any를 쓰면 당장은 편해 보입니다. 타입 오류가 거의 나지 않기 때문입니다. 하지만 그만큼 TypeScript가 도와줄 수 있는 것도 줄어듭니다. 자동완성도 약해지고, 잘못된 속성에 접근해도 미리 잡아주지 못합니다.

function returnValue(value: any) {
  return value;
}

const result = returnValue("hello");
result.toFixed();

위 코드에서 result는 문자열인데도 toFixed()를 호출할 수 있는 것처럼 보입니다. any를 사용했기 때문에 TypeScript가 이 값이 문자열인지 숫자인지 더 이상 추적하지 못하기 때문입니다.

제네릭을 사용하면 같은 유연함을 유지하면서도 타입 정보는 잃지 않을 수 있습니다.

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

const text = returnValue("hello");
const count = returnValue(10);

여기서 text는 string으로 추론되고, count는 number로 추론됩니다. 같은 함수가 여러 타입을 받을 수 있지만, 각각의 타입 정보는 그대로 유지됩니다. 이것이 any와 제네릭의 가장 중요한 차이입니다.

가장 단순한 제네릭 예시로 감 잡기

제네릭을 처음 배울 때 자주 나오는 예시가 identity 함수입니다. 입력받은 값을 그대로 반환하는 함수입니다. 기능만 보면 특별할 것이 없지만, 제네릭의 감각을 잡기에는 좋습니다.

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

const name = identity("Kim");
const age = identity(30);
const isActive = identity(true);

이 함수에서 T는 실제 값이 아닙니다. 타입이 들어갈 자리입니다. identity("Kim")을 호출하면 T는 string이 되고, identity(30)을 호출하면 T는 number가 됩니다. 호출하는 값에 따라 타입이 자연스럽게 정해지는 것입니다.

타입 정보가 유지된다는 의미

중요한 점은 반환값의 타입도 입력값의 타입과 연결된다는 것입니다. string을 넣으면 string이 나오고, number를 넣으면 number가 나옵니다. TypeScript는 이 연결 관계를 알고 있기 때문에 이후 코드에서도 올바른 자동완성과 타입 검사를 제공할 수 있습니다.

제네릭은 단순히 여러 타입을 받기 위한 문법이 아닙니다. “들어온 타입과 나가는 타입의 관계”를 표현하는 방법입니다. 이 관점으로 보면 React props나 API 응답 타입에서도 제네릭이 왜 필요한지 자연스럽게 이어집니다.

React props에서 제네릭이 필요한 순간

React를 사용하다 보면 비슷한 목록 컴포넌트를 여러 번 만들게 됩니다. User 목록을 보여주는 컴포넌트, Product 목록을 보여주는 컴포넌트, Post 목록을 보여주는 컴포넌트가 따로 생길 수 있습니다. 그런데 자세히 보면 구조는 거의 같습니다. 배열을 받고, 각 아이템을 화면에 렌더링합니다.

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

type Product = {
  id: number;
  title: string;
  price: number;
};

User와 Product는 데이터 구조가 다릅니다. 하지만 “items 배열을 받아서 각각 렌더링한다”는 컴포넌트의 형태는 같습니다. 이럴 때 제네릭을 사용하면 같은 List 컴포넌트를 여러 데이터 타입에 맞게 재사용할 수 있습니다.

type GenericListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
};

function GenericList<T>({ items, renderItem }: GenericListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

여기서 GenericListProps<T>는 items 배열의 타입과 renderItem 함수의 item 타입을 같은 T로 묶습니다. 즉 items가 User 배열이면 renderItem의 item도 User가 됩니다. items가 Product 배열이면 renderItem의 item도 Product가 됩니다.

<GenericList
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
/>

<GenericList
  items={products}
  renderItem={(product) => <span>{product.title}</span>}
/>

이 방식의 장점은 renderItem 안에서 타입 추론이 살아난다는 점입니다. user는 User 타입으로, product는 Product 타입으로 추론됩니다. 그래서 없는 속성에 접근하면 TypeScript가 미리 알려주고, 사용할 수 있는 속성은 자동완성으로 확인할 수 있습니다.

API 응답 타입에서 제네릭 활용하기

제네릭이 특히 유용한 또 다른 상황은 API 응답 타입입니다. 서버 응답은 보통 일정한 공통 구조를 가집니다. 예를 들어 상태값, 메시지, 실제 데이터가 함께 내려오는 형태입니다.

ApiResponse 타입 안에 User Post Product 데이터가 들어가는 구조 설명 이미지
type ApiResponse<T> = {
  status: "success" | "error";
  message: string;
  data: T;
};

이 타입에서 status와 message는 공통입니다. 달라지는 것은 data입니다. 어떤 API는 User를 반환하고, 어떤 API는 Post를 반환하고, 어떤 API는 Product 배열을 반환할 수 있습니다. 이때 data 자리에 들어갈 타입만 나중에 정할 수 있도록 만든 것이 ApiResponse<T>입니다.

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

type Post = {
  id: number;
  title: string;
  content: string;
};

type UserResponse = ApiResponse<User>;
type PostResponse = ApiResponse<Post>;
type ProductListResponse = ApiResponse<Product[]>;

이렇게 작성하면 응답의 바깥 구조는 한 번만 정의하고, 실제 데이터 타입만 바꿔서 사용할 수 있습니다. 타입을 매번 새로 만들 필요가 줄어들고, 응답 구조가 바뀌었을 때도 공통 타입 하나를 중심으로 관리할 수 있습니다.

any로 처리했을 때 생기는 문제

API 응답을 any로 처리하면 처음에는 코드가 빠르게 작성됩니다. 하지만 이후에 data 안에 어떤 속성이 있는지 TypeScript가 알 수 없게 됩니다. 예를 들어 User 응답이라고 생각하고 data.name을 사용했는데 실제 타입이 다르더라도, 컴파일 단계에서 오류를 잡기 어렵습니다.

async function fetchUser(): Promise<ApiResponse<User>> {
  const response = await fetch("/api/user");
  return response.json();
}

const result = await fetchUser();
console.log(result.data.name);

이 코드에서는 result.data가 User 타입이라는 정보가 유지됩니다. 그래서 name, email 같은 속성은 자동완성되고, 존재하지 않는 속성에 접근하면 TypeScript가 오류를 알려줍니다. 제네릭을 사용하는 이유는 단순히 타입을 예쁘게 정리하기 위해서가 아니라, 실제 작업 중 실수를 줄이기 위해서입니다.

제네릭은 언제 쓰면 좋을까

제네릭을 모든 곳에 사용할 필요는 없습니다. 오히려 타입이 한 가지로 고정되어 있고 재사용할 일이 없다면 일반 타입을 쓰는 편이 더 읽기 쉽습니다. 제네릭은 반복되는 구조가 보일 때 꺼내는 도구에 가깝습니다.

  • 구조는 같고 안에 들어가는 타입만 달라질 때
  • 함수의 입력 타입과 반환 타입이 서로 연결되어야 할 때
  • React props에서 같은 컴포넌트를 여러 데이터 타입에 재사용하고 싶을 때
  • API 응답처럼 공통 포맷은 유지하고 data 타입만 바꾸고 싶을 때
  • any를 쓰지 않고 유연한 타입을 표현하고 싶을 때

반대로 타입이 하나뿐이고 앞으로도 바뀔 가능성이 없다면 제네릭을 억지로 넣지 않아도 됩니다. 제네릭은 코드를 더 복잡하게 만들기 위한 문법이 아니라, 중복과 불안정한 타입 처리를 줄이기 위한 선택지입니다.

T, K, U 같은 이름은 꼭 정해져 있을까

제네릭 예시에서는 T를 가장 많이 봅니다. T는 보통 Type의 약자로 사용됩니다. 하지만 반드시 T만 써야 하는 것은 아닙니다. 상황에 따라 Item, Data, Response처럼 더 의미 있는 이름을 사용할 수도 있습니다.

type ApiResponse<Data> = {
  status: string;
  message: string;
  data: Data;
};

입문 단계에서는 T 하나로 시작해도 충분합니다. 다만 제네릭이 여러 개 등장하거나 의미를 더 분명히 하고 싶다면 짧은 문자보다 설명적인 이름이 읽기 쉬울 때도 있습니다. 중요한 것은 이름 자체가 아니라, 그 자리가 “나중에 들어올 타입”이라는 점입니다.

정리: 제네릭은 타입을 재사용하는 방법이다

TypeScript 제네릭은 처음 보면 낯설지만, 핵심은 단순합니다. 타입을 바로 고정하지 않고 나중에 넣을 수 있도록 비워두는 것입니다. 그래서 같은 구조에 여러 타입을 적용해야 할 때 유용합니다.

any는 타입 검사를 포기하고 유연함을 얻는 방식입니다. 반면 제네릭은 유연함을 유지하면서도 타입 정보를 잃지 않습니다. 이 차이를 이해하면 왜 실무에서 제네릭을 사용하는지 훨씬 분명해집니다.

React props에서는 GenericList처럼 같은 컴포넌트를 User, Product, Post에 재사용할 수 있습니다. API 응답 타입에서는 ApiResponse<T>처럼 공통 응답 구조를 한 번만 만들고 data 타입만 바꿔 사용할 수 있습니다. 제네릭을 처음 배울 때는 어려운 타입 체계로 확장하기보다, “구조는 같고 안에 들어가는 타입만 다른가?”라는 질문부터 떠올리면 충분합니다.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기