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 컴포넌트 한번 도입해보시는건 어떨까요?