2023. 1. 19. 16:37ㆍReact/고민거리들
0. 배경
React의 props로 Generic을 넘겨줘야 할 일이 종종 있는데 대표적으로 드롭다운(셀럭터)를 만들 때가 바로 그렇다.
직관적으로 생각해보면 새로 컴포넌트 인스턴스를 생성할 때 적절한 타입을 캐스팅 해주면 되지 않을까 싶은데 React.FC에서 Generic이 지원되지 않는다는 점에서 문제가 일어난다.
이 글에서는 드롭다운에서 Generic을 왜 넘겨줘야하는지 그 전략과 어떻게 해결했는지 공유해보려고 한다.
1. 왜 제네릭을 사용하려고 하는가?
<Selector
categories={
[
{ id: 0, content: '이름 순 정렬', customData: 'NAME' },
{ id: 1, content: 'id 순 정렬', customData: 'ID' },
{ id: 2, content: '검색한 챔피언 순', customData: 'RECENTLY_SEARCHED' },
]
}
initId={0}
callback={(item) => onChangeSortFilter(item.customData)}
/>
드롭다운을 구현할 때 크게 고려해야할 것은 두 가지이다.
- UI로 나타낼 string.
- 상태관리 등 내부 로직에 필요한 커스텀 데이터.
드롭다운을 클릭 했을 때 부모 컴포넌트에서 내려준 콜백함수를 실행한다.
그리고 우리가 사전에 정의했던 커스텀 데이터를 return 해주면 부모 컴포넌트에서 확인할 수 있는 구조로 정의했다.
드롭다운을 효과적으로 사용할 수 있는 상황은 사용자가 골라야할 데이터가 몇 개 없는 경우라고 생각한다. (개인적으로 10개 이하일 때 사용해야한다고 생각한다.)
예를 들어, 이번에 만들고 있는 서비스는 정렬을 위해 (이름 순, id 순, 최근 검색한 챔피언 순)으로 총 3 가지 범주를 제공한다. 이에 해당하는 내용을 따로 type을 정의해서 핸들러나 상태로 사용하는 전략을 세웠다.
export type ORDER_TYPE = 'NAME' | 'ID' | 'RECENTLY_SEARCHED';
위처럼 primitive type이 아니라 내부 로직으로 사용하기 위해서 특정 type이나 객체를 저장하고 싶을 때가 있다.
이런 경우에는 커스텀 데이터를 넣어둘 props를 unknown으로 정의하고 제네릭으로 타입 캐스팅을 해줘야하는데, 컴포넌트를 선언할 때 도대체 어떻게 타입을 넣어줘야하는지 감이 안잡혔었다.
2. interface 캐스팅에 사용하던 React.FC와 제네릭
//@types\react\index.d.ts
// Class Interfaces
// ----------------------------------------------------------------------
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
익명 함수 혹은 화살표 함수로 props를 쉽게 캐스팅 해줄 수 있는 인터페이스 중에 React.FC가 있다.
React가 원래 TypeScript를 사용하던 프레임워크가 아니기 때문에 따로 설치한 @types/react에서 정의된 props를 보면 context나 props에 정의한 interface를 넣어주는걸 확인할 수 있다. (그리고 제네릭은 없다.)
결론적으로 제네릭을 사용할 때에는 React.FC를 사용하지 않아야 한다.
( + 다른 블로그에서 'React.FC 사용을 지양해야하는 이유' 와 같은 포스트가 있어서 읽어봤었는데 implicit children props랑 geneirc을 사용할 수 없다는 이유로 적혀있었다. React 18이 되면서 children props는 이제 명시적으로 작성해줘야하기 때문에 제네릭을 사용하는 경우만 고려해주면 되겠다.)
// withoutFC.tsx
interface MyInterface {
id: number;
content: string;
}
function myFunc = ({ id, content }: MyInterface) => {
//...
}
일반 hooks를 정의할 때 사용하는 함수처럼 객체와 그 객체의 프로퍼티를 정의해주는 인터페이스를 묶어주는 식으로 사용하면 제네릭을 사용할 수 있다. 정의되지 않은 값을 넣어주기 때문에 초기값은 unknown으로 처리해줬다.
3. 드롭다운 구현
// Selector.tsx
interface CategoryProps<T> {
id: number;
content: string;
customData?: T;
}
interface SelectorProps<T> {
categories: CategoryProps<T>[];
initId?: number;
placeholder?: string;
callback?: (item: CategoryProps<T>) => void;
}
const Selector = <T extends unknown>(props: SelectorProps<T>) => {
const { categories, initId, placeholder, callback } = props;
const [selectedCategory, setSelectedCategory] = useState<CategoryProps<T>>({ id: -1, content: '' });
//..
}
드롭다운도 두 가지 종류가 있을 수 있다고 가정했다.
- 최초값 중 특정 index의 값을 초기값으로 가지는 드롭다운. (ex. 서울 🔻)
- placeholder로 초기값이 없는 드롭다운. (ex. ----지역을 선택해주세요.----🔻)
두 값 모두 optional props로 받고, useEffect에서 처리해주면 쉽게 처리 가능하다.
이어서, 가장 중요한 기능 두 가지를 만들어야한다.
- 버튼을 눌렀을 때, 리스트를 토글하는 기능.
- 특정 값을 마우스 이벤트로 받았다면, 그 값을 콜백함수를 통해 부모 컴포넌트로 보내주는 기능.
// Selector.tsx
//..
const onHandlerToggleSelector = useCallback((e?: React.MouseEvent<HTMLButtonElement>) => {
e?.preventDefault();
e?.stopPropagation();
setOpenList((prev) => !prev);
}, []);
const onChangeSelectedItem = useCallback((e: React.MouseEvent<HTMLSpanElement>, category: CategoryProps<T>) => {
e.preventDefault();
e.stopPropagation();
setSelectedCategory(category);
callback && callback(category);
onHandlerToggleSelector();
}, [callback]);
return (
//여러분 입맛대로 만드세요!
)
3-1. 드롭다운 컴포넌트 생성
이 컴포넌트를 생성할 때 어떤 type으로 customData props에 바인딩 해줄지 정의해야한다.
// 드롭다운을 사용하려는 부모 컴포넌트.tsx
// customData로 사용한 타입 T가 콜백될 때 optional로 반환됨.
const Parent = () => {
// 드롭다운 콜백 핸들러
const onChangeSortFilter = useCallback((val?: ORDER_TYPE) => {
if (!val) return;
dispatch({ type: 'ORDER', val: val });
}, []);
return (
<div>
<Selector<ORDER_TYPE>
categories={
[
{ id: 0, content: '이름 순 정렬', customData: 'NAME' },
{ id: 1, content: 'id 순 정렬', customData: 'ID' },
{ id: 2, content: '검색한 챔피언 순', customData: 'RECENTLY_SEARCHED' },
]
}
initId={0}
callback={(item) => onChangeSortFilter(item.customData)}
/>
</div>
);
jsx 문법 안에서 꺾쇠로 타입을 넣어줘야한다.
신기한 점은 callback 등을 통해 받는 Generic이 optional로 반환된다는 점이다.
핸들러에 optional 체크를 넣어줘서 대응해야한다.
+ 개선하고 싶은 점
1. Default type 지정
위에서도 언급하긴 했지만, 드롭 다운에서 Generic이 유용한 경우는 customData로 특정 타입 혹은 객체를 넣어주는 경우이다. 만약 10개 이상의 많은 값을 넣어주거나, 범주로 제한되지 않는 매번 변경될 수 있는 값이 들어갈 수 있다. default type으로 string 등을 지정한다면 편할 것 같다.
const Selector = <T extends unknown>(props: SelectorProps<T | string>) => (X)
const Selector = <T extends unknown>(props: SelectorProps<Boolean(typeof T) ? T : string>) => (X)
문제는 아래 정의된 콜백 함수나 상태에서 저 string 때문에 타입 체커가 에러를 뿜어낸다.
삼항 연산자도 안된다. 이런..
string type을 지정했을 때 예외 상황을 모든 함수나 상태에서 처리해주는게 효율적인 해결방법인지 모르겠다.
'React > 고민거리들' 카테고리의 다른 글
React + TypeScript로 HOC 사용해보기 (0) | 2023.02.06 |
---|---|
아무무의 붕대투척으로 알아보는 라이엇 스킬 툴팁 해석 (0) | 2023.02.03 |
[TIL] static assets을 한 방에 불러와보자! (0) | 2022.11.14 |
[TIL] React.StrictMode는 두 번 렌더링 한다. (1) | 2022.11.12 |
[React] github page 그리고 react-router-dom (1) | 2022.10.03 |