react-hook-form을 사용하고 있었지만, 나 나름대로 유효성 검사를 했다고 생각했는데,
다시 코드를 확인했을 때 제대로 활용하지 못하고 있다는 것을 깨달았다.
그래서 이번에 react-hook-form을 사용하면서 이번엔 정말 제대로된 form을 만들어보려고 한다!
react-hook-form
react form 라이브러리로 유효성 검사 등 폼에 관련한 유용한 기능들을 제공한다.
나같은 경우는 일단 component에 props로 폼 값들을 넘겨주기 싫어서 사용했었다.
먼저 알아야 할 유용한 몇몇 hook들이 있다.
FormProvider & useFormContext
form을 FormProvider로 감싸고 입력 컴포넌트에서 useFormContext의 register 통해 입력 값들을 form과 연결시킬 수 있다.
register
input필드에 register('입력 필드 이름', {options})을 해주면, 알아서 form에는 '입력 필드 이름' : 값 이 들어가게 된다.
options에는 required(필수), pattern(전화번호 패턴 000-0000-0000)과 같은 설정을 해줄 수 있고, 이런 options와 맞지 않으면 erros의 해당 필드에 error객체가 생긴다.
formState{errors}
form값들의 유효성 검사를 한 후, 통과되지 못하면 formState(errors)에 에러 객체가 추가된다.
errors.'입력 필드 이름'.message를 통해 어떤 에러가 발생한 것인지 확인할 수 있다.
trigger
trigger('입력 필드 이름')을 수행하게 되면, 해당 필드의 유효성 검사를 실행한다.
나같은 경우는 입력 필드에서 엔터를 쳤을 때, 다음 입력 필드로 넘어가기 전에 trigger로 검사한 후, 성공했을 때 넘어가도록 구현하였다.
setValue
setValue('입력 필드 이름')은 실제 필드에 값을 넣어주는 것이다.
나같은 경우는 form에 버튼으로 값을 선택해야 하는 경우가 있었고, 이때는 setValue로 직접 값을 매핑해주었다.
watch, getValues
watch는 값이 변할때마다 동적으로 감지하는 것이고, getValues는 호출했을 때 그 필드의 값을 감지하는 것이다.
1. form에 입력 필드 등록하기
먼저, form을 Form Provider로 감싸고, 밑에 <NameBox>라는 입력 필드 컴포넌트를 넣어주었다.
// NameBox.tsx
<FormField title="이름" required={true}>
<input
className={inputContent({ state: !!errors.name ? 'error' : 'default' })}
type="text"
placeholder="이름을 입력하세요."
tabIndex={1}
{...register('name', {
required: '이름을 입력해주세요.',
pattern: {
value: regex,
message: '올바른 형식이 아닙니다.',
},
})}
onKeyDown={handleKeyDown}
/>
{errors?.name && <div className={error}>{errors.name.message}</div>}
</FormField>
register하면 form의 name필드에 input값이 들어가게 된다.
react-hook-form은 먼저 유효성 검사를 하고 실제 submit을 진행하게 되는데, 만약 input에 내용을 입력하지 않고 제출 버튼을 누르면
named의 required옵션이 지켜지지 않았으므로, errors.name에 {message: '이름을 입력해주세요'}라는 객체가 들어가게 되고,
위 코드에 따르면 error가 화면에 보여지게 된다. (당연히, 유효성 검사에서 이미 실패했기 때문에 submit이 작동하지 않는다)
만약, 내용을 입력했으나, pattern에 어긋난다면, '올바른 형식이 아닙니다'라는 메세지가 화면에 출력된다.
2. 엔터 입력 시 다음 입력 필드로 넘어가기
이때 나는 정확히는 엔터 입력 시 해당 필드만 유효성 검사를 진행하고, 성공하면 다음 탭 필드로 넘어갔다.
// NameBox.tsx
const handleKeyDown = async (
event: React.KeyboardEvent<HTMLInputElement>,
) => {
// 설정안하면 event가 두번 작동하게 됨.
if (event.nativeEvent.isComposing) {
return;
}
if (event.key === 'Enter') {
event.preventDefault();
const isValid = await trigger('name');
if (isValid) {
setFocus('nickName')
}
}
};
중요한게 몇몇 있다.
- event.nativeEvent.isComposing
- 엔터를 누를때 console을 통해 확인해보니, 해당 이벤트 핸들러가 2번 작동하였다.
- 원인을 찾아보았을 때, IME때문이었는데 이는 한글 자모가 조합이 완료된 상태인지 아닌지 정확히 알 수 없기 때문에 각각의 상태에서 모두 이벤트 핸들러를 호출하기 때문이었다.
- 따라서 자모가 조합이 완료된, 조합중이지 않은 상태인 isComposing=false일때만 이벤트 핸들러가 동작하도록 구현하였다.
- 먼저, event.preventDefault를 실행하여 기본 이벤트를 막아야한다.( submit과 같은 )
- 그리고 trigger를 통해 해당 필드만 유효성 검사를 실행한다.
- 유효성 검사를 통과하면, 다음 인풋 필드로 setFocus함수를 통해 포커스 시킨다.
위와같이 코드를 작성한다면, 유효성 검사를 실행하고 그에 맞는 에러도 잘 보여주며
엔터 누를때 다음 입력 필드로 자연스럽게 이동하도록 구현하는 것이 가능하다!
+) 삽질
- 남성, 여성 선택 버튼이 있었고 button요소로 했더니 submit했을 때와 같은 일이 일어났다.
- form안에서 button요소를 누르면 자동으로 submit으로 인정되어서 type="button"으로 지정해주어야 했다.
- 몇몇 input에 onChange가 있었는데, 이는 register가 충돌이 나서 결국 form에 Input값이 제대로 안들어오는 오류가 발생했다. 그래서 register할 input필드에는 왠만하면 onChange핸들러를 사용하지 않는게 좋은 것 같다.
- 최신수정) register options에 onChange가 있다. 따라서, input onChange하고 싶은게 있다면 register('field, {onChange: (e) => {} })로 하면 된다.