요즘IT
위시켓
AIDP
콘텐츠프로덕트 밸리
요즘 작가들컬렉션물어봐
놀이터
콘텐츠
프로덕트 밸리
요즘 작가들
컬렉션
물어봐
놀이터
새로 나온
인기
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘ITAIDP
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
yozm_help@wishket.com
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

타입스크립트 제네릭, 실무에서 제대로 활용하기

효빈
7분
1시간 전
157
에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중

타입스크립트를 쓰다 보면 제네릭을 마주치는 일이 정말 많습니다. 라이브러리의 타입 정의를 읽을 때, API 응답 타입을 만들 때, 공통 함수를 작성할 때 등 어디에서든 <T> 같은 꺾쇠괄호가 등장하는데요, 그런데 다른 사람이 작성한 제네릭은 대충 읽을 수 있어도, 막상 직접 작성하려면 막막한 경우가 많습니다.

 

이번 글에서는 제네릭이 왜 필요한지부터 실무에서 자주 만나는 패턴까지 단계별로 살펴보겠습니다.

 

미리 요점만 콕 집어보면?

  • 타입스크립트를 쓰다 보면 제네릭을 마주치는 일이 정말 많습니다, 제네릭이 왜 필요한지부터 실무에서 자주 만나는 패턴까지 단계별로 살펴보겠습니다.
  • any를 쓰면 반환 값의 타입 정보가 사라집니다, 타입의 중복도 없애고 타입 안전성도 유지할 수 있는 방법이기 때문입니다.
  • 제네릭에 익숙해지면 타입스크립트가 기본으로 제공하는 유틸리티 타입(Pick, Omit, Partial 등)을 이해하는 것도 훨씬 수월해집니다.
 

제네릭이 필요한 이유

제네릭이 왜 필요한지 이해하려면, 제네릭 없이 코드를 작성했을 때 어떤 불편함이 생기는지 먼저 살펴보는 것이 좋습니다.

 

1) 타입을 하드코딩하면 생기는 문제

배열의 첫 번째 요소를 반환하는 간단한 함수를 만든다고 해봅시다. string 배열이라면 이렇게 작성할 수 있습니다.

 

function getFirstString(arr: string[]): string {
  return arr[0];
}

 

그런데 number 배열도 처리해야 한다면 어떻게 해야 할까요? 같은 로직의 함수를 하나 더 만들어야 합니다.

 

function getFirstNumber(arr: number[]): number {
  return arr[0];
}

 

로직은 완전히 같은데 타입만 다르다는 이유로 함수가 계속 늘어납니다. boolean 배열, 객체 배열까지 대응하려면 끝이 없어지겠죠.

 

2) any를 사용하면 되지 않을까?

<출처: 작가, ChatGPT로 생성>

 

그렇다면 매개변수 타입을 any로 바꾸면 어떨까요? 중복 문제는 해결됩니다.

 

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

const result = getFirst([1, 2, 3]);
result.toUpperCase(); // 에러가 안 남

 

any를 쓰면 타입 정보가 사라질뿐 아니라, 타입스크립트의 검사 자체가 대부분 우회됩니다. result가 number인데도 toUpperCase()를 호출해도 타입스크립트가 경고해 주지 않습니다. 결국 런타임에서야 에러가 터지게 되는데, 이렇게 되면 타입스크립트를 쓰는 의미가 퇴색되겠죠.

 

바로 이런 상황에서 필요한 것이 제네릭입니다. 타입의 중복도 없애고, 타입 안전성도 유지할 수 있는 방법이기 때문입니다.

 

 

제네릭 기본 문법

제네릭의 필요성을 확인했으니, 이제 실제 문법을 살펴보겠습니다. 함수, 인터페이스, 그리고 제약 조건까지 단계적으로 알아볼게요.

 

<출처: 작가, Claude로 생성>

 

1) 함수에서의 제네릭

제네릭 함수는 꺾쇠괄호(<T>)로 타입 매개변수를 선언합니다. T는 호출 시점에 전달되는 값에 따라 타입이 결정됩니다.

 

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

const num = getFirst([1, 2, 3]);       // T는 number로 추론
const str = getFirst(["a", "b", "c"]); // T는 string으로 추론

 

getFirst([1, 2, 3])을 호출하면 타입스크립트가 인자를 보고 T를 number로 추론합니다. 덕분에 반환값 num도 number 타입으로 정확하게 잡히죠. 아까 any를 썼을 때와 달리, num.toUpperCase()를 쓰면 컴파일 단계에서 에러가 발생합니다.

 

타입 추론이 원하는 대로 되지 않을 때는 직접 타입을 명시할 수도 있습니다.

 

const value = getFirst<string>(["a", "b", "c"]);

 

하지만 대부분 타입스크립트가 알아서 추론해 주기 때문에 특별한 이유가 없다면 명시하지 않아도 괜찮습니다.

 

2) 인터페이스와 타입에서의 제네릭

제네릭은 함수뿐 아니라 인터페이스와 타입 별칭에도 적용할 수 있습니다. 특히 구조는 같지만 내부 데이터 타입만 달라지는 경우에 유용합니다.

 

interface KeyPair<K, V> {
  key: K;
  value: V;
}

const pair1: KeyPair<string, number> = {
  key: "age",
  value: 27,
};

const pair2: KeyPair<string, boolean> = {
  key: "isActive",
  value: true,
};

 

하지만 함수와 한 가지 다른 점이 있습니다. 제네릭 함수는 호출 시 인자를 보고 타입을 추론할 수 있지만, 제네릭 인터페이스나 타입 별칭은 함수처럼 호출 인자를 통해 즉시 추론되는 경우가 적기 때문에, 사용하는 위치에 따라 타입 인수를 직접 명시하는 경우가 많습니다.

 

타입 별칭에서도 동일하게 사용할 수 있습니다.

 

type Pair<K, V> = {
  key: K;
  value: V;
};

 

3) 제네릭 제약 조건(extends)

제네릭은 기본적으로 어떤 타입이든 받을 수 있습니다. 하지만 실무에서는 아무 타입이나 받으면 안 되는 경우가 많은데요, 이때 extends 키워드로 타입의 범위를 제한할 수 있습니다. 

 

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

getLength("hello");      // ✅ string은 length 속성이 있음
getLength([1, 2, 3]);    // ✅ 배열은 length 속성이 있음
getLength({ length: 5 }); // ✅ 직접 정의한 객체도 가능
getLength(123);           // ❌ number에는 length가 없어서 에러

 

T extends { length: number }는 “T는 반드시 number 타입의 length 속성을 가져야 한다”는 제약입니다. 이 조건을 만족하지 않는 타입이 들어오면 컴파일 단계에서 에러가 발생합니다. 이처럼 제약 조건을 활용하면 제네릭의 유연함은 유지하면서도, 필요한 속성이나 메서드가 있다는 것을 보장받을 수 있습니다.

 

 

실무에서 자주 쓰는 제네릭 패턴

문법을 익혔으니, 이제 실무에서 제네릭이 어떻게 쓰이는지 살펴보겠습니다. 가장 흔하게 만나는 두 가지 패턴을 소개해 보겠습니다.

 

1) API 응답 타입 래퍼

실무에서 백엔드 API를 호출하면, 응답 구조가 보통 일정한 형태를 따릅니다. 상태 코드와 메시지는 공통이고, 실제 데이터 부분만 엔드포인트 마다 달라지는 경우가 많죠.

 

interface ApiResponse<T> {
  success: boolean;
  statusCode: number;
  data: T;
  message: string;
}

 

이렇게 data 부분만 제네릭으로 열어두면, 하나의 인터페이스로 모든 API 응답을 커버할 수 있습니다.

 

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

interface Post {
  id: number;
  title: string;
  content: string;
}

type UserResponse = ApiResponse<User>;
type PostListResponse = ApiResponse<Post[]>;

 

<출처: 작가, Claude로 생성>

 

ApiResponse<User>는 data가 User 타입이 되고, ApiResponse<Post[ ]>는 data가 Post[ ] 타입이 됩니다. 엔드포인트가 수십 개라도 API 응답 래퍼를 매번 새로 정의할 필요가 없습니다.

 

2) 공통 유틸 함수에 제네릭 적용하기

프로젝트를 진행하다 보면 배열이나 객체를 다루는 공통 유틸 함수를 만들게 됩니다. 이런 함수에 제네릭을 적용하면 반환 타입까지 정확하게 추론되는 것을 확인할 수 있습니다.

 

// 배열에서 특정 조건을 만족하는 첫 번째 요소 반환
function findFirst<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
  return arr.find(predicate);
}

const users = [
  { name: "홍길동", age: 27 },
  { name: "김철수", age: 30 },
];

const found = findFirst(users, (user

 

findFirst에 users 배열을 넘기면, 타입스크립트가 T를 { name: string; age: number }로 추론합니다. 덕분에 predicate 콜백의 user 매개변수에서도 user.age에 자동완성이 동작하고, 반환값의 타입도 정확하게 잡힙니다.

 

객체의 특정 필드를 추출하는 함수도 제네릭과 제약 조건을 조합하면 타입 안전하게 만들 수 있습니다.

 

function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
  return arr.map((item) => item[key]);
}

const names = pluck(users, "name"); // string[]
const ages = pluck(users, "age");   // number[]

 

K extends keyof T는 “K는 T의 키 중 하나여야 한다”는 제약입니다. 덕분에 존재하지 않는 키를 넘기면 컴파일 에러가 발생하고, 반환 타입도 해당 키의 값 타입에 맞게 자동으로 추론됩니다.

 

 

마치며

이번 글에서는 제네릭이 필요한 이유부터 기본 문법, 그리고 실무 패턴까지 살펴봤습니다. 처음에는 <T> 같은 꺾쇠괄호가 낯설게 느껴질 수 있지만, 결국 핵심은 하나입니다. 타입을 값처럼 매개변수로 전달해서 중복 없이 다양한 타입에 대응하는 것이죠. 실무에서 반복되는 타입 패턴이 보인다면, 그것이 바로 제네릭을 쓸 타이밍입니다. 특히 API응답 타입이나 공통 유틸 함수처럼 구조는 같고 데이터만 달라지는 상황에서 제네릭은 가장 큰 효과를 발휘합니다.

 

제네릭에 익숙해지면 타입스크립트가 기본으로 제공하는 유틸리티 타입(Pick, Omit, Partial 등)을 이해하는 것도 훨씬 수월해집니다. 이 유틸리티 타입들도 결국 제네릭으로 만들어진 것이기 때문입니다.


<참고>

  • 한 입 크기로 잘라먹는 타입스크립트
  • 타입스크립트 Generic 타입 정복하기

 

©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

) =>
user.age >
25
);
// found의 타입: { name: string; age: number } | undefined