TypeScript: 제네릭(Generic)과 유틸리티 타입 조합

ε( ε ˙³˙)з ○º·2025년 6월 22일
post-thumbnail

Intro

TypeScript에서 제공하는 유틸리티 타입은 Partial, Pick, Omit, Record처럼 자주 사용되는 타입 조작 패턴을 간결하게 표현할 수 있습니다.

하지만 유틸리티 타입만으로는 부족한 경우도 있어요. 동적으로 키를 선택해야 하거나, 타입 제약이 필요한 함수를 만들 때는 제네릭(Generic)과 함께 사용하는 게 더 효과적입니다.

이번 글에서는 유틸리티 타입과 제네릭을 함께 사용하는 예제를 정리해보았어요.


◻️Pick과 제네릭으로 안전한 속성 추출 함수 만들기

이 함수는 객체에서 필요한 필드만 뽑아서 새로운 객체로 만들어주는 유틸 함수입니다.

함수 시그니처를 살펴보면 T는 전체 객체의 타입이며 K extends keyof T는 선택 가능한 키를 제한하고 Pick<T, K>는 선택한 키만 포함된 부분 객체 타입을 생성합니다.

Pick<T, K>는 "T 타입에서 K 키만 남긴 새 타입을 만든다"는 의미로 Pick<User, "name" | "email">은 아래와 같은 타입을 만들어요.

// Pick<User, "name" | "email">
{
  name: string;
  email: string;
}

또한 K extends keyof T 제약을 통해 존재하지 않는 키를 넘기면 컴파일 타임에 에러가 발생하도록 타입 안정성을 확보할 수 있어요.

💡 실무에서 Pick을 주로 사용하는 경우

  • 테이블/카드 UI에서 일부 필드만 미리보기로 표시할 때
  • API 요청 전에 불필요한 필드를 제거하고 필요한 값만 전송할 때
  • 폼 제출 시 선택된 필드만 서버에 넘길 때

이처럼 제네릭을 활용하면 입력 객체와 추출 키에 따라 결과 타입이 정확히 추론되기 때문에 런타임 오류를 줄이면서도 재사용 가능한 함수를 만들 수 있습니다.


◻️Partial과 제네릭으로 선택적 수정 함수 만들기

이 함수는 기존 객체에 일부 필드만 덮어써서 수정된 결과를 반환하는 유틸 함수입니다. Partial<T>는 타입 T의 모든 속성을 선택적(optional)으로 만들어주기 때문에 업데이트할 필드만 골라서 넘길 수 있습니다.

💡 실무에서 Partial을 주로 사용하는 경우

  • PATCH API 요청: 전체 필드가 아닌 변경 필드만 보낼 때
  • 상태 업데이트: 기존 상태를 복사한 후, 일부 필드만 변경할 때
  • React에서 form 입력값을 부분적으로 저장하거나 검증할 때

이 함수는 제네릭을 활용하기 때문에 User, Product, Post 등 어떤 객체 타입에도 재사용할 수 있고 updates 객체에 존재하지 않는 필드를 넘기면 컴파일 타임에 타입 에러를 발생시켜 안전하게 막아줍니다.

Partial<T>와 제네릭 조합은 유연한 타입을 제공과 함께 실제 객체 조작에서 타입 안전성과 재사용성을 동시에 확보할 수 있는 실용적인 패턴입니다.


◻️ Record와 제네릭으로 키-값 매핑 객체 만들기

이 코드는 Status라는 문자열 리터럴 유니언 타입을 key로 모든 상태에 대응되는 메시지를 값으로 갖는 매핑 객체를 정의한 예시입니다.

여기서 사용한 Record<K, T>는 "K에 있는 모든 키를 갖고, 각 키의 값은 타입 T여야 한다"는 유틸리티 타입입니다.

Record<Status, string>은 아래와 같은 타입을 의미합니다.

// Record<Status, string>
{
  loading: string;
  success: string;
  error: string;
}

💡 실무에서 Record을 주로 사용하는 경우

  • 상태 라벨 관리: API 상태 코드를 사용자에게 보여줄 메시지 매핑
    ("REJECTED""요청이 반려되었습니다.")
  • 권한 관리: 특정 사용자 그룹의 페이지 접근 여부 설정 ("admin"true, "guest"false)
  • 데이터 포맷 관리: 테이블, 상세 페이지에서 데이터 출력 포맷 지정
    ("price" → 가격 포맷터, "date" → 날짜 포맷터)

이렇게 작성하면 각 키에 맞는 값을 반드시 정의해야 하고, 정의되지 않은 키를 사용하면 타입 오류가 발생해요.

Record<K, T>는 반복적인 매핑 구조를 정확하게 타입으로 보장하면서도 제네릭과 함께 사용하면 다양한 키 타입에 대해 확장성과 재사용성을 모두 갖춘 타입 안전한 매핑 도구를 만들 수 있습니다.


◻️ DeepPartial 제네릭으로 커스텀 유틸리티 타입 만들기

사용자 정보처럼 중첩된 구조의 객체를 부분적으로 수정해야 할 때 기본 Partial<T>만으로는 부족할 수 있어요.

예를 들어 음과 같은 사용자 타입이 있다고 가정해보면, Partial<User>를 사용하면 profile 전체를 생략하거나 교체할 수는 있지만 contact.email만 선택해서 수정하려 하면 타입 오류가 발생합니다.

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

✅ 예제: 사용자 정보 수정 API 요청

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

Partial<T>는 객체의 바로 아래 단계 필드만 선택적으로 바꿀 수 있지만, DeepPartial<T>는 객체 안의 중첩된 필드까지 모두 선택적으로 바꿀 수 있습니다.

이런 구조는 깊이 있는 객체를 수정할 때 필요한 필드만 안전하게 선택할 수 있도록 타입을 설계하는 데 유용합니다.

이렇게 작성하면 contact.email만 수정하고 싶은 상황에서도 타입 에러 없이 필요한 필드만 안전하게 넘길 수 있어요.

✅ 🔍 Partial<T>와 차이

Partial<T>를 사용하면 profile 속성 자체는 생략할 수 있지만
profile 안의 nameemail은 여전히 필수입니다.

  • 사용자 프로필 편집, 주소 편집 등 부분 필드 수정 UI
  • 상태 관리 라이브러리에서 deep merge 업데이트를 할 때
  • React Hook Form, Zod, Yup 등에서 초기값(defaultValues) 설정 시

이처럼 객체를 전체 교체하기보다는, 깊이 있는 구조 중 일부만 수정해야 하는 경우가 많기 때문에 DeepPartial<T> 같은 커스텀 타입 유틸은 실무에서 유용하게 사용할 수 있습니다.


💭 마무리하며

이처럼 Partial<T>, Pick<T, K>, Record<K, T> 같은 유틸리티 타입은 제네릭과 함께 조합해 사용하면 더 강력하고 유연한 타입 설계를 할 수 있습니다.

이번에 유틸리티 타입과 제네릭을 함께 정리해보면서, 실무에서 마주했던 타입 구조나 에러 상황을 더 구조적이고 깔끔하게 정리할 수 있었겠다는 생각이 들었습니다.

앞으로도 복잡한 타입 설계가 필요할 때 제네릭과 유틸리티 타입을 잘 활용해보고 싶어요. 💪🏻


📚 Reference


이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻

profile
차곡차곡 쌓아두기 💭

0개의 댓글