input 또는 textarea를 포함하는 InputLabel컴포넌트 만들기

고성인·2025년 2월 17일

React

목록 보기
7/17

연습 프로젝트를 진행하면서 다음과 같은 구조로 이루어져 있는 사용자 입력 태그를 구현해야 했다.

<p>
  <label>Title</label>
  <input type="text" />
</p>
<p>
  <label>Description</label>
  <textarea />
</p>
<p>
  <label>DueDate</label>
  <input type="date" />
</p>

p태그 안에 label태그와 입력태그가 존재하는 구조이고, input인지 textarea인지의 차이만 존재하며 스타일은 모두 같은 상황이였다.

그렇다면 이것을 하나의 컴포넌트로 만들면 재사용할 수 있기때문에 구현해보았다.

typescript를 사용중이기에 해당 컴포넌트의 타입을 제어하는 것이 조금 까다로운 작업이였다.

전체 코드

전체 코드는 다음과 같다.

import { InputHTMLAttributes, Ref, TextareaHTMLAttributes } from "react";

interface InputLabelProps<T extends "input" | "textarea"> {
  title: string;
  InputType?: T;
  ref?: Ref<HTMLInputElement | HTMLTextAreaElement>;
}

type InputElementProps<T extends "input" | "textarea"> = T extends "input"
  ? InputHTMLAttributes<HTMLInputElement>
  : TextareaHTMLAttributes<HTMLTextAreaElement>;

const InputLabel = <T extends "input" | "textarea">({
  title,
  InputType,
  ...props
}: InputLabelProps<T> & InputElementProps<T>) => {
  const resolvedInputType = InputType ?? "input";
  const classes =
    "w-full p-1 border-b-2 rounded-sm border-stone-300 bg-stone-200 text-stone-600 focus:outline-none focus:border-stone-600";

  return (
    <p className="flex flex-col gap-1 my-4">
      <label className="text-sm font-bold uppercase text-stone-500" htmlFor={title}>
        {title}
      </label>
      {resolvedInputType === "input" && (
        <input
          className={classes}
          id={title}
          type="text"
          {...(props as InputHTMLAttributes<HTMLInputElement>)}
        />
      )}
      {resolvedInputType === "textarea" && (
        <textarea
          className={classes}
          id={title}
          {...(props as TextareaHTMLAttributes<HTMLTextAreaElement>)}
        />
      )}
    </p>
  );
};

export default InputLabel;

세부적으로 보자면 타입 선언 부분과 input, textarea를 조건적으로 렌더링 하는 부분으로 나눌 수 있을것같다.

타입 선언

interface InputLabelProps<T extends "input" | "textarea"> {
  title: string;
  InputType?: T;
  ref?: Ref<HTMLInputElement | HTMLTextAreaElement>;
}

type InputElementProps<T extends "input" | "textarea"> = T extends "input"
  ? InputHTMLAttributes<HTMLInputElement>
  : TextareaHTMLAttributes<HTMLTextAreaElement>;

먼저 타입을 선언하는 부분을 살펴보면 InputLabelProps타입에서 InputType을 제네릭을 통해 "input"또는 "textarea"가 올 수 있도록 해주었다.
InputElementProps에서도 제네릭을 통해 "input"또는 "textarea"를 가질 수 있게 하였으며, 만약 현재 T가 "input"일 경우 input태그의 props들에대한 타입 지정을 위해 InputHTMLAttributes<HTMLInputElement>, 그 밖의 경우 TextareaHTMLAttributes<HTMLTextAreaElement>로 설정해주었다.

이제 실제 컴포넌트를 정의하는 함수 부분에서 다음과 같이 작성하여 기본 제네릭 값을 설정하였다.

const InputLabel = <T extends "input" | "textarea">({
  title,
  InputType,
  ...props
}: InputLabelProps<T> & InputElementProps<T>) => {
  const resolvedInputType = InputType ?? "input";
  ...
  ...
}

조건적 렌더링

위에서 설정한 제네릭 타입에 따라 input태그와 textarea태그가 조건적으로 렌더링하게 설정하였다.

{resolvedInputType === "input" && (
  <input
    className={classes}
    id={title}
    type="text"
    {...(props as InputHTMLAttributes<HTMLInputElement>)}
  />
)}
{resolvedInputType === "textarea" && (
  <textarea
    className={classes}
    id={title}
    {...(props as TextareaHTMLAttributes<HTMLTextAreaElement>)}
  />
)}

구조분해할당을 통해 넘어온 props를 정상적으로 전달하기 위해 타입상적으로 전달하기 위해 타입캐스팅을 사용하여 알맞는 타입으로 변경해주었다.

컴포넌트 사용

해당 컴포넌트는 다음과 같이 사용할 수 있다.

<InputLabel title="Title" type="text" ref={titleRef} />
<InputLabel title="Description" InputType={"textarea"} ref={descriptionRef} />
<InputLabel title="Due Date" type="date" ref={dueDateRef} />

InputType을 지정해주지 않으면 기본적으로 input태그를 사용하며, textarea로 변경할 경우 InputType을 textarea로 설정하여 사용한다.
각 설정에 따라 input태그에 올 수 있는 속성과 textarea에 올 수 있는 속성을 자동완성 기능을 제공받을 수 있다.

0개의 댓글