2024-07-13
최근에 회사에서 기존 컴포넌트를 제네릭으로 개선한 경험을 기록하기 위해 포스팅하려고 합니다.
여기 SelectBox라는 컴포넌트가 있습니다. (문맥상 필요 없는 코드는 생략했습니다!)
이 컴포넌트의 문제점이 눈에 들어오시나요?
1import { Dispatch, SetStateAction } from "react";
2
3type SelectBoxType = {
4 selected: number | string | null;
5 setSelected: Dispatch<SetStateAction<number | string | null>>;
6 options: number[] | string[];
7};
8
9export const SelectBox = ({
10 selected,
11 setSelected,
12 options,
13}: SelectBoxType) => {
14 return <></>;
15};
A라는 컴포넌트에서 이 SelectBox를 사용해볼게요.
아… 타입 에러가 발생하네요.
SelectBox에서 selected와 setSelected의 타입이 각각 아래와 같이 정의되어 있어서 타입 에러가 발생합니다.
1 selected: number | string | null;
2 setSelected: Dispatch<SetStateAction<number | string | null>>;
타입을 아래와 같이 수정하면 에러는 발생하지 않습니다.
하지만 A 컴포넌트에서 사용되는 SelectBox는 options가 number 타입이니까, selected의 타입을 number | string | null로 지정하는 것은 좀 이상하죠? options가 number 타입이면 selected가 string 타입이 되는 일은 없습니다.
이렇게 되면 타입 에러는 발생하지 않겠지만, 코드의 문맥이 이상해지고 나중에 코드 리딩이 힘들어질 수 있습니다.
이 문제는 제네릭으로 쉽게 해결할 수 있습니다!
SelectBox를 아래와 같이 수정해볼게요.
1import { Dispatch, SetStateAction } from "react";
2
3type SelectBoxType<T> = {
4 selected: T;
5 setSelected: Dispatch<SetStateAction<T>>;
6 options: number[] | string[];
7};
8
9export const SelectBox = <T extends number | string | null>({
10 selected,
11 setSelected,
12 options,
13}: SelectBoxType<T>) => {
14 return <></>;
15};
16
이제 selected는 제네릭 타입이 되었고, number | string | null 타입만 들어오도록 제약을 줬습니다.
A 컴포넌트에서 확인해보면…?
이제 number | null 타입을 지정해도 타입 에러가 발생하지 않습니다. 👍
options를 string 타입으로 사용해도 문제가 없는 것도 확인할 수 있고요.
한 가지 첨언하자면, options 타입도 제네릭으로 바꿔주는 게 좋습니다. options 타입이 number[] | string[]라는 것은 맞지만, 엄밀히 말하자면 selected의 타입과 일치해야 하니까, 아래와 같이 options의 타입을 NonNullable<T>[]로 수정하겠습니다.
1import { Dispatch, SetStateAction } from "react";
2
3type SelectBoxType<T> = {
4 selected: T;
5 setSelected: Dispatch<SetStateAction<T>>;
6 options: NonNullable<T>[];
7};
8
9export const SelectBox = <T extends number | string | null>({
10 selected,
11 setSelected,
12 options,
13}: SelectBoxType<T>) => {
14 return <></>;
15};
16
이렇게 수정하면 이제 개발자(저…)가 엉뚱한 짓을 하더라도 TypeScript가 오류를 발생시켜주니까, 코드를 더욱 안정적으로 작성할 수 있게 되었습니다.
타입 에러가 발생하지 않도록 타입을 정의하는 것보다, 문맥을 고려해서 타입을 정의하는 것이 중요합니다. 당연한 이야기처럼 들릴 수 있겠지만, 의외로 개발을 하다보면 가끔씩 타입을 무작정 정의하는 경우를 종종 보게 됩니다.
제네릭을 잘 활용하면서 개발해봅시다~! 😉
© 2025 Kae All Rights Reserved.