Polymorphic한 React 컴포넌트 만들기
date
Apr 14, 2023
thumbnail
slug
Polymorphic한 React 컴포넌트 만들기
author
status
Published
tags
React
TypeScript
Polymorphic
summary
사내 디자인 시스템을 개발하며 Polymorphic 컴포넌트를 개발하게 된 경험을 다루고 있어요.
type
Post
updatedAt
Dec 13, 2023 11:30 AM
Polymorphic 컴포넌트란?왜 Polymorphic해야할까?#1 역할에 어울리는 태그 변화하기#2 여러 디자인은 유지하고 태그만 변화하기 Polymorphic한 컴포넌트 개발하기forwardRef 타입 분석하기마치며
이 글에서는..
사내 디자인 시스템을 개발하며 Polymorphic 컴포넌트를 개발하게 된 경험을 다루고 있어요.
Polymorphic 컴포넌트란?
먼저 Polymorphic은
다형성
이라고 해요.리액트에서의 다형성,
Polymorphic Component
는 다양한 형태로 변화할 수 있는 컴포넌트라고 말할 수 있어요.Polymorphic Component
는 코드에 따라서 어떤 HTML요소가 될 수도 있어야 하고 그에 따른 속성과 타입도 변화해야하는 것이죠.정리하면 무엇이든지 될 수 있는 React Component가 바로
Polymorphic Component
입니다.왜 Polymorphic해야할까?
이번에
Polymorphic
을 적용한 컴포넌트는 Stack
이라는 컴포넌트이에요.
flex
와 같은 단순한 레이아웃을 더 편하게 사용할 수 있는 기능이라고 생각하면 될 것 같은데 디자인 시스템에 사용되는 컴포넌트이니 Stack
이라는 기본적인 기능에 충실하되, 여러 태그로 변화할 수 있는 컴포넌트를 개발하려고 했습니다.코드를 보면서 하나씩 예를 들어볼게요.
#1 역할에 어울리는 태그 변화하기
const Example = () => {
return (
<Box>
<p>Some contents</p>
<p>Some contents</p>
<p>Some contents</p>
</Box>
);
};
const Example = () => {
return (
<Box>
<BlogPostCard />
<BlogPostCard />
<BlogPostCard />
<BlogPostCard />
</Box>
);
};
첫 번째 구조에는 Box 컴포넌트가 article 태그, 두번째 구조는 section태그가 더 어울릴 것 같네요.
#2 여러 디자인은 유지하고 태그만 변화하기
// 스타일이 적용된 버튼 컴포넌트
export const Button = ({ as, ...props }) => {
const Element = as || "button";
return (
<Element style={{ backgroundColor: "black", color: "white" }} {...props} />
);
};
const Example = () => {
return (
<div>
<Button>Route 이동하기</Button>
</div>
);
};
스타일이 적용된 버튼 컴포넌트를 눌렀을 때 다른 웹페이지로 이동해야하는 상황이라고 해봅시다
물론 window.open()
으로 가능하지만..! 올바른 태그를 사용하는게 좋으니까요 :)
이 버튼은 button이라는 태그가 적용되었고 다른 웹으로 이동하는 태그에는
a 태그
를 사용하는게 좋겠죠?const Example = () => {
return (
<div>
<a ref={'some url'}>
<Button>Route 이동하기</Button>
</a>
</div>
);
};
이런 방식으로 활용해도 좋겠지만
a > div
로 불필요한 중첩이 되는 것 같아보이네요.const Example = () => {
return (
<div>
<Button as='a'>Route 이동하기</Button>
</div>
);
};
as
키워드를 활용해서 Button 컴포넌트 자체를 a 태그
로 변화시키는 방식으로 활용할 수 있을 것 같아요.Polymorphic한 컴포넌트 개발하기
사용되는 타입들
- HTMLPropsWithOutRef<T>: Ref를 제외한
T
태그의 props type
- HTMLPropsWithRef<T>: Ref를 포함한
T
태그의 Props type
type BoxProps<T extends ElementType = 'div'> = HTMLPropsWithOutRef<T> & {
as?: T;
};
export const Box = <T extends ElementType = "div">({
as,
...props
}: BoxProp<T>) => {
const Element = as || "div";
return <Element {...props} />;
};
Box라는 컴포넌트를 예시로 가져왔어요.
타입을 제네릭으로 받고
HTMLPropsWithOutRef
를 통해서 props를 변환하는데 위 예시에서는 Ref를 사용할 수 없으니 Ref를 추가해볼게요.type BoxProps<T extends ElementType = 'div'> = HTMLPropsWithOutRef<T> & {
as?: T;
};
type BoxRefType<T extends ElementType = 'div> = Pick<ComponentPropsWithRef<T>, 'ref'>;
export const Box = forwardRef(<T extends ElementType = "div">(
{ as, ...props }: BoxProps<T>,
ref: BoxRefType<T>
) => {
const Element = as || "div";
return <Element {...props} />;
});
ComponentPropsWithRef
를 사용해서 ref의 타입을 선언하고 forwardRef로 ref를 전달하는 로직이에요.하지만 위 컴포넌트의 타입은 prop에 한에서만 적용되어있고 함수 자체에는 적용되어있지 않아요.
forwardRef 타입 분석하기
function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
interface ForwardRefExoticComponent<P> extends NamedExoticComponent<P> {
defaultProps?: Partial<P> | undefined;
propTypes?: WeakValidationMap<P> | undefined;
}
interface NamedExoticComponent<P = {}> extends ExoticComponent<P> {
displayName?: string | undefined;
}
interface ExoticComponent<P = {}> {
/**
* **NOTE**: Exotic components are not callable.
*/
(props: P): (ReactElement|null);
readonly $$typeof: symbol;
}
forwardRef의 return 타입을 보면
ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>
으로 되어있는데 ForwardRefExoticComponent
도 ExoticComponent
를 상속받아 함수 컴포넌트가 되는 것을 알 수 있어요.(props: P): (ReactElement|null);
을 보면 어떤 forwardRef가 어떤 타입을 return 해야할지 알 수 있어요.type BoxComponentType = <T extends ElementType = 'div'>(
props: BoxProps<T> & Pick<ComponentPropsWithRef<T>, 'ref'>,
) => ReactElement | null;
위 예시를 보고 Box컴포넌트의 타입을 작성해봤어요
export const Box: BoxComponentType = forwardRef(<T extends ElementType = "div">(
{ as, ...props }: BoxProps<T>,
ref: BoxRefType<T>
) => {
const Element = as || "div";
return <Element {...props} />;
});
이제 as 키워드에 따라 타입 추론이 잘되네요!
마치며
이런게 있으면 좋겠다~ 라는 막연한 생각에서 알게 된
Polymorphic
이라는 개념은 꼭 디자인 시스템이 아니더라도 확장성을 챙길 수 있다는 점에서 활용할 수 있을 것 같아요.HTML의 시멘틱 태그를 잘 활용하고 싶지만 기존의 컴포넌트를 활용해야하는 분들은 Polymorphic 컴포넌트 한번 도입해보시는건 어떨까요?