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 컴포넌트란?

먼저 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>>으로 되어있는데 ForwardRefExoticComponentExoticComponent 를 상속받아 함수 컴포넌트가 되는 것을 알 수 있어요.
(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 컴포넌트 한번 도입해보시는건 어떨까요?