Typescript | 다양한 타입의 값으로 공용 Select 사용하기

샘샘·2026년 2월 26일

TypeScript

목록 보기
14/14
post-thumbnail

프로젝트를 하다 보면 Select 컴포넌트를 반복적으로 만들게 된다.
그런데 상황에 따라 value 타입이 달라지는 경우가 많다.

  • 문자열 enum (예: "DAILY" | "WEEKLY")
  • 숫자 값 (예: 1 | 2 | 3)

이럴 때마다 컴포넌트를 새로 만드는 대신,
제네릭(Generic)을 활용하면 타입 안전하게 재사용 가능한 Select를 만들 수 있다.

"use client";

import { MenuItem, SelectChangeEvent } from "@mui/material";
import { SelectBox, selectMenuStyle, selectPaperStyle } from "components/drawer/styles";

interface CustomSelectProps<T extends string | number> {
  value: T;
  onChange: (event: SelectChangeEvent<T>) => void;
  options: { label: string; value: T }[];
}

export const CustomSelect = <T extends string | number>(props: CustomSelectProps<T>) => {
  const { value, onChange, options } = props;

  return (
    <SelectBox
      size="small"
      value={value}
      onChange={onChange as (event: SelectChangeEvent<unknown>) => void}
      MenuProps={{
        PaperProps: {
          sx: { ...selectPaperStyle() },
        },
        MenuListProps: {
          sx: {
            ...selectMenuStyle(),
          },
        },
      }}
    >
      {options.map(r => (
        <MenuItem key={String(r.value)} value={r.value as string | number} sx={{ fontSize: 14 }}>
          {r.label}
        </MenuItem>
      ))}
    </SelectBox>
  );
};

1️⃣ 제네릭으로 value 타입을 유연하게 받는다

interface CustomSelectProps<T extends string | number> {
  value: T;
  onChange: (event: SelectChangeEvent<T>) => void;
  options: { label: string; value: T }[];
}
  • T extends string | number
    - Select의 value는 string 또는 number만 허용한다
  • value: T
    - 사용하는 쪽에서 타입을 결정한다
  • options: { value: T }[]
    - options의 value도 동일한 타입을 강제한다

👉 value와 options의 타입이 항상 일치하도록 보장된다

즉, 이런 실수를 방지할 수 있다:

// ❌ 잘못된 케이스
value: "DAILY"
options: [{ value: 1 }]

2️⃣ 컴포넌트를 제네릭으로 선언한다

export const CustomSelect = <T extends string | number>(props: CustomSelectProps<T>) => {

이렇게 선언하면, 사용할 때 별도로 타입을 지정하지 않아도
value와 options를 기반으로 타입이 자동 추론된다

3️⃣ MUI SelectChangeEvent 타입 문제 해결

onChange={onChange as (event: SelectChangeEvent<unknown>) => void}

MUI의 Select는 내부적으로 SelectChangeEvent<unknown>을 사용하기 때문에
제네릭 타입 SelectChangeEvent<T>와 충돌이 발생한다.

그래서 다음 전략을 사용한다:

외부 props에서는 SelectChangeEvent<T>로 타입 안전 유지
내부에서는 unknown으로 캐스팅하여 연결

👉 타입 안전성과 라이브러리 제약을 동시에 해결하는 방법이다

4️⃣ MenuItem에서 타입 맞추기

<MenuItem
  key={String(r.value)}
  value={r.value as string | number}
>
  • key는 string이어야 하므로 String()으로 변환
  • MUI MenuItem의 value는 string | number 타입을 기대하기 때문에 캐스팅

사용 예시

            <CustomSelect<ReportType>
              value={reportType}
              onChange={onChangeReportType}
              options={reportTypeOptions}
            />

0개의 댓글