
Coworkers 프로젝트에서 공통적으로 사용되는 Input Form을 제작할 때,
폼 관리 라이브러리 중 react-hook-form을 선택하여 적용하였습니다.
공통적으로 사용될 Input Form을 제작할 때, 폼 상태 관리 방법을 고민하며 다음과 같은 대안과 react-hook-form의 장점을 비교하였습니다.
✅ 사용법이 직관적이고 간단함
❌ 폼 필드가 많아질수록 불필요한 렌더링 증가
❌ 모든 입력값을 개별 useState로 관리해야 하므로 코드가 길어지고 복잡해짐
✅ 불필요한 리렌더링 없이 성능 최적화 가능
✅ 기본적으로 useState를 사용하지 않음 → 폼 상태가 DOM을 통해 관리됨
✅ register 함수로 손쉽게 폼 요소 연결 가능
✅ errors 객체를 통해 유효성 검증 에러를 직관적으로 확인 가능
❌ 초기 학습이 필요할 수 있음
→ react-hook-form은 가벼우면서도 강력한 성능 최적화가 가능하므로
→ Coworkers 프로젝트의 Input Form에 적합한 선택지라고 판단하여 적용하였습니다.
제작한 Input Form의 핵심 기능과 코드에서 주목할 부분을 정리하였습니다.
register를 통해 입력값을 react-hook-form에 등록
const { register } = useFormContext();
<input
{...register(name, validationRules)} // 입력 필드 등록
type={inputType}
placeholder={placeholder}
/>
✅ 별도의 useState 없이도 상태가 자동으로 관리됨
✅ 유효성 검증(validationRules)을 적용할 수 있음
validationRules?: RegisterOptions;
<input
{...register(name, {
required: '이 필드는 필수 입력 항목입니다.',
maxLength: { value: 30, message: '최대 30자까지 입력 가능합니다.' }
})}
/>
✅ required, maxLength 등 다양한 검증 규칙 적용 가능
✅ errors[name]?.message를 통해 에러 메시지를 쉽게 표시
const [isVisibleToggle, setIsVisibleToggle] = useState(false);
<button type="button" onClick={handleToggleClick}>
{isVisibleToggle ? <IconVisibility /> : <IconInVisibility />}
</button>
✅ useState를 활용하여 비밀번호 보이기/숨기기 기능 제공
{errors[name] && (
<span className="text-sm text-status-danger">
{errors[name]?.message as string}
</span>
)}
✅ 입력값 검증 실패 시 즉시 사용자에게 피드백 제공
import { MouseEvent, MouseEventHandler, ReactNode, useState } from 'react';
import { useFormContext, RegisterOptions } from 'react-hook-form';
import IconVisibility from '@/app/components/icons/IconVisibility';
import IconInVisibility from '@/app/components/icons/IconInVisibility';
type InputProps = {
name: string; // 필드 이름 (폼 데이터의 키)
title?: string; // 라벨 제목
type: string; // input 타입 (예: text, password 등)
placeholder: string; // 플레이스홀더
autoComplete: string; // 자동 완성 옵션
validationRules?: RegisterOptions; // react-hook-form 유효성 검증 규칙
backgroundColor?: string; // 입력 필드 배경색
customButton?: ReactNode; // 추가 버튼 컴포넌트
className?: string; // 스타일 추가
onClick?: MouseEventHandler<HTMLInputElement>; // 클릭시 이벤트
value?: string | undefined; // input에 들어올 value 값
defaultValue?: string;
readOnly?: boolean; // 직접 값을 입력할 수 없도록(날짜,시간 선택시)
};
function Input({
name,
title,
type = 'text',
placeholder,
autoComplete,
validationRules,
backgroundColor = 'bg-background-secondary',
customButton,
className,
onClick,
value,
defaultValue,
readOnly,
}: InputProps) {
const [isVisibleToggle, setIsVisibleToggle] = useState(false);
const {
register,
formState: { errors },
} = useFormContext();
const isPassword = type === 'password';
const inputType = isPassword ? (isVisibleToggle ? 'text' : 'password') : type;
const handleToggleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setIsVisibleToggle(!isVisibleToggle);
};
const inputBorderClass = errors[name]
? 'border-status-danger' // 에러시 border 색상
: 'border-[#F8FAFC1A]'; // 기본 border 색상
return (
<div className="flex flex-col gap-3">
<label className="text-base font-medium text-text-primary" htmlFor={name}>
{title}
</label>
<div className="relative">
<input
className={`placeholder:text-text-danger h-full w-full rounded-xl border px-4 py-[0.85rem] text-text-primary placeholder:text-lg focus:border-interaction-focus focus:outline-none ${backgroundColor} ${inputBorderClass} ${className}`}
{...register(name, validationRules)}
type={inputType}
id={name}
placeholder={placeholder}
autoComplete={autoComplete}
onClick={onClick}
value={value}
defaultValue={defaultValue}
readOnly={readOnly}
/>
{isPassword && customButton && (
<div className="absolute right-4 top-2 z-20">{customButton}</div>
)}
{isPassword && !customButton && (
<button
className="absolute right-4 top-3 z-10"
type="button"
onClick={handleToggleClick}
>
{isVisibleToggle ? <IconVisibility /> : <IconInVisibility />}
</button>
)}
</div>
{errors[name] && (
<span className="text-sm text-status-danger">
{errors[name]?.message as string}
</span>
)}
</div>
);
}
export default Input;