React Hook Form
이는 사용하기 쉬운 유효성 검사를 통해 선응이 뛰어나고 유연하며 확장 가능한 form입니다.
기존의 form형식을 간단히 아래에 적어보도록 하겠습니다.
import { useState } from "react";
export default function Forms() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [formErros, setFormErros] = useState("");
const onUsernameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setUsername(value);
};
const onEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setEmail(value);
};
const onPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const {
currentTarget: { value },
} = event;
setPassword(value);
};
const onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
if (username === "" || email === "" || password === "") {
setFormErros("All fields are required");
}
console.log(username, email, password);
};
return (
<form onSubmit={onSubmit}>
<input
value={username}
onChange={onUsernameChange}
type="text"
placeholder="Username"
required
/>
<input
value={email}
onChange={onEmailChange}
type="email"
placeholder="Email"
required
/>
<input
value={password}
onChange={onPasswordChange}
type="password"
placeholder="Password"
required
/>
<input type="submit" value="Create Account" />
</form>
);
}
이러한 코드에 3개의 input이 있고 저희는 유저의 값을 submit하는 것을 지켜봐야 합니다. 하지만 저희는 client들을 믿어서는 안됍니다. 잘못된 값이 들어올 수 있는 가능성이 즐비하기 때문입니다. 막 실수로 한칸 안적어서 낸다거나, 이메일 형식에 맞지 않게 낸다거나, ... 너무 경우의 수가 많습니다. 따라서 저희는 기존이라면 FormErrors여러개를 만든 다음에 각각의 경우마다 다른 에러 텍스트 메세지를 보여지게 만들었습니다. 이러면 코드도 너무 장황해지고 어려워 집니다.
즉 저희는 이렇게 Form의 유효성 검증을 User Experience를 좋게 만들기 위해서 React Hook Form을 사용하게 됩니다. 위의 코드를 React Hook Form으로 다시 써보도록 하겠습니다.
import { useState } from "react";
import { useForm } from "react-hook-form";
// Less code ( checked )
// Better validation
// Better Errors (set, clear, display)
// Have contro over inputs
// Dont deal with events ( checked )
// Easier Inputs ( checked )
export default function Forms() {
const { register, watch } = useForm();
return (
<form>
<input {...register("username")} type="text" placeholder="Username" required />
<input {...register("email")} type="email" placeholder="Email" required />
<input
{...register("password")}
type="password"
placeholder="Password"
required
/>
<input type="submit" value="Create Account" />
</form>
);
}
다음과 같이 매우 간단해 졌습니다.
여기서 핵심은 regitser, watch입니다.
register:(name: string, RegisterOptions?) => ({onChange, onBlur, name, ref}) 이 메서드를 사용하면 input을 등록하거나 엘리먼트를 선택하고 React Hook Form에 유효성 검사 규칙을 적용할 수 있습니다. 유효성 검사 규칙은 모두 HTMl표준을 기반으로 하며 사용자 지정 유효성 검사 방법도 혀용합니다.
watch 는 우리가 등록했던 input의 변화를 감지하는 역할을 합니다.
React Hook Form Validation
이제 validation을 하는 방법을 봅시다. 우선 저희의 3개의 form은 모두 required입니다. 이를 위해서는 register의 두번째 변수(options?)에다 {required: true}를 넣어주어야 합니다. 이 외에도 다양한 옵션들을 설정할 수 있습니다.
하지만 이를 추가해도 form에 아무것도 안적어도 아마 브라우저는 그냥 submit을 해 버릴겁니다. 그 이유는 form의 onSubmit을 아무것도 처리를 안해주었기 때문입니다. 여기서 사용될 함수는 React Hook Form의 handleSubmit입니다. 이는 인자로 2개의 함수를 받고 최소 1개의 함수를 받습니다. 첫번째 인자는 onValid로서 (성공적인 콜백)이라고도 하며 형식은 (data: Object, e?:Event) => void입니다. 두 번쨰 함수는 onInValid로서 (오류 콜백)이라고도 하며 형식은 (erros: Object, e?:Event) => void입니다.
저희는 위의 방식에 따라서 코드를 다음과 같이 수정해 주겠습니다.
import { useState } from "react";
import { useForm } from "react-hook-form";
// Less code ( checked )
// Better validation
// Better Errors (set, clear, display)
// Have contro over inputs
// Dont deal with events ( checked )
// Easier Inputs ( checked )
export default function Forms() {
const { register, watch, handleSubmit } = useForm();
const onValid = () => {
console.log("im valid bby");
};
return (
<form onSubmit={handleSubmit(onValid)}>
<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="Create Account" />
</form>
);
}
이렇게 되면 Js코드가 안쓴 form을 인식해서 form을 제출하려고 하면 자동으로 안쓴 form으로 커서를 이동시켜 줄겁니다.
또한 password마져 채우게 되면 다음과 같이 onValid함수에서 써준 것이 보일 겁니다.
이제 가장 중요한 파트 입니다. onInvalid함수를 만들어 봅시다. 이는 form validation에서 오류가 났을 때 발생하는 콜백입니다. 일단 코드부터 봅시다.
import { useState } from "react";
import { FieldErrors, useForm } from "react-hook-form";
// Less code ( checked )
// Better validation
// Better Errors (set, clear, display)
// Have contro over inputs
// Dont deal with events ( checked )
// Easier Inputs ( checked )
interface LoginForm {
username: string;
password: string;
email: string;
}
export default function Forms() {
const { register, watch, handleSubmit } = useForm<LoginForm>({
defaultValues: {},
});
const onValid = (data: LoginForm) => {
console.log("im valid bby");
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
<form onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "The username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
/>
<input
{...register("email", { required: "Email is required" })}
type="email"
placeholder="Email"
/>
<input
{...register("password", { required: "Password is required" })}
type="password"
placeholder="Password"
/>
<input type="submit" value="Create Account" />
</form>
);
}
일단 자동완성 기능을 위해 onInvalid의 인자의 errors타입에는 FieldErrors를 넣고, LoginForm interface를 만든다음에 필요한 곳에 다 집어 넣어 줍시다. 그러면 자동완성기능이 멋지게 나타날 겁니다. 그리고 input의 option에 제약조건을
다음과 같은 조건들을 넣을 수 있습니다. 이름이 직관적이여서 이해가 다들 가시죠?? https://react-hook-form.com/api/useform/register 여기에 자세하게 나와있습니다.
그리고 각각의 제약조건에 무슨 이상한 메세지가 들어가 있을 겁니다.
이는 FieldsError의 속성으로 각각의 에러에 맞는 적절한 메세지를 넣어줄 수 있는 기능입니다. 위의 저의 예시에서는 만약에 다음과 같이 제가 form을 채웠다고 봅시다.
당연히 email, password는 비어있고 username은 mixLen이 5인데 길이가 4여서 오류가 3개 다 뜰겁니다. 이 떄 onInvalid에서 FieldErrors타입의 errors데이터를 보면 다음과 같습니다.
각각의 키에 맞는 값에 type에 오류가 난 이유와 message에 저희가 객체로 준 message와 이의 ref가 들어 있는 것을 보실 수 있습니다. 매우 놀라운 기능이네요, 각각의 ErrorState를 정의하지 않고도 이렇게 간단하게, Error를 관리할 수 있다는 것이요. 주목해야 할점은 현제까지 아직은 브라우저에서 지원하는 required, minLength, maxLength ... 등만 보았다는 것입니다. 이제 브라우저에서도 지원하지 않는 예를 들어 이메일 gmail.com만 허용해주세요. 등을 어떻게 validation하는지 알아보겠습니다.
우선 커스텀 validation을 적용한 코드부터 봅시다.
import { useState } from "react";
import { FieldErrors, useForm } from "react-hook-form";
// Less code ( checked )
// Better validation
// Better Errors (set, clear, display)
// Have contro over inputs
// Dont deal with events ( checked )
// Easier Inputs ( checked )
interface LoginForm {
username: string;
password: string;
email: string;
}
export default function Forms() {
const {
register,
watch,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({
mode: "onChange",
defaultValues: {
username: "hyunseo",
},
});
const onValid = (data: LoginForm) => {
console.log("im valid bby");
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
return (
<form onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "The username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
/>
<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?.message)
? "border-red-500 outline-none focus:border-red-500 focus:outline-none focus:ring-1 focus:ring-0"
: ""
}`}
/>
{errors.email?.message}
<input
{...register("password", { required: "Password is required" })}
type="password"
placeholder="Password"
/>
<input type="submit" value="Create Account" />
</form>
);
}
여기서 validate의 속성으로 Object를 주었습니다. 키 값으로는 notGmail을 주었는데, 이는 오류 이름을 주면 됩니다. 그리고 속성으로는 인자로 input의 값을 주고 걸러낼 로직을 작성해 주면 됩니다. 여기서는 !value.includes("@gmail.com") || "message"을 주었습니다. 즉 @gmail.com이 들어가면 FieldErrors의 메시지로 message로 설정하도록 했습니다.
그리고 저희는 이러한 message를 전역적인 변수로서 화면에 보여주고 싶었을 겁니다. 그럴 때는 useForm의 formState를 활용해 주면 됩니다.
export declare type FormState<TFieldValues> = {
isDirty: boolean;
dirtyFields: FieldNamesMarkedBoolean<TFieldValues>;
isSubmitted: boolean;
isSubmitSuccessful: boolean;
submitCount: number;
touchedFields: FieldNamesMarkedBoolean<TFieldValues>;
isSubmitting: boolean;
isValidating: boolean;
isValid: boolean;
errors: FieldErrors<TFieldValues>;
};
이와 같이 FormState는 React Hook Forms의 모든 정보를 가지고 있습니다. 여기서 저희는 FieldErrors<TFieldValues>만이 필요하므로 이만 끄집어 줍니다. 그리고 email input아래에 {errors.email?.meesage}으로 message가 있을 때만 렌더링 해줍니다. (옵셔널 체이닝 사용)
또한 저희는 tailwind와 React Hook Form을 같이 사용할 수 있습니다. Boolean(errors.email?.message)을 해주어 이가 true면 message가 있어 오류가 나타난 것이므로, border와 ring을 red-500으로 주어 태두리를 빨간색으로 지정해 줄 수 있습니다. 아래와 같이 말이죠
또한 이 외에도 form의 defaultVlaues를 useForm의 인자로 주어 초기화 가능합니다 (interface로 LoginForm의 타입을 지정해 주어서 자동 타입 추론이 가능해 졌을 겁니다. ) 이외에도 저희는 위의 그림에서 메시지와 빨간색 태두리가 Create Account버튼을 누르고 onInvalid가 실행되어야지만 작동합니다. 하지만 useForm의 인수로 mode값을 다양하게 주어서 이의 메커니즘을 바꿀 수 있습니다.
대충 이런식이고 onChange는 form의 데이터가 바뀌자 마자 바로, onBlur는 커서가 form바깥으로 갔을 떄 ... 등등입니다. https://react-hook-form.com/api/useform 공식 홈페이지에 자세하게 나와있으므로 참고하면 좋을 것 같습니다.
Extras
일단 코드를 봅시다.
import { useState } from "react";
import { FieldErrors, useForm } from "react-hook-form";
// Less code ( checked )
// Better validation
// Better Errors (set, clear, display)
// Have contro over inputs
// Dont deal with events ( checked )
// Easier Inputs ( checked )
interface LoginForm {
username: string;
password: string;
email: string;
errors?: string;
}
export default function Forms() {
const {
register,
watch,
handleSubmit,
formState: { errors },
setError,
setValue,
reset,
resetField,
} = useForm<LoginForm>({
mode: "onChange",
defaultValues: {
// username: "hyunseo",
},
});
const onValid = (data: LoginForm) => {
console.log("im valid bby");
setError("errors", { message: "Backend is offline sorry." });
// reset();
resetField("password");
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
// console.log(watch("email"));
// setValue("username", "hello");
return (
<form onSubmit={handleSubmit(onValid, onInvalid)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
message: "The username should be longer than 5 chars.",
value: 5,
},
})}
type="text"
placeholder="Username"
/>
<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?.message)
? "border-red-500 outline-none focus:border-red-500 focus:outline-none focus:ring-0"
: ""
}`}
/>
{errors.email?.message}
<input
{...register("password", { required: "Password is required" })}
type="password"
placeholder="Password"
/>
<input type="submit" value="Create Account" />
{errors.errors?.message}
</form>
);
}
위에서 설명한 것 외에도, 중요한 몇가지 기능들을 예기해 보려고 합니다.
먼저 위에서도 말했지만 watch입니다. watch가 중요한 이유는 많은 곳에서 field의 데이터를 필요로 하는데, 그때마다 watch로 값을 사용해와야하기 때문입니다. watch("username")와 같이 fieldname을 인자로서 하나만 인식할수도, 필드 데이터 모두를 인식할 수도 있습니다
그 다음으로는 setError입니다. 만약 onValid가 실행되는데 그 안에서 fetch된 api가 오류를 뜨면 어떻게 해야 할까요? 바로 이 떄도 error를 설정하고 화면에 보여 주어야 합니다. 그러려면 LoginForm을 수정하고 setError을 통해 적절히 Error를 Manual하게 설정할 수 있습니다.
그 외에도 모든 form state또는 form state의 일부를 초기화 하는 reset()도 있습니다.
그리고 개별 field state를 재 설정하는 resetField()도 있습니다.
'Web > CloneCoding' 카테고리의 다른 글
[Carrot Market] #9 - Authentication - 1 (0) | 2022.05.12 |
---|---|
[Carrot Market] #8 REFACTORING Form (0) | 2022.05.04 |
[Carrot Market] #6 Prisma - PlanetScale (0) | 2022.05.03 |
[Carrot Market] #5 TAILWIND ReFactoring UI - 2 (0) | 2022.05.02 |
[Carrot Market] #5 TAILWIND ReFactoring UI - 1 (0) | 2022.05.02 |