React-hook-form
개인적으로는 이것저것 커스텀 훅을 만들어서 라이브러리 없이 인풋을 처리해왔지만, 슬슬 한계가 오고 있지 않나 싶다. 마침 새로운 프로젝트에서는 인풋을 React-hook-form(줄여서 훅폼)으로 관리하자는 이야기가 나와서 ─ 새로운 기술을 배워야할 때 늘 그래왔던 것처럼 ─ 공식 문서를 좀 읽어보았다. 쓸만한 것들(useful 말고 will use)을 여기에 조금 남겨본다.
useForm
optional arguments
useForm 훅은 여러 옵션을 설정할 수 있는 하나의 객체를 받아서, 인풋에 달아줄 수 있는 다양한 함수와 객체와 값들을 리턴한다.
mode
onChange | onBlur | onSubmit | onTouched | all
mode 속성은 input validation을 어느 시점에 수행할 것인가를 결정한다. 기본값은 onSubmit이고, onChange로 설정하면 다수의 리랜더링이 발생할 수 있어 성능에 영향을 끼칠 수 있다고 한다.
reValidateMode
onChange | onBlur | onSubmit | onTouched | all
reValidateMode 속성은 한 번 Validate를 진행한 이후 어느 시점에 값들을 재검증할 것인가를 결정한다. 기본값은 onChange이다.
defaultValues
{
defaultValues: {
firstName: '',
lastName: ''
}
}
defaultValues 속성은 input에 미리 값을 넣어두는 initValue를 설정할 수 있도록 한다.
values
function App() {
const values = useFetch("/api")
useForm({
defaultValues: {
firstName: "",
lastName: "",
},
values, // will get updated once values returns
})
}
values 속성은 인풋의 initValue를 설정하는데, 다만 어떠한 비동기 작업의 결과를 나중에 initValue로 업데이트하여 넣어주는 듯하다.
shouldFocusError
shouldFocusError 속성은 기본값이 true인데, 이는 form submit 과정에서 validation을 실패했을 때 여러 인풋 중 에러가 발생한 첫번째 인풋에 focus를 주는 것이다. 당연히 false로 해두면 이런 일은 발생하지 않는다.
shouldUnregister
shouldUnregister 속성은 조금 복잡하기는 한데, 조건부 랜더링과 같은 이유로 ─ 이럴 경우가 얼마나 될까 싶기는 하지만 아무튼 ─ input이 unmount 되어도 해당 input의 value를 유지한다고 한다. 이 때 shouldUnregister 속성을 true로 해두면 input이 unmount될 때 useForm도 해당 input을 unregister 하게 된다.
resolver
npm install @hookform/resolvers
resolver 속성을 사용하면 Yup이나 Zod 등의 외부 라이브러리를 사용하여 validation을 처리할 수 있다. 이 중 Zod의 사용법에 대해서는 뒤에서 정리해보도록 하겠다.
what dose useForm return
resister 메소드
resister 메소드는 string 하나를 받는데, 이 string의 형태에 따라서 submit 결과가 달라지는 듯하다.
이 함수는 onChange, onBlur, name, ref를 가지고 있는 하나의 객체를 리턴한다. resister에서 어떤 validation을 취급할 것인지 ─ 가령 minLength나 required 따위를 ─ 추가해줄 수 있는데, 커스텀 에러 메세지를 사용하려면 validation에 따라 각각 다른 방식으로 에러 메세지를 튜닝해야 해서 필요할 때마다 공식문서 들어가는 수 밖에 없을 것 같다. 물론 나는 Zod에서 처리해주는 커스텀 에러 메세지를 사용할 거라 상관 없을 지도?
정해진 validation 외에도 원하는 validate 로직을 추가할 수 있다. 따라서 아래와 같은 방식으로 비밀번호와 비밀번호 확인이 일치하는지를 검증할 수 있다.
const {
register,
watch,
setError,
getValues,
clearErrors,
formState: { errors },
} = useForm({ mode: "onChange" });
useEffect(() => {
if (
watch("password") !== watch("passwordCheck") &&
watch("passwordCheck")
) {
setError("passwordCheck", {
message: "비밀번호가 일치하지 않습니다",
});
} else {
clearErrors("passwordCheck");
}
}, [watch("password"), watch("passwordCheck")]);
return (
<>
<input {...register("password")} />
<input
{...register("passwordCheck", {
required: true,
validate: (value) => {
const { password } = getValues();
return password === value || "비밀번호가 일치하지 않습니다";
},
})}
/>
{errors.passwordCheck && <div>{errors.passwordCheck.message}</div>}
</>
)
formState
formState 객체는 전체 form에 대한 정보를 담고있다. 따라서 form 상태를 추적해야할 때 도움이 된다.
- isDirty : 전체 input 중 하나라도 value가 변했다면 true. 기준은 useForm에 넣어준 defValue인 듯하다.
- dirtyFields : value가 변한 input 묶음을 객체로 들고 있다.
- touchedFields : 한 번이라도 onFocus 된 input 묶음을 객체로 들고 있다.
- isValid : 모든 input이 문제가 없을 때 ture. useForm 선언 당시 mode="onChange"로 했어야만 의미가 있다.
- isSubmitted : 성공 여부와 상관 없이 한 번이라도 form이 submit을 했으면 true
- isSubmitSuccessful : form submit이 성공하면 true
- isSubmitting : form submit 중이면 ture
- submitCount : 몇 번 submit 했는지를 number로 세어준다
- isLoading : form이 비동기 뭐시기로 defValue를 로딩하고 있을 때 true
- errors : validate error가 발생한 input 묶음을 객체로 들고 있다. 이 모든 프로퍼티 중에서 가장 중요한 프로퍼티일 듯
watch 메소드
watch 메소드는 input value를 onChange가 달려있는 것처럼 계속 추적할 수 있는 도구이다. 아규먼트를 넣지 않으면 모든 input value를 객체로 추적하고, 문자열을 넣으면 해당 input 하나만 추적하며, 배열에 문자열을 담으면 해당하는 input들을 배열로 추적한다.
이러한 추적이 과도한 randering을 일으키지 않게 하려면 watch 메소드에 콜백 함수를 넣어서 useEffect로 감싸는 아래와 같은 방법이 존재한다.
useEffect(() => {
const subscription = watch((data) =>
console.log(data)
)
return () => {
subscription.unsubscribe()
}
}, [watch])
handleSubmit 메소드
const onSubmit = async (data, error) => {}
const onError = () => {}
<form onSubmit={handleSubmit(onSubmit, onError)}>
handleSubmit 메소드는 내부에서 에러가 터지는 걸 처리하지 못한다. 따라서 onSubmit 함수에서 try catch를 통해 에러 핸들링을 해주어야 한다.
reset + resetField 메소드
reset 메소드는 모든 input value를 날려버리는 기능을 한다. 이 메소드 안에 객체를 넣어서 value를 싹 날리는 대신 특정한 값으로 변하도록 만들 수도 있다.
onClick={() => {
reset({
firstName: "bill",
lastName: "luo",
})
}}
resetField 메소드는 하나의 input value를 핀포인트로 날려보낸다. 여러 옵션을 넣어서 사용할 수 있는데 필요할 때마다 공식 문서 찾아 보는 게 낫겠다.
<button
type="button"
onClick={() => resetField("firstName", { keepError: true, keepTouched: true, keepDirty: true, defaultValue: "New" })}
>
useController
Controller 컴포넌트
외부 라이브러리를 이용해 컴포넌트를 추적하기 위해서는 훅폼이 자체적으로 지원하는 Controller 컴포넌트를 이용하면 된다. 이 컴포넌트를 사용해서 외부 라이브러리가 랜더링 되도록 한다.
render에 넣어주는 콜백 함수의 아규먼트에는 객체가 하나 들어온다. 이 객체는 field, fieldState, formState 객체를 담고 있고, 보통은 field 객체가 가지고 있는 onChange, onBlur, value, name, ref 등의 속성을 이용해서 외부 라이브러리에 필요한 정보를 가져오게 된다.
<Controller
// useForm이 추적할 수 있는 이름
name={'starRate'}
// useForm이 리턴하는 값 중 하나인 control 달아주기
control={control}
// 실제로 랜더링되는 것은 콜백함수가 리턴하는 컴포넌트
render={({ field: { onChange, value } }) => <Rating value={parseInt(value)} onChange={onChange} />}
/>
useFormContext
이 커스텀 훅을 사용하면 중첩된 구조를 따라서 prop을 내려주지 않고도 마치 context api를 사용한 것처럼 직접 useForm이 리턴한 값들을 내려보낼 수 있다.
import React from "react"
import { useForm, FormProvider, useFormContext } from "react-hook-form"
export default function App() {
const methods = useForm()
const onSubmit = (data) => console.log(data)
return (
<FormProvider {...methods}>
// pass all methods into the context
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInput />
<input type="submit" />
</form>
</FormProvider>
)
}
function NestedInput() {
const { register } = useFormContext() // retrieve all hook methods
return <input {...register("test")} />
}
Zod
npm install zod
앞서 useForm의 optional arguments에서 resolver 속성을 사용하면 외부 라이브러리를 사용해 validate 로직을 처리할 수 있다고 했었다. Zod를 사용하면 validate 로직 뿐만 아니라 각 input의 타입도 ─ 거의 ─ 자동으로 처리할 수 있다.
기본적인 사용 모습
const schema = z.object({
email: z.string().email(),
password: z.string().regex(),
passwordCheck: z.string().regex(),
})
type Schema = z.infer<typeof schema>
const App = () => {
const { register, handleSubmit } = useForm<Schema>({
resolver: zodResolver(schema),
})
const onSubmit = (data: Schema) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
<input {...register("password")} />
<input {...register("passwordCheck")} />
</form>
)
}
useForm을 보면 타입도 잘 들어가고, resolver 속성을 통해 Zod를 사용해 설정한 validate 로직을 사용할 수 있게 된다. 문제는 passwordCheck인데, 이는 Zod가 제공하는 superRefine 메소드를 사용해 해결할 수 있다.
superRefine 메소드
.superRefine(({ checkPassword, password }, ctx) => {
if (checkPassword !== password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'password not matched',
path: ['checkPassword'],
});
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'password not matched',
path: ['password'],
});
}
});
refine 메소드는 훅폼의 register 메소드의 validate 속성처럼 Zod에 존재하지 않는 validate 로직을 알아서 추가할 수 있게 해준다. 여기서 더 나아가서 superRefine 메소드는 각 input 사이에서 값을 비교하고 validate 로직을 처리할 수 있도록 한다. 이 메소드의 첫 번째 아규먼트는 전체 input value를 추적하고, 두 번째 아규먼트는 특정 input에 error를 세팅한다.
블로그의 정보
Ayden's journal
Beard Weard Ayden