TypeScript와 함께 확장 가능한 React 컴포넌트 만들기

TypeScript와 rest parameter로 재사용성, 확장성이 높은 컴포넌트를 만들어봅니다.

5 min read
views

최근 회사에서 컴포넌트를 만들다가 불편함을 느낀 것이 있다. 고유 html button 요소의 속성들을 사용하고 싶었는데, 이를 사용하기 위해서는 컴포넌트의 props에 onClick, disabled 등의 속성을 추가해야 했다.

export default function Button({
  children,
  onClick,
  disabled,
}: {
  children: ReactNode;
  onClick: () => void;
  disabled: boolean;
}) {
  return <button onClick={onClick}>{children}</button>;
}

근데 MUI, Chakra UI와 같은 UI 라이브러리를 사용해보면 Button이라는 React 컴포넌트이지만 기존 html 태그인 button의 속성들도 사용할 수 있다는 것을 알 수 있다. 마치 고유 html button 태그처럼 onClick, disabled 등의 속성말고도 수많은 속성들을 사용할 수 있었다. 어떻게 이렇게 확장성 높은 컴포넌트를 만들 수 있는지 궁금하여 알아보았다.

HTMLAttributes

다행히 @types/react에는 이러한 속성들을 정의해놓은 HTMLAttributes라는 타입이 있다. 이를 이용해 컴포넌트의 타입을 정의하면 기존 고유 html 태그의 속성들에 대해 IDE가 자동 완성을 제공해준다.

그리고 중요한 것은 ...props으로 나머지 속성들을 받아온다는 것이다. 이를 통해 컴포넌트의 확장성을 높일 수 있다.

type ButtonProps = HTMLAttributes<HTMLButtonElement>;
 
export function Button({ children, ...props }: ButtonProps) {
  return <button {...props}>{children}</button>;
}

HTMLButtonElement는 React에서 사용하는 onChange가 아니라 브라우저에서 사용하는 onchange를 담고 있다. 따라서 @types/react에서 제공하는 HTMLAttributes에 제네릭으로 HTMLButtonElement를 넘겨 React가 사용하는 이벤트들로 바꾸어주어야 한다.

  • HTMLButtonElement -> HTMLElement -> GlobalEventHandlers 으로 d.ts 파일들을 들어가다보면 onchange가 나온다는 것을 알 수 있다.
  • 반면, HTMLAttributes -> DOMAttributes 으로 들어가면 onChange가 나온다는 것을 알 수 있다.


이제 사용하는 곳에서는 기존 html button 태그처럼 onClick이나 disabled 속성을 사용할 수 있다.

ComponentPropsWithoutRef

또 다른 방법은 ComponentPropsWithoutRef라는 타입을 이용하는 것이다. 이름처럼 ref가 forward 되지 않은 컴포넌트에 사용하면 된다.

type ButtonProps = ComponentPropsWithoutRef<'button'>;
 
export function Button({ children, ...props }: ButtonProps) {
  return <button {...props}>{children}</button>;
}

ComponentPropsWithRef

반면에 ref가 forward 된 컴포넌트에는 ComponentPropsWithRef를 함께 사용하면 된다.

type ButtonProps = ComponentPropsWithoutRef<'button'>;
 
const Button = forwardRef(function InnerButton( // displayName을 표시하기 위함.
  { children, ...props }: ButtonProps,
  ref: ComponentPropsWithRef<'button'>['ref'],
) {
  return <button {...props}>{children}</button>;
});
 
export default function App() {
  const buttonRef = useRef<HTMLButtonElement>(null);
 
  function handleClickButton() {
    console.log('버튼 클릭');
  }
 
  return (
    <Button ref={buttonRef} onClick={handleClickButton}>
      버튼
    </Button>
  );
}

기존 props는 ComponentPropsWithoutRef로 받고, ref는 ComponentPropsWithRef로 받는다.

ComponentProps라는 타입도 있다. 하지만 이는 d.ts 파일의 주석을 보면 알 수 있듯이, ComponentProps 대신 ComponentPropsWithoutRef 혹은 ComponentPropsWithRef를 사용하라고 나와 있다. 그 이유를 이 링크에서 알 수 있었는데, ref가 forward 되었는지 안 되었는지 type 이름만으로 명시적이게 판단할 수 있게 하기 위함이라고 한다.

레퍼런스