[TS] 개발자처럼 사용하기 6 : 컴포넌트 타입을 주는 방법

Zero·2024년 2월 15일
8
post-thumbnail

아침에 일어난 나를 반겨주는 것은 타입스크립트에 대한 내용이었습니다.

해당 코드는 다음과 같습니다.

HTMLAttributes는 평소에 자주 사용하던 속성을 넘겨주는 부분이었는데 좋지 않다고 얘기를 하고있습니다. 어느 부분에서 문제인지 파악을 해봐야겠습니다.

아래부터 무엇이 문제인지 또 왜 위로갈수록 좋은 패턴인지에 대해서 정리해봅니다.

💩HTMLAttributes

흔히 사용하던 HTMLAttributes부터 확인을 들어가봅니다.

HTMLAttributes의 생김새는 다음과 같습니다.

interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
    // React-specific Attributes
    defaultChecked?: boolean | undefined;
    defaultValue?: string | number | readonly string[] | undefined;
    suppressContentEditableWarning?: boolean | undefined;
   
  	// ...
  
    itemRef?: string | undefined;
    results?: number | undefined;
    security?: string | undefined;
    unselectable?: "on" | "off" | undefined;

    inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;

    is?: string | undefined;
}

HTML 태그를 만든다고 했을 때 사용할 수 있는 모든 속성들이 들어가 있습니다.

🧐 근데 괜히 궁금증이 생깁니다.. 과연 T에는 무슨 타입이 들어갈 수 있을까?

타고타고 들어가면 결론에 도달합니다.

    interface DOMAttributes<T> {
        children?: ReactNode | undefined;
        dangerouslySetInnerHTML?: {
            // Should be InnerHTML['innerHTML'].
            // But unfortunately we're mixing renderer-specific type declarations.
            __html: string | TrustedHTML;
        } | undefined;

        // Clipboard Events
        onCopy?: ClipboardEventHandler<T> | undefined;
        onCopyCapture?: ClipboardEventHandler<T> | undefined;
        onCut?: ClipboardEventHandler<T> | undefined;
        onCutCapture?: ClipboardEventHandler<T> | undefined;
        onPaste?: ClipboardEventHandler<T> | undefined;
        onPasteCapture?: ClipboardEventHandler<T> | undefined;
    /// ...
    }

전부 optional인 타입 밭이 보입니다. 때문에 any타입과 같이 어떤 타입을 주더라도 다 받아주는 겁니다.

그럼 왜 💩 이라는 평가를 받는지 생각을 해봅니다.

  1. 타입 안정성의 부족 : UserType과 같이 정해져있지않은 모든 타입이 들어갈 수 있습니다. 해당 타입은 any 타입으로 어떤 값을 넣어도 다 받아줍니다.
  2. IDE의 자동 완성 기능 제한 : any 타입이다 보니 자동 완성을 제공하거나 정확한 타입 힌트를 제공하기가 어렵습니다.

제가 만약 커스텀 버튼 컴포넌트를 만든다고 해보겠습니다.

type PropsType = {
  	onClick: () => void
} & React.HTMLAttributes<HTMLButtonElement>

const MainButton = (props:PropsType) => {
  
	return (
    	<button {...props}>
      		{// ...}
      	</button>
    )
}

버튼의 타입을 본다면 any 타입이기 때문에 모든 타입의 속성들을 받을 수 있습니다. 하지만 불필요한 모든 HTML 속성들까지 담고 있는 것을 볼 수 있습니다. 이는 인터페이스를 명확히 하기 위해서는 불필요한 타입까지 보여지는게 아닐까? 라는 생각이 듭니다.

👍ComponentProps

두번째 타입인 ComponentProps 타입을 보겠습니다.

type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends
    JSXElementConstructor<infer P> ? P
    : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T]
    : {};
  1. T가 JSXElementConstructor<infer P> 타입인 경우: infer 키워드를 사용하여 제네릭 타입 P를 추론합니다.
    추론된 P 타입은 해당 컴포넌트의 속성 타입을 나타냅니다.
    예를 들어, T가 React.FC와 같은 함수 컴포넌트 생성자인 경우, P는 Props 타입이 됩니다.

    export interface ButtonProps
      extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
      asChild?: boolean
    }
    
    const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
      ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : "button"
        return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
      },
    )
    
    type ButtonType = React.ComponentProps<typeof Button>;
    // ButtonType: ButtonProps & React.RefAttributes<HTMLButtonElement>
  2. T가 JSX.IntrinsicElements의 키인 경우: JSX.IntrinsicElements는 React의 내장 요소(Element) 타입들을 포함하는 인터페이스입니다. T가 이러한 내장 요소의 키인 경우, JSX.IntrinsicElements[T]를 반환합니다. 예를 들어, T가 div인 경우, JSX.IntrinsicElements['div']div요소의 속성 타입을 반환한다.

    export interface ButtonProps
      extends React.ButtonHTMLAttributes<HTMLButtonElement>,
        VariantProps<typeof buttonVariants> {
      asChild?: boolean
    }
    
    const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
      ({ className, variant, size, asChild = false, ...props }, ref) => {
        const Comp = asChild ? Slot : "button"
        return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
      },
    )
    
    type ButtonType = React.ComponentProps<"div">
    // ButtonType: React.ClassAttributes<HTMLDivElement> & React.HTMLAttributes<HTMLDivElement>

1을 활용하는 방법은 다음과 같습니다. 컴포넌트 만으로 Props의 타입을 가져올 수 있다.

type Props = {
  textList: string[]
  duration: number
}

export default function SlotMachine({ textList, duration }: Props) {  
 // ...
}
  

type SlotType = ComponentProps<typeof SlotMachine>

다음과 같이 작성이 되어있는 컴포넌트의 Props 타입을 가져오기 위해서 아래와 같이 작성하면 타입을 가져올 수 있는 것입니다.

type Props = {
  user_name: string
  user_gender: string
  user_age: number
  user_phone: string
  user_date: string
  user_hobby: string
}
  
export function UserList(props: PropsWithChildren<Props>) {
	return (
  		<ul>
        	{// ...}  
  		</ul>
  	)  
}

// UserList 컴포넌트의 타입을 재사용 할 수 있습니다.
export function OneUser(props:ComponentProps<typeof UserList>){
  return (
  	<li>
  	{// ...}
  	</li>
  )
}

이전에는 ComponentProps타입을 모를 경우에는 매번 타입도 같이 내보내주면서 했었지만 이제 컴포넌트 만으로도 타입을 사용 가능한 것을 알았습니다.

2를 활용하는 방법은 다음과 같습니다.

ComponentProps로 속성을 가져올 경우 HTML에 있는 기본 속성들만 보여주는 것이 아닌 button에는 className, style, onClick 등과 같은 일반적인 HTML 속성들만 보여줍니다.

ComponentProps<'button'>에는 button에만 사용이 되는 속성이 들어간다고 생각하면 됩니다. 반면 이전 HtmlAttributes는 해당 속성들도 포함하지만 HTML 요소에 대한 모든 attributes의 자동완성을 포함하기 때문에 불필요한 자동완성이 많습니다. 때문에 컴포넌트 간의 인터페이스를 명확히 하고, 다른 개발자와 협업할 때는 ComponentProps<T>를 사용할 경우 불필요한 attributes를 줄일 수 있어 효율적으로 사용할 수 있습니다.

🎇 ComponentPropsWithRef, ComponentPropsWithoutRef

ComponentPropsWithoutRef는 이름에서부터 알 수 있듯이 Ref속성 만을 받지 않는다고 생각을 하면 됩니다.

하지만 사실 타입의 이름에서 알 수 있듯이 Ref를 받지 않는다고 하지만 실제로는 ComponentProps와 똑같이 작동합니다.

협력을 할 경우 구성 요소의 참조가 전달되는지 여부를 명시적으로 지정하는 것을 선호할 수 있습니다

때문에 ComponentPropsWithoutRefPerfect하다고 얘기를 한 것 같습니다.

만약 ref와 같이 사용하고 싶다고 한다면 ComponentPropsWithRef를 사용하면 됩니다. 사용법은 ComponentProps와 동일합니다.

요약

불필요한 props까지 넘기지 말고 실제로 사용하는 props만 추출해서 넘기자. ComponentPropsWithoutRef는 타입 이름에서 알 수 있다시피 구성 요소의 참조가 없다는 것까지 알려주기에 좋다고 하는 것이다.

참고자료

profile
0에서 시작해, 나만의 길을 만들어가는 개발자.

0개의 댓글

관련 채용 정보