연습 프로젝트를 진행하면서 다음과 같은 구조로 이루어져 있는 사용자 입력 태그를 구현해야 했다.
<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에 올 수 있는 속성을 자동완성 기능을 제공받을 수 있다.