function Typography({ children, variant = 'Body1', className = '', ...props }: Props) {
return (
<p className={cn(variantClasses({ type: variant }), className)} {...props}>
{children}
</p>
);
}
처음엔 p태그가 고정인 것을 인지하지 못하고 있었는데 다른 컴포넌트를 만들고계신 팀원분께서 타이포그라피가 p태그로 고정된것이 아쉽다고 말씀해주셨습니다.
저도 이 부분에 대해서 생각해보았는데 검색 엔진 최적화(SEO)나 접근성 측면에서도 마찬가지로도 아쉽다고 생각이 됐습니다. 또 버튼 안에 들어가는 텍스트는 span이나 label으로 감싸줘야 더 적절한데, Typography가 무조건 p태그니까 이걸로 해결하는 방법은 또 다른 컴포넌트를 만들어야하는 수밖에 없다고 느꼈습니다.
mui 디자인에서는 어떻게 하나 찾아봤는데 대부분은 태그명을 prop을 주는 것을 확인했습니다
다른 Typography컴포넌트처럼 태그를 prop으로 받도록 하는 것으로 하였습니다
이런 컴포넌트를 다형성 컴포넌트라고 부릅니다.
들어가기에 앞서 Polymorphism은 한국어로 다형성이라고 부르는데, 여러 개의 형태를 가진다라는 의미를 가진 그리스어에서 유래된 단어다. 그럼 이 글의 제목에 포함된 Polymorphic은 다형의 혹은 다양한 형태의 등으로 표현할 수 있을 것이다. 컴퓨터 과학에서 다형성은 프로그래밍적인 요소가 여러 형태로 표현 될 수 있는 것을 의미하는데 보통은 객체가 여러 자료형으로 나타날 수 있음을 표현할 때 사용한다.
Polymorphic 컴포넌트는 리액트(React)에서 하나의 컴포넌트가 여러 다른 HTML 태그 또는 컴포넌트를 유연하게 렌더링할 수 있도록 하는 패턴입니다.
type ButtonProps = {
as?: React.ElementType;
children: React.ReactNode;
};
export function Button({ as: Component = 'button', children, ...props }: ButtonProps) {
return <Component {...props}>{children}</Component>;
}
<Button>기본 버튼</Button> // <button>기본 버튼</button>
<Button as="a" href="/about">링크</Button> // <a href="/about">링크</a>
<Button as="div">디브</Button> // <div>디브</div>
as라는 prop에 a, div와 같은 태그명을 넣어서 다른 태그로서 사용할 수 있게 합니다.
type TypographyProps = {
variant: TypographyVariant;
className?: string;
as?: React.ElementType;
children: React.ReactNode;
} & ComponentPropsWithoutRef<T>;
function Typography({
variant = 'Body1',
as: Component = 'p',
...
}: TypographyProps) {
return (
<Component className={cn(variantClasses({ type: variant }), className)} {...props}>
{children}
</Component>
);
}
이렇게 간단하게 as속성을 받아서 사용할 수 있게되었어요 ㅎㅎ



link나 input같은 태그는 단일태그이기 때문에 Typography태그의 prop으로 받기에는 적절하지 않습니다. 그래서 아까 받은 as?: React.ElementType; 를 좁혀야 더 안전하게 사용할 수 있습니다.
그래서 자주사용하는
'p' , 'div' , 'label' , 'span' 요 4개의 태그로 추려서 받아보기로 정했습니다.
type TypographyProps = {
as?: 'span' | 'div' | 'label' | 'p';
// ...
} & ComponentPropsWithoutRef<'div' | 'span' | 'label' | 'p'>;
이렇게 되면 참 좋겠지만....
이 두 태그가 가진 props가 서로 다르고, 일부는 충돌하거나 존재하지 않기 때문에
타입스크립트가 "이거 뭐가 뭔지 모르겠는데?" 하고 타입 충돌을 내는 상황이 발생합니다.
실제 문제 상황:
Apply to Typography.t...
<Component {...props}>
Component가 label일 때는 label에 맞는 props만 받아야 하는데
div의 props도 함께 전달되려고 해서 충돌이 발생
특히 이벤트 핸들러(onCopy 등)가 서로 다른 타입을 가져서 문제가 됨
제네릭이란?
함수나 타입을 작성할 때, 구체적인 타입을 나중에 넘겨서 결정할 수 있게 하는 도구
type TypographyProps<T extends ElementType> = {
as?: T;
} & ComponentPropsWithoutRef<T>;
여기서 T는 'div'일 수도 있고 'label'일 수도 있습니다. 그리고 그 선택에 따라 타입스크립트가 정확한 HTML props를 추론해줍니다.
type AllowedTag = 'p' | 'div' | 'label' | 'span';
type TypographyProps<T extends AllowedTag> = {
as?: T;
className?: string;
children?: ReactNode;
variant?: TypographyVariant;
} & Omit<ComponentPropsWithoutRef<T>, 'as' | 'className' | 'children'>;
제네릭은 사용자가 넘긴 태그 (as="label")의 타입을 나타냅니다.
T는 'p' | 'div' | 'label' | 'span' 중 하나만 될 수 있게되고, 이 T는 사용자가 as로 넘긴 태그에 따라 자동으로 추론됩니다
ComponentPropsWithoutRef를 통해 T에 해당하는 기본 props를 자동 추론해줍니다
다음과 같이 사용할 수 있습니다.
<Typography as="label" htmlFor="name">이름</Typography>
className, children, as는 우리가 수동으로 정의했기 때문에 충돌을 막기 위해 Omit으로 제외합니다.
export function Typography<T extends AllowedTag = 'p'>({
as,
className,
children,
variant = 'Body1',
...props
}: TypographyProps<T>) {
const Component = as || 'p';
// as가 주어지면 그걸 컴포넌트로, 아니면 기본값으로 'p' 사용
const createTypography = (variant: TypographyVariant) => {
function Component<T extends AllowedTag = 'p'>(props: Omit<TypographyProps<T>, 'variant'>) {
return <Typography variant={variant} {...props} />;
}
return Component;
};
짜쨘~ ✨ 이렇게 자동완성이 잘 뜨는 다형성 컴포넌트가 완성되었습니다
