다형성 컴포넌트 정리
No Filled
다형성 컴포넌트란?
asprops를 통해 렌더링될 HTML 요소나 컴포넌트를 동적으로 변경할 수 있는 컴포넌트 하나의 컴포넌트로 다양한 요소를 표현하면서도 타입 안정성을 유지
다형성 컴포넌트 관련 코드를 보게 되어 한 번 기본 개념을 정리해보자 한다.
기본적인 타입 코드는 다음과 같이 구성할 수 있다.
import {
ElementType,
ComponentPropsWithRef,
ComponentPropsWithoutRef,
ReactElement
} from 'react';
export type AsProp<C extends ElementType> = {
as?: C;
};
export type PropsToOmit<C extends ElementType, P> = keyof (AsProp<C> & P);
export type PolymorphicComponentProps<
C extends ElementType,
Props = {}
> = Props &
AsProp<C> &
Omit<ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
export type PolymorphicRef<C extends ElementType> =
ComponentPropsWithRef<C>['ref'];
export type PolymorphicComponentType<
DefaultElement extends ElementType,
Props = {}
> = <C extends ElementType = DefaultElement>(
props: PolymorphicComponentProps<C, Props>
) => ReactElement | null;타입 코드 분석하기
AsProp
export type AsProp<C extends ElementType> = {
as?: C;
};as prop의 타입을 정의한다.
// C = 'button'일 때
type ButtonAs = AsProp<'button'>;
// { as?: 'button' }
// C = 'a'일 때
type LinkAs = AsProp<'a'>;
// { as?: 'a' }PropsToOmit
export type PropsToOmit<C extends ElementType, P> = keyof (AsProp<C> & P);타입 충돌을 방지하기 위해 제거할 prop 키들을 추출한다.
컴포넌트 고유 props와 HTML 요소의 기본 props가 겹칠 때,
컴포넌트 고유 props를 우선시하기 위해 HTML 요소에서 해당 키를 제거한다.
type ButtonOwnProps = {
variant: 'primary' | 'secondary';
};
// 1단계: AsProp<C> & P를 병합
AsProp<'button'> & ButtonOwnProps
// { as?: 'button'; variant: string }
// 2단계: keyof로 키만 추출
keyof { as?: 'button'; variant: string }
// 'as' | 'variant'PolymorphicComponentProps
export type PolymorphicComponentProps<
C extends ElementType,
Props = {}
> = Props &
AsProp<C> &
Omit<ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;다형성 컴포넌트의 최종 props 타입을 생성한다.
앞서 정의한 Omit<ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>을 통해 충돌 키를 제거한다.
Props // 1. 컴포넌트 고유 props
& AsProp<C> // 2. as prop 추가
& Omit<ComponentPropsWithoutRef<C>, // 3. HTML 요소의 기본 props
PropsToOmit<C, Props>> // (충돌 키 제거)PolymorphicRef
export type PolymorphicRef<C extends ElementType> =
ComponentPropsWithRef<C>['ref'];ref 타입을 별로도 추출한다.
// C = 'button'일 때
PolymorphicRef<'button'>
// Ref<HTMLButtonElement>
// C = 'a'일 때
PolymorphicRef<'a'>
// Ref<HTMLAnchorElement>
// C = 'input'일 때
PolymorphicRef<'input'>
// Ref<HTMLInputElement>PolymorphicComponentType
export type PolymorphicComponentType
DefaultElement extends ElementType,
Props = {}
> = <C extends ElementType = DefaultElement>(
props: PolymorphicComponentProps<C, Props>
) => ReactElement | null;forwardRef와 함께 사용될 컴포넌트의 타입 시그니처를 정의한다.
예시: Button 컴포넌트
다형성 컴포넌트는 forwardRef와 함께 사용한다.
이는 부모 컴포넌트가 ref를 통해
해당 타입을 기반으로 Button 컴포넌트를 만들어보면 다음과 같이 사용할 수 있다.
import { forwardRef, ElementType } from 'react';
import {
PolymorphicComponentProps,
PolymorphicRef,
PolymorphicComponentType
} from './polymorphic.types';
// Step 1: 컴포넌트 고유 Props 정의
type ButtonOwnProps = {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
};
// Step 2: 다형성 Props 타입 생성
type ButtonProps<C extends ElementType = 'button'> = PolymorphicComponentProps<
C,
ButtonOwnProps
>;
// Step 3: 컴포넌트 구현
const Button: PolymorphicComponentType<'button', ButtonOwnProps> = forwardRef(
<C extends ElementType = 'button'>(
{
as,
variant = 'primary',
size = 'md',
isLoading = false,
children,
className,
...restProps
}: ButtonProps<C>,
ref?: PolymorphicRef<C>
) => {
const Component = as || 'button';
const buttonClass = `btn btn-${variant} btn-${size} ${className || ''}`;
return (
<Component ref={ref} className={buttonClass} {...restProps}>
{isLoading ? 'Loading...' : children}
</Component>
);
}
);
Button.displayName = 'Button';
export default Button;