기본적인 input 과 button 컴포넌트는 아름답지 않습니다. 그렇기 때문에 거의 대부분의 프로젝트에서 커스텀해서 사용하고 있는데요, 이러한 커스텀 input 컴포넌트는 어떤 Type을 사용해야 input 컴포넌트의 기본적인 속성을 사용할 수 있을까요?

Props Type?

우리는 예쁘게 디자인된 Input 컴포넌트를 만들어보겠습니다. 아래와 같이 간단하게 구현할 수 있습니다.
TypeScript
// Input.tsx
interface Props extends React.HTMLAttributes<HTMLInputElement> {}

const Input = (props: Props) => {
  return <input {...//이런저런 스타일} {...props}/>
}
이 타입을 해석하면 "HTMLAttributes 중 HTMLInputElement의 타입을 확장한다"로 이해할 수 있습니다. 정확한 해석은 이어서 설명하겠습니다. 우선, 이렇게 만든 <Input /> 컴포넌트를 비밀번호 입력 필드에 적용해보겠습니다.

  • 비밀번호 요구사항
    • 비밀번호 입력 시 '*'로 표시되어야 한다.
    • 비밀번호는 최소 6자리 이상, 12자리 이하로 입력해야 한다.

위 요구사항에 따라 우리의 Input 컴포넌트를 적용해봅시다.
TypeScript
const Login = () => {
  const [password, setPassword] = useState('')
  // 로직
  return (
    // 기타 마크업
    <Input type="password" minLength={6} maxLength={12} value={password} onChange={e => setPassword(e.target.value)} />
  )
}
위와 같이 간단히 사용할 수 있습니다. 하지만 현실에서는 에러가 발생하곤 합니다.
Type '{ type: string; minLength: number; maxLength: number; value: string; onChange: (e: FormEvent<HTMLInputElement>) => void; }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'type' does not exist on type 'IntrinsicAttributes & Props'.
"type이 없다"는 에러가 발생하는 이유는 무엇일까요? 이를 알아보기 위해 HTMLAttributes 타입을 살펴봅시다.
TypeScript
interface HTMLAttributes<T> extends AriaAttributes, DOMAttributes<T> {
        // React-specific Attributes
        defaultChecked?: boolean | undefined;
        defaultValue?: string | number | readonly string[] | undefined;
        suppressContentEditableWarning?: boolean | undefined;
        suppressHydrationWarning?: boolean | undefined;

        // Standard HTML Attributes
        accessKey?: string | undefined;
        autoFocus?: boolean | undefined;
        className?: string | undefined;
        contentEditable?: Booleanish | "inherit" | "plaintext-only" | undefined;
        contextMenu?: string | undefined;
        dir?: string | undefined;
        draggable?: Booleanish | undefined;
        hidden?: boolean | undefined;
        id?: string | undefined;
        lang?: string | undefined;
        nonce?: string | undefined;
        slot?: string | undefined;
        spellCheck?: Booleanish | undefined;
        style?: CSSProperties | undefined;
        tabIndex?: number | undefined;
        title?: string | undefined;
        translate?: "yes" | "no" | undefined;

        // Unknown
        radioGroup?: string | undefined; // <command>, <menuitem>

        // WAI-ARIA
        role?: AriaRole | undefined;

        // RDFa Attributes
        about?: string | undefined;
        content?: string | undefined;
        datatype?: string | undefined;
        inlist?: any;
        prefix?: string | undefined;
        property?: string | undefined;
        rel?: string | undefined;
        resource?: string | undefined;
        rev?: string | undefined;
        typeof?: string | undefined;
        vocab?: string | undefined;

        // Non-standard Attributes
        autoCapitalize?: string | undefined;
        autoCorrect?: string | undefined;
        autoSave?: string | undefined;
        color?: string | undefined;
        itemProp?: string | undefined;
        itemScope?: boolean | undefined;
        itemType?: string | undefined;
        itemID?: string | undefined;
        itemRef?: string | undefined;
        results?: number | undefined;
        security?: string | undefined;
        unselectable?: "on" | "off" | undefined;

        // Living Standard
        /**
         * Hints at the type of data that might be entered by the user while editing the element or its contents
         * @see https://html.spec.whatwg.org/multipage/interaction.html#input-modalities:-the-inputmode-attribute
         */
        inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
        /**
         * Specify that a standard HTML element should behave like a defined custom built-in element
         * @see https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
         */
        is?: string | undefined;
    }
결과적으로, HTMLAttributes<HTMLInputElement>는 실제로 'input' 요소에서 사용되는 type, maxLength 등의 속성을 포함하고 있지 않습니다. 그렇다면 어떤 타입을 사용해야 할까요?

ComponentProps

TypeScript
// Input.tsx
interface Props extends React.ComponentProps<'input'> {}

const Input = (props: Props) => {
  return <input {...//이런저런 스타일} {...props}/>
}
ComponentProps<'input'>를 사용하면, 앞서 언급된 에러를 해결할 수 있습니다. ComponentPropsinput 요소가 받을 수 있는 모든 속성의 타입을 포함합니다. 그러므로, ComponentProps<'input'>를 사용하면, type, maxLength 등의 속성을 문제없이 사용할 수 있습니다. 그렇다면 ComponentProps는 어떻게 이 문제를 해결할 수 있는 것일까요?
 
간단하게 동작원리를 볼 필요가 있습니다.
ComponentProps는 React의 내장 유틸리티 타입 중 하나로, 주어진 컴포넌트 타입의 props 타입을 추출합니다. 여기서 'input'은 HTML input 요소를 나타내며, ComponentProps<'input'>input 요소가 받을 수 있는 모든 속성의 타입을 나타내는 구조입니다.
그렇기때문에 ComponentProps는 커스텀 컴포넌트가 특정 HTML 요소나 다른 컴포넌트와 동일한 속성을 받도록 하고 싶을 때 사용합니다.
예를 들어, input 요소와 같은 속성을 가진 커스텀 InputComponent를 만들 때 ComponentProps<'input'>를 사용할 수 있습니다. 위 예시처럼 말이죠!

HTMLAttributes 는 언제 사용할까?

그렇다면 HTMLAttribute 는 언제 사용할까요? HTMLAttribute 는 React의 타입 정의 중 하나로, 모든 표준 HTML 요소에 대한 일반적인 속성 집합을 제공합니다. HTMLAttributes<HTMLInputElement>input HTML 요소에 적용될 수 있는 속성들을 나타내는 타입입니다. 그렇기 때문에 이 타입은 input 요소의 속성만을 다루는 데에 집중됩니다. 예를 들어, input 요소를 확장하는 커스텀 컴포넌트에서 기본 HTML input 요소의 속성을 받기 위해 사용될 수 있습니다.
추가로, HTMLAttributes 말고 AllHTMLAttributes<HTMLInputElement> 를 사용하는 방법도 있답니다!
어떤것이 옳다는 없지만 위 예시처럼 input 요소와 같은 속성을 가진 커스텀 컴포넌트를 만들때엔 ComponentProps를 사용하는것이 추가 타입을 선언하지 않고 간단하게 사용할 수 있는 방법일 것입니다.