2025-11-01
다형성 컴포넌트 정리
다형성 컴포넌트란?
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;