TypeScript 제네릭: React 개발자를 위한 가이드

뚜루미·2025년 5월 5일

React

목록 보기
39/39

✅ 제네릭(Generics)이란?

  • 정의: 타입을 파라미터처럼 전달받아, 다양한 타입에서도 재사용 가능한 코드를 작성할 수 있도록 하는 기능.
  • 왜 사용할까?
    • 재사용성 + 타입 안전성 보장
    • 같은 로직을 여러 타입에 맞춰 반복 작성할 필요 없음

✅ 제네릭 기본 문법

기본 형태

함수나 컴포넌트가 입력 타입에 따라 유연하게 동작하도록 만듬

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

identity<number>(10);   // 반환 타입은 number
identity<string>("Hello"); // 반환 타입은 string

화살표 함수로 작성

일반 함수뿐 아니라 화살표 함수에서도 동일하게 사용 가능.

const identity = <T>(arg: T): T => arg;

✅ React Query + 제네릭으로 API 타입 안전성 확보

API 호출 시 응답 타입을 제네릭으로 지정해, 타입 오류를 사전에 방지 가능

import { useQuery } from '@tanstack/react-query';

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

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

const { data, isLoading } = useQuery<User>(['user'], fetchUser);
  • useQuery로 API 응답 타입 안전성 확보
  • API 변경 시 타입 오류로 빠르게 인지 가능

✅ 재사용 가능한 Dropdown 컴포넌트

다양한 타입의 데이터를 처리할 수 있는 범용 UI 컴포넌트

type DropdownProps<T> = {
  options: T[];
  getLabel: (option: T) => string;
  onSelect: (option: T) => void;
};

function Dropdown<T>({ options, getLabel, onSelect }: DropdownProps<T>) {
  return (
    <select onChange={(e) => onSelect(options[e.target.selectedIndex])}>
      {options.map((o, i) => <option key={i}>{getLabel(o)}</option>)}
    </select>
  );
}
  • 다양한 데이터 타입 대응
  • 공통 컴포넌트로 확장성 확보

✅ Form 상태 관리 컴포넌트

폼 구조가 달라져도 하나의 컴포넌트로 다양한 타입의 상태를 관리할 수 있습니다.

type FormProps<T> = {
  initialValues: T;
  onSubmit: (values: T) => void;
};

function Form<T>({ initialValues, onSubmit }: FormProps<T>) {
  const [values, setValues] = useState<T>(initialValues);

  return (
    <form onSubmit={(e) => { e.preventDefault(); onSubmit(values); }}>
      <button type="submit">Submit</button>
    </form>
  );
}
  • 폼 데이터 타입에 따라 유연하게 대응 가능
  • 확장성 있는 폼 컴포넌트 구현

✅ 커스텀 훅(Custom Hook) + 제네릭


커스텀 훅에 제네릭을 적용하면 다양한 타입의 데이터를 안전하게 관리할 수 있습니다.

function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    const item = window.localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  const setValue = (value: T) => {
    setStoredValue(value);
    window.localStorage.setItem(key, JSON.stringify(value));
  };

  return [storedValue, setValue] as const;
}
  • 로컬 스토리지에 다양한 타입 저장/관리
  • 커스텀 훅의 재사용성과 타입 안전성 확보

✅ 제네릭 타입 제한하기 (Constraints)


특정 조건을 만족하는 타입만 허용해 안전성

function getValue<T extends { id: number }>(item: T): number {
  return item.id;
}

getValue({ id: 1, name: 'Apple' });  // ✅ OK
// getValue(123);                   // ❌ Error
  • 활용 예시: 공통 속성(id, length, name 등)이 있는 데이터 처리 시 사용.
  • 실무에서 API 데이터 처리, UI 리스트 렌더링 등에 자주 활용.

✅ 기본 타입(Default Type) 지정

제네릭 타입에 기본값을 설정해, 더 유연하게 사용할 수 있습니다.

type ApiResponse<T = any> = {
  data: T;
  error?: string;
};

const res: ApiResponse = { data: "test" };  // T는 any로 자동 설정
  • 공통 유틸리티 타입 정의 시 편리.
  • 타입 명시가 필요 없는 경우 코드가 간결해짐.

✅ 의미 있는 제네릭 네이밍

무조건 , 보다는

의도를 드러내는 이름

❌ Bad✅ Good
<T, U><Key, Value>
<T extends {}>
  • 협업 시 가독성 향상
  • 특히 공통 컴포넌트, 유틸리티 함수에서 효과적

🚨 제네릭을 피해야 할 때

  • 타입이 고정적일 때 → 유니온 타입 사용
type ButtonType = "primary" | "secondary";
  • 타입 추론이 잘 되는 경우 → 굳이 명시 X
  • 과도한 제네릭 사용으로 코드 복잡도 증가

0개의 댓글