
📅 2025-12-10
➡️ React Hook Form, Zod에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
제어 컴포넌트
setState 호출 → 리렌더링 발생import { useState } from "react";
function ControlledInput() {
const [value, setValue] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
return (
<div>
<input value={value} onChange={handleChange} />
<p>현재 값: {value}</p>
</div>
);
}
비제어 컴포넌트
import { useRef } from "react";
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleSubmit = () => {
if (!inputRef.current) return;
alert(`입력한 값: ${inputRef.current.value}`);
};
return (
<div>
<input ref={inputRef} />
<button type="button" onClick={handleSubmit}>
값 확인
</button>
</div>
);
}
비교
| 기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
|---|---|---|
| 일회성 정보 검색 (예: 제출) | O | O |
| 제출 시 값 검증 | O | O |
| 실시간으로 필드 값의 유효성 검사 | O | X |
| 조건부로 제출 버튼 비활성화 (disabled) | O | X |
| 실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
| 동적 입력 | O | X |
React Hook Form - performant, flexible and extensible form library
특징
설치
yarn add react-hook-form
#또는
npm install react-hook-form
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<form>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</form>
);import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
<input {...register("password")} />
<button type="submit">로그인</button>
</form>
);
}useFrom 옵션const {
register,
handleSubmit,
formState: { errors, isValid, isDirty, isSubmitting },
watch,
reset,
control,
} = useForm({
mode: "onChange",
defaultValues: {
email: "",
password: "",
},
});mode
| mode | 설명 |
|---|---|
| onSubmit (기본값) | 제출할 때만 검사 |
| onChange | 입력이 변경될 때마다 검사 |
| onBlur | 포커스 벗어날 때 검사 |
| onTouched | 한 번이라도 건드린 후 검사 |
| all | 모든 상황에서 검사 |
defaultValues
register 옵션 (Validation)<input
{...register("email", {
required: "이메일은 필수입니다.",
minLength: { value: 3, message: "3자 이상 입력해주세요" },
maxLength: { value: 30, message: "30자 이하로 입력해주세요" },
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "이메일 형식이 아닙니다" },
validate: (value) => value !== "test" || "test는 사용할 수 없습니다",
})}
/>required
required: "이 필드는 필수입니다"
minLength / maxLength
minLength: { value: 3, message: "3자 이상 입력" }
maxLength: { value: 20, message: "20자 이하로 입력" }
pattern
pattern: {
value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
message: "이메일 형식이 올바르지 않습니다.",
}
validate
true 반환 → 통과false / string 반환 → 실패(문자열이면 에러 메시지)validate: (v) => v === "admin" ? "admin은 불가" : true
formState 속성
const {
formState: { errors, isDirty, isValid, isSubmitting },
} = useForm({ mode: "onChange" });
errors
isDirty
isValid
isSubmitting
import { useForm } from "react-hook-form";
type LoginFormValues = {
email: string;
password: string;
};
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginFormValues>();
const onSubmit = (data: LoginFormValues) => {
console.log("폼 데이터:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 이메일 */}
<div>
<input
type="email"
placeholder="이메일"
{...register("email", { required: "이메일은 필수입니다." })}
/>
{errors.email && <p>{errors.email.message}</p>}
</div>
{/* 비밀번호 */}
<div>
<input
type="password"
placeholder="비밀번호"
{...register("password", {
required: "비밀번호는 필수입니다.",
minLength: {
value: 6,
message: "비밀번호는 최소 6자 이상이어야 합니다.",
},
})}
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">로그인</button>
</form>
);
}
export default LoginForm;
설치
npm install zod @hookform/resolvers
# 또는
yarn add zod @hookform/resolvers
Zod 스키마 정의
const loginSchema = z.object({
email: z
.string()
.min(1, "이메일을 입력해주세요.")
.email("이메일 형식이 올바르지 않습니다."),
password: z
.string()
.min(8, "비밀번호는 최소 8자 이상이어야 합니다."),
});
loginSchema: 폼 필드를 Zod로 정의하고, 동시에 검증 규칙과 메시지를 함께 선언스키마 기반 타입 생성
type LoginFormValues = z.infer<typeof loginSchema>;
z.infer<typeof loginSchema>: 스키마로부터 폼 값 타입을 자동으로 추출React Hook Form과 연동
const { register, handleSubmit, formState: { errors } } =
useForm<LoginFormValues>({
resolver: zodResolver(loginSchema),
});
resolver: zodResolver(loginSchema): React Hook Form이 Zod 검증을 사용하도록 설정errors.email?.message: Zod 메시지를 그대로 UI에서 사용예시
const onSubmit = async (values: SignUpFormValues) => {
const isDuplicated = await checkEmailDuplicated(values.email);
if (isDuplicated) {
// 서버 응답 기반으로 RHF 에러 수동 등록
setError("email", {
type: "server",
message: "이미 사용 중인 이메일입니다.",
});
return;
}
console.log("회원가입 진행:", values);
};
예시
const passwordSchema = z.object({
password: z
.string()
.min(8, "비밀번호는 8자 이상이어야 합니다.")
.regex(/[0-9]/, "숫자를 최소 1개 포함해야 합니다.")
.regex(/[A-Z]/, "대문자를 최소 1개 포함해야 합니다."),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="password"
placeholder="새 비밀번호"
{...register("password")}
/>
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">저장</button>
</form>
);