React 컴포넌트에서 Conditional TypeScript Generics
Source: Dev.to
이전 기사 여기에서 저는 TypeScript 제네릭에 대한 논의를 계속하겠다고 약속했습니다 — React 컴포넌트를 다룰 때 특히 중요하다고 생각하는 주제입니다.
그 기사에서는 제네릭 컴포넌트의 기본 정의를 다루고, 제네릭이 언제 그리고 왜 유용한지에 대한 간단한 예시를 살펴보았습니다. 이번 포스트에서는 다음 단계로 조건부 제네릭에 초점을 맞추고, 이것이 실제 React 컴포넌트에 어떻게 적용될 수 있는지 살펴보겠습니다.
여기서는 제가 실제 프로젝트에서 작업하면서 경험한 예시를 살펴보고자 합니다. 이 예시는 제네릭 타입이 얼마나 우아하고 강력한지 보여줍니다.
문제: 제네릭이 없는 선택 컴포넌트
다음은 props에 따라 하나 또는 여러 옵션을 선택할 수 있는 선택 컴포넌트를 고려해 보겠습니다.
// Just an interface of option structure
interface Option {
id: number;
name: string;
}
// It's your value type
type Value = Option[] | Option | undefined;
// Component props interface
interface Props {
value?: Value;
options?: Option[];
multiple?: boolean;
onChange?: (value: Value) => void;
}
이 컴포넌트를 사용할 때 value 타입에 대해 완전히 안전하지 않습니다. TypeScript는 컴포넌트가 다중 선택 모드인지 아닌지 알 수 없기 때문입니다.
value?: Value와 onChange?: (value: Value) => void는 multiple?: boolean과 무관하게 두 상태 모두에서 항상 사용할 수 있습니다. 이는 충분히 엄격하지 않으며, 다음과 같은 보장을 원합니다:
multiple이false(또는 생략)인 경우 →value는 단일 객체(또는undefined)여야 합니다;multiple이true인 경우 →value는 배열이어야 합니다.
Props 인터페이스에 제네릭 도입
우리는 동일한 Option 인터페이스를 유지합니다:
interface Option {
id: number;
name: string;
}
이제 Props를 두 개의 제네릭 파라미터 O와 M으로 확장합니다:
interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
value?: M extends undefined | false ? O | undefined : O[];
options?: O[];
multiple?: M;
onChange?: (
values: M extends undefined | false ? O | undefined : O[]
) => void;
}
설명
| 제네릭 | 의미 |
|---|---|
O | Option을 확장; 옵션 타입을 나타냅니다. |
M | `boolean |
value | M extends undefined | false ? O | undefined : O[] – multiple이 false이거나 생략된 경우 단일 O(또는 undefined), 그렇지 않으면 배열 O[]. |
onChange | 콜백 인수에 대해 동일한 조건부 타입을 사용합니다. |
조건부 타입 추출
조건부 표현식을 반복하는 것은 번거롭기 때문에, 이를 자체 제네릭 타입으로 추출합니다:
type Value<O extends Option, M extends boolean | undefined> =
M extends undefined | false ? O | undefined : O[];
이제 Props가 훨씬 깔끔해집니다:
interface Props<O extends Option = Option, M extends boolean | undefined = undefined> {
value?: Value<O, M>;
options?: O[];
multiple?: M;
onChange?: (values: Value<O, M>) => void;
}
컴포넌트 자체에 제네릭 추가하기
컴포넌트도 제네릭이어야 하며, 해당 파라미터들을 Props에 전달해야 합니다:
export const SelectionExample = <
O extends Option = Option,
M extends boolean | undefined = undefined
>({
value = undefined,
multiple = false,
onChange = () => undefined,
options = [],
}: Props<O, M>) => {
const isOptionSelected = (option: O): boolean => {
// `value`와 `multiple`을 사용한 구현
return false;
};
const handleOptionClick = (option: O) => {
if (multiple) {
// `onChange`를 사용한 다중 선택 구현
} else {
// `onChange`를 사용한 단일 선택 구현
}
};
return (
<div>
{options.map((option) => {
const selected = isOptionSelected(option);
return (
<div key={option.id}>
{/* 옵션 UI 렌더링 */}
</div>
);
})}
</div>
);
};
Note: 제네릭 파라미터
<O, M>은 컴포넌트에 선언된 뒤Props에 전달됩니다.
제네릭 컴포넌트 사용하기
1️⃣ 다중 선택
export const UsageExample = () => {
const options: Option[] = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
return (
<SelectionExample
multiple
options={options}
onChange={(selected) => {
// `selected` is inferred as `Option[]`
console.log("selected:", selected);
}}
/>
);
};
2️⃣ 단일 선택
export const SingleUsage = () => {
const options: Option[] = [
{ id: 1, name: "First" },
{ id: 2, name: "Second" },
{ id: 3, name: "Third" },
];
return (
<SelectionExample
options={options}
onChange={(selected) => {
// `selected` is inferred as `Option | undefined`
console.log("selected:", selected);
}}
/>
);
};
두 경우 모두 TypeScript가 value의 형태와 onChange의 인자 타입을 올바르게 추론하여, 우리가 원했던 엄격한 보장을 제공합니다.
Takeaways
- Conditional generics는 props 간의 관계를 표현할 수 있게 해줍니다 (예:
multiple↔value형태). - 반복되는 조건부 로직을 재사용 가능한 제네릭 타입(
Value)으로 추출하면 코드가 DRY하고 가독성이 유지됩니다. - 컴포넌트 자체에 제네릭을 선언하고 이를 props 인터페이스에 전달하면 전체 컴포넌트가 타입‑안전성을 확보합니다.
이 패턴을 사용하면 props 값에 따라 API가 조정되는 고도로 재사용 가능하고 타입‑안전한 React 컴포넌트를 만들 수 있습니다. 즐거운 타입 작성 되세요!
다중 선택 (추가 예시)
export const UsageExample = () => {
return (
<>
{/* ... */}
{/* `values` is the selected options array */}
{console.log(values)}
</>
);
};
단일 선택 (추가 예시)
export const UsageExample = () => {
return (
<>
{/* ... */}
{/* `value` is the selected option */}
{console.log(value)}
</>
);
};
잘못된 사용
export const UsageExample = () => {
return (
<>
{/* ... */}
{/* `values` has type `Option | undefined` – this is a misuse when multiple is expected */}
{console.log(values)}
</>
);
};
이 글이 TypeScript 기능을 더 깊게 파고들도록 만들었을 수도 있습니다. 하지만 직접 비슷한 구현을 마치고 여러분의 컴포넌트가 얼마나 더 강력해졌는지 깨달으면 만족감을 느낄 것이라고 믿습니다.