React로 form의 이벤트를 모두 컨트롤하기 위해서는 발생 가능한 이벤트를 다 상정하여 개별적으로 state를 설정하여 컨트롤 해야하는 문제가 있다. 물론 이 마저도 바닐라 JS에 비해서는 아주 많이 간편해진 셈이지만, 사람의 욕심은 끝이 없고 개발자의 게으른 부지런함(?)도 마찬가지다.
form과 관련된 웬만한 이벤트들을 한 번 싸그리 모아다가 상정해두고 이를 컨트롤할 간단한 방법을 만들어두면 두고두고 쓸 수 있지 않을까?
위 고민에 대한 답을 내놓는 React 라이브러리가 바로 React-hook-form 이다.
1. 설치하기
$ npm i react-hook-form
위 커멘드를 입력하면 설치가 진행된다.
2. 기본 개념
React를 기반으로 하고 있으므로 큰 틀은 React와 동일하다. react-hook-form은 form 전용 라이브러리인 만큼, form으로 쓸 컴포넌트를 함수로 구현한 뒤 이를 export default 시켜서 원하는 곳에 Rendering하여 사용한다.
이 때, React-hook-form의 기능들은 기본적으로 useForm() 함수를 바탕으로 활용한다. React의 useState 느낌이라고 생각하면 된다.
예제를 보자.
import { useForm } from "react-hook-form";
interface LoginForm {
id: string;
password: string;
nickname: string;
}
export default function Forms() {
const {
register,
watch,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({ mode: "onChange" });
return (...);
}
useForm에서 register, watch, handleSubmit, formState를 가져온 것을 볼 수 있다.
export declare function useForm<
TFieldValues extends FieldValues = FieldValues,
TContext = any
>(
props?: UseFormProps<TFieldValues, TContext>
): UseFormReturn<TFieldValues, TContext>;
useForm을 뜯어본 모습이다. 결국 useForm이라는 함수는 UseFormProps에 정해진 값을 optional하게 받아와서 UseFormReturn에 규정된 값을 Return 하는 함수일 뿐이다.
이제 UseFormProps와 UseFormReturn의 값의 종류를 알아야 할 차례다. UseFormProps와 UseFormReturn의 내용은 아래와 같다.
//UseFormProps
export declare type UseFormProps<
TFieldValues extends FieldValues = FieldValues,
TContext = any
> = Partial<{
mode: Mode;
reValidateMode: Exclude<Mode, "onTouched" | "all">;
defaultValues: DefaultValues<TFieldValues>;
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
shouldFocusError: boolean;
shouldUnregister: boolean;
shouldUseNativeValidation: boolean;
criteriaMode: CriteriaMode;
delayError: number;
}>;
//UseFormReturn
export declare type UseFormReturn<
TFieldValues extends FieldValues = FieldValues,
TContext = any
> = {
watch: UseFormWatch<TFieldValues>;
getValues: UseFormGetValues<TFieldValues>;
getFieldState: UseFormGetFieldState<TFieldValues>;
setError: UseFormSetError<TFieldValues>;
clearErrors: UseFormClearErrors<TFieldValues>;
setValue: UseFormSetValue<TFieldValues>;
trigger: UseFormTrigger<TFieldValues>;
formState: FormState<TFieldValues>;
resetField: UseFormResetField<TFieldValues>;
reset: UseFormReset<TFieldValues>;
handleSubmit: UseFormHandleSubmit<TFieldValues>;
unregister: UseFormUnregister<TFieldValues>;
control: Control<TFieldValues, TContext>;
register: UseFormRegister<TFieldValues>;
setFocus: UseFormSetFocus<TFieldValues>;
};
모든 내용을 다 다룰 수는 없으니 세부 내용은 공식문서를 통해 각자 살펴보기로 하고, 이하에서는 아주 기본적인 값들인 register watch handlesubmit 등을 알아보자.
3. register 함수 이해하기
register는 react-hook-form에 의해 아래 값으로 설정되어 있다.
const register = (name, options = {}) => {
let field = get(_fields, name);
const disabledIsDefined = isBoolean(options.disabled);
set(_fields, name, {
_f: {
...(field && field._f ? field._f : { ref: { name } }),
name,
mount: true,
...options,
},
});
_names.mount.add(name);
field
? disabledIsDefined &&
set(_formValues, name, options.disabled
? undefined
: get(_formValues, name, getFieldValue(field._f)))
: updateValidAndValue(name, true, options.value);
return {
...(disabledIsDefined ? { disabled: options.disabled } : {}),
...(_options.shouldUseNativeValidation
? {
required: !!options.required,
min: getRuleValue(options.min),
max: getRuleValue(options.max),
minLength: getRuleValue(options.minLength),
maxLength: getRuleValue(options.maxLength),
pattern: getRuleValue(options.pattern),
}
: {}),
name,
onChange,
onBlur: onChange,
ref: (ref) => {
if (ref) {
register(name, options);
field = get(_fields, name);
const fieldRef = isUndefined(ref.value)
? ref.querySelectorAll
? ref.querySelectorAll('input,select,textarea')[0] || ref
: ref
: ref;
const radioOrCheckbox = isRadioOrCheckbox(fieldRef);
const refs = field._f.refs || [];
if (radioOrCheckbox
? refs.find((option) => option === fieldRef)
: fieldRef === field._f.ref) {
return;
}
set(_fields, name, {
_f: {
...field._f,
...(radioOrCheckbox
? {
refs: [
...refs.filter(live),
fieldRef,
...(!!Array.isArray(get(_defaultValues, name))
? [{}]
: []),
],
ref: { type: fieldRef.type, name },
}
: { ref: fieldRef }),
},
});
updateValidAndValue(name, false, undefined, fieldRef);
}
else {
field = get(_fields, name, {});
if (field._f) {
field._f.mount = false;
}
(_options.shouldUnregister || options.shouldUnregister) &&
!(isNameInFieldArray(_names.array, name) && _stateFlags.action) &&
_names.unMount.add(name);
}
},
};
};
코드를 보면 알겠지만, 첫번째 인자로 받아오는 값이 name의 value가 된다. 따라서 만약 console.log(register('메롱'))을 찍어보면 아래와 같은 log를 볼 수 있다.
또한, name만 설정해주면 onBlur, onChange, ref 와 같은 Key가 자동으로 생성되는 것을 볼 수 있다.
따라서, 아래와 같이 스프레드 문법을 활용하면 register 함수에 의해 생성되는 오브젝트의 하위 항목들을 자동으로 input에 property로 추가할 수 있게 된다!
import { useForm } from "react-hook-form";
export default function Forms() {
const { register } = useForm();
console.log(register("메롱"));
return (
<form>
<input
{...register("username")}
type="text"
placeholder="Username"
required
/>
</form>
);
}
위 코드는 아래 코드와 같은 셈!
import { useForm } from "react-hook-form";
export default function Forms() {
const { register } = useForm();
console.log(register("메롱"));
return (
<form>
<input
name="username"
onBlur="async (e) => {...}"
onChange="async (e) => {...}"
ref:(ref) => {...}
type="text"
placeholder="Username"
required
/>
</form>
);
}
4. watch 함수 이해하기
const watch = (name, defaultValue) => isFunction(name)
? _subjects.watch.subscribe({
next: (info) => name(_getWatch(undefined, defaultValue), info),
})
: _getWatch(name, defaultValue, true);
watch 함수는 onChange 함수와 비슷한데, form의 변화를 주시하고 있다가 입력한 값을 받아오는 역할을 수행한다.
즉, useForm의 register 함수에 입력한 name 을 Key로 가져와서, 해당 form에 입력된 input의 value를 Value로 바인딩해준다.
5. Validation을 위한 handleSubmit 함수 이해하기
handleSubmit 함수는 기본적으로 Submit 이벤트를 다루기 위한 함수이다. 따라서 Validation도 기본적으로는 Submit시 다뤄야하는 개념이기 때문에, handleSubmit 함수를 통해 Validation을 컨트롤 하게 된다.
handleSubmit 함수는 아래와 같다.
export declare type UseFormHandleSubmit<TFieldValues extends FieldValues> = (
onValid: SubmitHandler<TFieldValues>,
onInvalid?: SubmitErrorHandler<TFieldValues>
) => (e?: React.BaseSyntheticEvent) => Promise<void>;
즉, handleSubmit 함수의 첫 번째 인자는 onValid, 두 번째 인자는 onInvalid가 되며, 두 번째 인자는 optional 사항이다. 우리는 onValid와 onInvalid 함수를 만든 뒤 handleSubmit 함수에 인자로 전달하여 각 경우에 따라 사용하며, handleSubmit은 onSubmit에 넣어 사용하기 때문에 최종적으로 react에 의해 validation을 통과한 경우 onValid가, 통과하지 못한 경우 onInvalid가 실행되는 식이다.
또한, onVaild는 form에 입력된 유효한 data를 인자로 받아서 전달하고, onInvaild는 error를 인자로 받아서 전달한다.
이 때, 구 버전의 경우, 아래의 1과 같은 코드로도 작동하였지만 이제는 2의 방식으로 코딩해줘야 TS가 에러를 주장하지 않는다. 공식 문서에서도 잘 나와있지 않은 부분이니 기억해두자.
//1번 방식 : 예전에는 이렇게 해도 되었다.
const onValid = (data: LoginForm) => {
console.log(data);
};
const onInvalid = (errors: FieldErrors) => {
console.log(errors);
};
//2번 방식 : 이제는 이렇게 해야만 TS가 오류를 주장하지 않는다.
const onValid: SubmitHandler<LoginForm> = (data) => {
console.log(data);
};
const onInvalid: SubmitErrorHandler<LoginForm> = (errors) => {
console.log(errors);
};
1번 방식으로 코딩할때 나오는 에러는 아래와 같다.
또한, react는 validation에 관한 정보를 register의 두 번째 인자로 전달된 validation 코드블럭에서 가져온다.
그렇다면 우리가 활용할 수 있는 validation의 종류로는 뭐가 있을까? TypeScript를 활용하면 그 종류를 볼 수 있다.
export declare type RegisterOptions<TFieldValues extends FieldValues = FieldValues,
TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>>
= Partial<{
required: Message | ValidationRule<boolean>;
min: ValidationRule<number | string>;
max: ValidationRule<number | string>;
maxLength: ValidationRule<number>;
minLength: ValidationRule<number>;
pattern: ValidationRule<RegExp>;
validate:
| Validate<FieldPathValue<TFieldValues, TFieldName>>
| Record<string, Validate<FieldPathValue<TFieldValues, TFieldName>>>;
valueAsNumber: boolean;
valueAsDate: boolean;
value: FieldPathValue<TFieldValues, TFieldName>;
setValueAs: (value: any) => any;
shouldUnregister?: boolean;
onChange?: (event: any) => void;
onBlur?: (event: any) => void;
disabled: boolean;
deps: InternalFieldName | InternalFieldName[];
}>;
required, min, max, maxLength, minLength, pattern 이 가능하다는 것을 확인할 수 있다. (ValidationRule!)
6. Error message 만들기
ValidationRule은 재밌는 Type이다. 이 Type을 활용하면, 유효값의 설정 외에도 'message'를 통해 에러 메세지를 생성할 수도 있고, 이렇게 생성된 메세지를 Front-end에서 바로 보여주도록 만들 수도 있다.
//1번
minLength: 5
//2번
minLength: {
value: 5
message: "5글자 이상 입력하세요"
}
즉, 1번처럼 곧바로 value를 지정하면 최소 글자 길이를 5로 설정할 수 있지만, 2번처럼 코드를 작성하면 Invalid 상황에서의 message까지도 함께 설정할 수 있다는 뜻이다.
7. Custom Validate 만들기
근데 간혹 독특한 우리만의 Customize된 Rule이 필요한 경우가 있다. 그런 경우 쓰는 것이 바로 위 코드의 validate Key이다. validate를 통해 우리는 우리만의 규칙을 생성할 수 있다.
validate: {
phoneNum : (value) => value.includes("010")
}
만약 위와 같은 코드를 register의 두 번째 인자로 전달한다면, phoneNum이라는 이름의 Custom Valid Condition이 값에 010이라는 숫자가 존재하는지 체크한 후 010이 있어야만 Valid 판정을 내려준다. 이 경우 Invalid 일 때 message를 전달해주기 위해서는 삼항 연산자를 통해 조건문 처리를 해주면 된다.
validate: {
phoneNum : (value) => value.includes("010") ? "":"010을 입력해주세요"
}
삼항연산자를 이용하면 true인 경우 "" 를 리턴하도록 작성해줘야하는데 이것이 보기 싫다면 OR를 통해 아래와 같이 처리할 수도 있다.
validate: {
phoneNum : (value) => value.includes("010") || "010을 입력해주세요"
}
8. useForm의 mode
useForm의 UseFormProps에서 본 mode에서는 validate의 시점을 설정할 수 있다.
export declare type UseFormProps<
TFieldValues extends FieldValues = FieldValues,
TContext = any
> = Partial<{
mode: Mode; //Mode
reValidateMode: Exclude<Mode, "onTouched" | "all">;
defaultValues: DefaultValues<TFieldValues>;
resolver: Resolver<TFieldValues, TContext>;
context: TContext;
shouldFocusError: boolean;
shouldUnregister: boolean;
shouldUseNativeValidation: boolean;
criteriaMode: CriteriaMode;
delayError: number;
}>;
export declare type Mode = keyof ValidationMode; //ValidationMode
export declare type ValidationMode = {
onBlur: 'onBlur';
onChange: 'onChange';
onSubmit: 'onSubmit';
onTouched: 'onTouched';
all: 'all';
};
onBlur | 초점이 흐려질 때(unFocused될 때) |
onSubmit | 제출 이벤트가 발생할 때 |
onChange | input value가 변할 때 |
onTouched | Focused될 때 |
'Learning-Log > Computer Science' 카테고리의 다른 글
[Windows] Zone.Identifier 란 무엇이고, 어떻게 지울 수 있나? (0) | 2022.08.22 |
---|---|
[아키텍쳐] ARM vs AMD... 어라 그럼 Intel은 어디로..? (0) | 2022.08.22 |
[NextJS] ReactJS를 품은 프레임워크, NextJS를 알아보자 (0) | 2022.08.08 |
[도서/AI] '구글 브레인 팀에게 배우는 딥러닝 with TensorFlow.js' 개발자 리뷰 (0) | 2022.08.07 |
[TS] Typescript의 enum, const enum, as const 에 대해 알아보자 (0) | 2022.07.18 |