
프로젝트를 하다 보면 Select 컴포넌트를 반복적으로 만들게 된다.
그런데 상황에 따라 value 타입이 달라지는 경우가 많다.
이럴 때마다 컴포넌트를 새로 만드는 대신,
제네릭(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>
);
};
interface CustomSelectProps<T extends string | number> {
value: T;
onChange: (event: SelectChangeEvent<T>) => void;
options: { label: string; value: T }[];
}
T extends string | numbervalue: Toptions: { value: T }[]👉 value와 options의 타입이 항상 일치하도록 보장된다
즉, 이런 실수를 방지할 수 있다:
// ❌ 잘못된 케이스
value: "DAILY"
options: [{ value: 1 }]
export const CustomSelect = <T extends string | number>(props: CustomSelectProps<T>) => {
이렇게 선언하면, 사용할 때 별도로 타입을 지정하지 않아도
value와 options를 기반으로 타입이 자동 추론된다
onChange={onChange as (event: SelectChangeEvent<unknown>) => void}
MUI의 Select는 내부적으로 SelectChangeEvent<unknown>을 사용하기 때문에
제네릭 타입 SelectChangeEvent<T>와 충돌이 발생한다.
그래서 다음 전략을 사용한다:
외부 props에서는 SelectChangeEvent<T>로 타입 안전 유지
내부에서는 unknown으로 캐스팅하여 연결
👉 타입 안전성과 라이브러리 제약을 동시에 해결하는 방법이다
<MenuItem
key={String(r.value)}
value={r.value as string | number}
>
<CustomSelect<ReportType>
value={reportType}
onChange={onChangeReportType}
options={reportTypeOptions}
/>