보통은 일반적으로 form을 작성한다고 해보자.
각각의 인풋마다 state를 만들어 관리해야 하고, 핸들러도 작성해야 하는데 특히 밸리데이션이 엄청나게 많아진다. ^^...
import { useState } from "react";
export default function Forms() {
//input의 value 상태
//보통은 각각의 인풋마다 state를 만들어 관리해야 한다.
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [formErrors, setFormErrors] = useState("");
//input 이벤트
const onUsernameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
setUsername(event.currentTarget.value);
};
const onEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
setEmail(event.currentTarget.value);
};
const onPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
setPassword(event.currentTarget.value);
};
//form 이벤트
const onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
//밸리데이션
if (username === "" || email === "" || password === "") {
setFormErrors("모든 항목을 채워주세요.");
}
if (username.length < 5) {
setFormErrors("username은 5글자 이상이어야 합니다.");
}
if (!email.includes("@")) {
setFormErrors("이메일에는 @이 포함되어야 합니다.");
}
//등등...엄청 많은 에러 발생 가능성 ^^
//서밋 핸들러가 엄청 커지게 된다.💥
console.log(username, email, password);
};
return (
<form className="flex flex-col" onSubmit={onSubmit}>
<input
required
type="text"
placeholder="Username"
value={username}
onChange={onUsernameChange}
minLength={5}
/>
<input
required
type="email"
placeholder="Email"
value={email}
onChange={onEmailChange}
/>
<input
required
type="password"
placeholder="Password"
value={password}
onChange={onPasswordChange}
/>
<input type="submit" value="가입하기" />
</form>
);
}
//하지만 이렇게 해놓으면 브라우저의 콘솔에서 사용자가 맘대로 required 없애고 type도 바꾸고 해버리면 브라우저가 그냥 허용해버려서 밸리데이션 무용지물 된다.
//따라서 밸리데이션을 따로 작성해줘야 한다.
const { register, watch } = useForm();
register: (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })
이 메서드를 사용하면 input을 등록하거나 엘리먼트를 선택하고 React Hook Form에 유효성 검사 규칙을 적용할 수 있다. 유효성 검사 규칙은 모두 HTML 표준을 기반으로 하며 사용자 지정 유효성 검사 방법도 허용한다.
import { useForm } from "react-hook-form";
const { register, handleSubmit } = useForm();
< input {...register("firstName", { required: true })} placeholder="First name" />
console.log(watch());
import { useForm } from "react-hook-form";
export default function Forms() {
//1. 모든 건 useForm Hook에서 나온다.
const { register, watch } = useForm();
//2. input을 state에 등록하기 위해서 useForm이 반환하는 register함수를 사용한다.
//register 함수: input을 state와 연결시켜주는 역할
//3. form을 보게해주는 useForm이 반환하는 watch함수 사용한다.
//watch함수를 호출하면, form이 register함수에 입력한 이름을 key로 가지는 객체가 되는데 그것을 볼 수 있다.
console.log(watch());
return (
<form className="flex flex-col">
<input
{...register("username")}
type="text"
placeholder="Username"
/>
<input {...register("email")}
type="email"
placeholder="Email" />
<input
{...register("password")}
type="password"
placeholder="Password"
/>
<input type="submit" value="가입하기" />
</form>
);
}
React Hook Form은 양식 검증을 위한 기존 HTML 표준에 맞춰 form 검증을 쉽게 만든다.
const { register, handleSubmit } = useForm();
<form onSubmit={handleSubmit(onValid, onInvalid)}>
onValid(): 유효성 검증에 성공했을 때 호출되는 메서드이다.onInvalid(): 유효성 검증에 실패했을 때 호출되는 메서드이다.예를들면 input의 속성 값으로 준 required의 경우, 유저가 임의로 브라우저의 콘솔에서 검증 값을 삭제해 버리면 무용지물이 되어 버린다. 이런 변수 상황에서도 유효성 검증이 제대로 이루어질 수 있게 하려면, JS를 사용하여 validate을 작성해야 한다.
그렇기 때문에 수 많은 validate로 인해 onSubmit 핸들러는 검증을 하느라 커질 수 밖에 없다.
이 두 번째 인자는 아주 강력하다.
각각의 필드마다 register()의 두 번째 인자에 validationRule 규칙을 따로 정해줄 수 있다.
때문에 기존처럼 onSubmit 함수에 validation을 몰빵으로 넣어서 커질 일이 없다.
<input
{...register("username", { required: true })}
type="text"
placeholder="Username"
/>
import { useForm } from "react-hook-form";
export default function Forms() {
const { register, handleSubmit } = useForm();
const onValid = () => {
console.log("✅ 유효성 검증 완료!");
};
const onInvalid = () => {
console.log("❌ 유효성 검증 실패!");
};
return (
<form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", { required: true })}
type="text"
placeholder="Username"
/>
<input
{...register("email", { required: true })}
type="email"
placeholder="Email"
/>
<input
{...register("password", { required: true })}
type="password"
placeholder="Password"
/>
<input type="submit" value="가입하기" />
</form>
);
}
📝 List of validation rules supported:
- required
- min
- max
- minLength
- maxLength
- pattern: 정규식으로 입력값 필드를 검증할 때 사용
- validate
만약에 매우 특정한 규칙들이 필요한 상황이라면 여기에 있는 규칙들만으로는 필요한 유연성을 다 확보할 수는 없다. 다행히도 validate에 다 있으니 걱정하지 않아도 된다.
타입스크립트를 사용하고 있기 때문에 onValid가 받는 인자인 data의 타입을 지정해 줘야 한다.
interface로 LoginForm 타입을 만들고, useForm의 타입에 LoginForm 타입을 넣어준다.import { FieldError, useForm } from "react-hook-form";
interface LoginForm {
username: string;
email: string;
password: string;
}
export default function Forms() {
const { register, handleSubmit } = useForm<LoginForm>();
const onValid = (data: LoginForm) => {
console.log(data);
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
<form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", { required: true })}
type="text"
placeholder="Username"
/>
<input
{...register("email", { required: true })}
type="email"
placeholder="Email"
/>
<input
{...register("password", { required: true })}
type="password"
placeholder="Password"
/>
<input type="submit" value="가입하기" />
</form>
);
}

const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};

{ required: true }를 보내는 대신, { required: "에러 메시지" }를 보낼 수 있다.<input
{...register("email", { required: "Email is required" })}
type="email"
placeholder="Email"
/>

<input
{...register("username", {
required: "Username is required",
minLength: 5,
})}
type="text"
placeholder="Username"
/>

<input
{...register("username", {
required: "Username is required",
minLength: {
message: "Username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
/>
validate 객체는 유효성을 검사할 인수로 콜백 함수를 전달하거나 콜백 함수의 개체를 전달하여 모든 유효성을 검사할 수 있다.
<input
{...register("email", {
required: "Email is required",
validate: {
notGmail: (value) => !value.includes("@gmail.com"),
},
})}
type="email"
placeholder="Email"
/>
이메일 input에 @gmail.com을 입력하면 에러가 뜨고, 에러 타입이 notGmail로 뜬다.

여기에도 에러 message를 넣어줄 수 있다.
notGmail에 조건에 따른 반환값을 넣어주면 된다.
<input
{...register("email", {
required: "Email is required",
validate: {
notGmail: (value) =>
!value.includes("@gmail.com") ? "" : "Gmail is not allowed",
//또는 || (OR) 써서 같은 효과
//!value.includes("@gmail.com") || "Gmail is not allowed",
},
})}
type="email"
placeholder="Email"
/>

React Hook Form은 form의 오류를 표시하는 오류 객체를 제공한다. 오류 유형은 주어진 유효성 검사 제약 조건을 반환한다.
현재 콘솔에서 에러를 보고 있는데, 에러를 화면의 input에서 보여줘 보자.
FormState는 errors를 포함해 많은 것을 제공하는 객체이다.
//...
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors }, // formState 객체에 있는 errors를 사용해보자.
} = useForm<LoginForm>();
const onValid = (data: LoginForm) => {
console.log(data);
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
//onInvalid 함수 밖에서도 errors 사용할 수 있게됨
console.log(errors);
return (
<form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "Username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
/>
{errors.username?.message} //errors.username이 있으면 message 보여주기
<input
{...register("email", {
required: "Email is required",
validate: {
notGmail: (value) =>
!value.includes("@gmail.com") || "Gmail is not allowed",
},
})}
type="email"
placeholder="Email"
/>
{errors.email?.message}
<input
{...register("password", { required: "Password is required" })}
type="password"
placeholder="Password"
/>
{errors.password?.message}
<input type="submit" value="가입하기" />
</form>
);
}
mode라고 부른다.
mode의 옵션
allonBlur: 작성하는 input을 벗어 날 때 validation 일어남
- 입력 후 벗어났을 때
onChange: input이 변하면 validation 일어남
- 입력 하나하나에 매번 validation
- 그래서 주로 username이 이미 사용중인지 확인할 때 사용하기에 유용함
onSubmit: (*기본값) submit 버튼을 누를 때 validation 일어남onTouched
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({ mode: "" });
유효성 검증에 실패하면 input이 빨간 테두리가 생기게 해보자.
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "Username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
className={`${Boolean(errors.username) ? "border-red-500" : ""}`}
/>
setError()
이 함수를 사용하면 하나 이상의 오류를 수동으로 설정할 수 있다.
예를 들어, 백엔드를 POST로 fetch한 후, 그에 대한 응답을 받았는데 DB가 오프라인이어서 에러가 난 상황이라고 해보자.
setErrors()를 사용할 수 있다.setError()는 특정 필드에 관한 에러가 아니더라도 에러를 설정할 수 있게 해준다.import { FieldErrors, useForm } from "react-hook-form";
interface LoginForm {
username: string;
email: string;
password: string;
errors?: string; //전역 errors 추가
}
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors },
setError,
} = useForm<LoginForm>({ mode: "onBlur" });
const onValid = (data: LoginForm) => {
console.log(data);
//DB 오프라인 문제로 에러 발생한 상황
setError("errors", { message: "Backend is offline sorry." });
//어떤 것을 타입의 에러를 설정하고 싶은지 선택하면 됨
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
//...
{errors.errors?.message} //form 마지막에 에러 메세지 표시하기
</form>
이번엔 onValid함수에 에러가 없는 상황을 가정해보자.
예를 들어 데이터를 백엔드로 보냈는데 내가 validation을 하지 않아서 username이 이미 존재한다고 메시지가 왔다고 해보자.
setErrors()를 사용하면 된다.const onValid = (data: LoginForm) => {
console.log(data);
//백엔드에서 이미 이 username을 가진 유저가 있다는 응답 받은 경우
setError("username", { //필드를 설정하고
message: "이 username이 이미 존재합니다." //메세지 적으면 된다.
});
};
{errors,username?.message}로 에러 메시지를 유저에게 표시하면 된다.reset()
전체 form state 또는 form state의 일부를 리셋한다.
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<LoginForm>({ mode: "onBlur" });
const onValid = (data: LoginForm) => {
console.log(data);
//form 제출 후, onValid 실행 되면 전체 input 초기화
reset();
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
//...
resetField()
개별 field state를 재설정한다.
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors },
resetField,
} = useForm<LoginForm>({ mode: "onBlur" });
const onValid = (data: LoginForm) => {
console.log(data);
//form 제출 후, onValid 실행 되면 password input만 초기화 하기
resetField("password");
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
//...
📝 제어된 input 통합(Integrating Controlled Inputs)
📝 기존 양식 통합(Integrating an existing form)
기존 form을 통합하는 것은 간단해야 한다.
컴포넌트의 ref를 등록하고 관련 props를 input에 할당하면 된다.
import { FieldErrors, useForm } from "react-hook-form";
interface LoginForm {
username: string;
email: string;
password: string;
errors?: string;
}
export default function Forms() {
const {
register,
handleSubmit,
formState: { errors },
setError,
reset,
} = useForm<LoginForm>({ mode: "onBlur" });
const onValid = (data: LoginForm) => {
console.log(data);
// setError("username", {
// //필드를 설정하고
// message: "이 username이 이미 존재합니다.", //메세지 적으면 된다.
// });
//form 제출 후, onValid 실행 되면 전체 input 초기화
reset();
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
// console.log(errors);
return (
<form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "Username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
className={`${Boolean(errors.username) ? "border-red-500" : ""}`}
/>
<span>{errors.username?.message}</span>
<input
{...register("email", {
required: "Email is required",
validate: {
notGmail: (value) =>
!value.includes("@gmail.com") || "Gmail is not allowed",
},
})}
type="email"
placeholder="Email"
className={`${Boolean(errors.email) ? "border-red-500" : ""}`}
/>
<span>{errors.email?.message}</span>
<input
{...register("password", {
required: "Password is required",
minLength: {
message: "password should be longer than 8 chars.",
value: 8,
},
})}
type="password"
placeholder="Password"
className={`${Boolean(errors.password) ? "border-red-500" : ""}`}
/>
<span>{errors.password?.message}</span>
<input type="submit" value="가입하기" />
{errors.errors?.message}
</form>
);
}