June 19, 2024
안녕하세요. 요즘 실무에서 form을 다룰 때 react-hook-form library를 사용하고 있습니다.
form의 input을 상태로 관리하면 상태가 바뀔 때마다 리렌더링이 발생해 불필요한 리렌더링이 많아집니다. react-hook-form을 사용하면 각 form을 독립적으로 관리할 수 있어서 다른 form의 리렌더링을 방지할 수 있습니다. 또한 성능 최적화도 잘 되어 있고 쉽게 사용가능한 유용한 api들도 제공해줘서 form 관리가 편해졌습니다.
실무에서 다양한 요구사항을 구현하면서 많이 검색해봤지만, 검색해도 안 나오는 부분들이 많아서 api의 type을 보면서 스스로 터득한 것들을 sandbox와 함께 공유하려고 합니다. 코드의 각 줄에 주석을 달아 설명을 추가했습니다. 코드 아래에 sandbox 링크를 첨부할테니 활용해보시면 좋을 것 같습니다.
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import { useController, useForm } from 'react-hook-form'
import './styles.css'
const formatPhoneInput = (value: string) => {
return (
value
// 숫자를 제외한 입력값 제거
.replace(/[^0-9]/g, '')
// 휴대폰 번호 형식으로 하이픈 추가
.replace(
/(^02|^0505|^1[0-9]{3}|^0[0-9]{2})([0-9]+)?([0-9]{4})/,
'$1-$2-$3'
)
// 하이픈이 여러개면 제거
.replace('--', '-')
)
}
function App() {
const [includePhone, setIncludePhone] = useState(false)
const {
register,
handleSubmit,
watch,
formState: { errors },
control,
} = useForm({
mode: 'onChange',
// form value 초기값을 설정할 수 있습니다.
// 해당 객체의 key값은 항목의 'name'입니다.
defaultValues: {
example: '',
exampleRequired: '',
exampleRequiredWithMessage: '',
mobile: '',
email: 'abc@abc.com',
},
})
// errors는 각 항목 name들을 key로 가지는 map입니다.
// 예를 들어 type은 다음과 같으며 등록된 항목에 따라 바뀝니다.
// DeepMap<{example: string; exampleRequired: string; exampleRequiredWithMessage: string;}, FieldError>
// error가 없을 땐 빈 map입니다.
// {}
// error가 발생하면 해당 항목에 에러 정보가 담깁니다. error가 없으면 해당 항목 error 정보는 없습니다.
// message는 사용자가 지정한 메시지이고, type에는 어떤 validation rule인지가 담깁니다.
// {
// phone: {
// message: '올바른 휴대폰 번호를 입력해주세요.",
// ref: HTMLInputElement,
// type: 'pattern'
// },
// exampleRequiredWithMessage: {
// message: 'example required',
// ref: HTMLInputElement,
// type: 'required'
// }
// }
// 유저가 입력할 때 하이푼이 자동으로 들어가도록 설정하고 싶음.
// 커스텀하기 위해 controller를 사용합니다.
const { field: mobileField } = useController({
name: 'mobile',
control,
rules: {
required: '휴대폰 번호를 입력해주세요.',
pattern: {
value: /^01(0|1|[6-9])-(?:\d{3}|\d{4})-\d{4}$/,
message: '올바른 휴대폰 번호를 입력해주세요.',
},
maxLength: 13,
},
})
useEffect(() => {
console.info('errors : ', errors)
}, [{ ...errors }])
// watch에 항목의 'name'을 넘겨서 값이 바뀔 때마다 체크할 수 있습니다.
console.log(watch('example'))
return (
<form
// form 제출을 할 때 유효성 검사에 성공하면 handleSubmit callback의 parameter로 form data가 넘어옵니다.
// 유효성 검사에 실패하면 실패한 input에 focus가 됩니다.
onSubmit={handleSubmit(data => {
alert(JSON.stringify(data))
})}
>
<label>Example</label>
{/* 항목을 등록할 때 register 함수를 사용합니다.
'example' 항목을 등록합니다. */}
<input {...register('example')} />
<label>ExampleRequired</label>
<input
/* register의 두번째 인자로 ValidationRule option을 넘길 수 있습니다.
require rule과 maxLength rule을 추가합니다. */
{...register('exampleRequired', { required: true, maxLength: 10 })}
/>
{/* validation을 통과하지 못하면 errors 객체에 정보가 넘어옵니다.
message를 등록할 때 지정하지 않고 이렇게 임의로 보여줄 수 있습니다. */}
{errors.exampleRequired && <p>This field is required</p>}
<label>ExampleRequiredWithMessage</label>
<input
// required rule validation을 통과하지 못했을 때 메시지를 에러 정보에 담고 싶다면 메시지를 바로 넘기면 됩니다.
{...register('exampleRequiredWithMessage', {
required: 'example required',
})}
// validation rule에 조건을 주고 싶을 때가 있습니다. 그러면 ValidationRule의 형태로 넘기면 됩니다.
{...register('exampleRequiredWithMessage', {
required: {
value: true, // 원하는 조건식
message: 'example required',
},
})}
/>
{/* errors에 담긴 message를 사용합니다. */}
{errors.exampleRequiredWithMessage && (
<p>{errors.exampleRequiredWithMessage?.message}</p>
)}
<label>ExampleRequiredWithMessage</label>
<input
// validation을 통과하지 못했을 때 메시지를 보여주고 싶다면 required rule에 string을 넘기면 됩니다.
{...register('exampleRequiredWithMessage', {
required: 'example required',
})}
/>
{errors.exampleRequiredWithMessage && (
<p>{errors.exampleRequiredWithMessage?.message}</p>
)}
<label>Email</label>
<input
{...register('email', {
required: '이메일을 입력해 주세요.',
pattern: {
value: /\S+@\S+\.\S+/,
message: '올바른 이메일 주소를 입력해주세요',
},
})}
/>
{errors.email && <p>{errors.email?.message}</p>}
<div>
<label htmlFor="includePhone">Include Phone</label>
<input
id="includePhone"
type="checkbox"
onChange={({ target: { checked } }) => setIncludePhone(checked)}
style={{ width: 'auto' }}
></input>
</div>
{includePhone && (
<>
<label>Mobile</label>
<input
// 하이픈이 들어간 휴대폰 번호를 받고 싶다면
{...mobileField}
onChange={e => {
const formattedValue = formatPhoneInput(e.target.value)
mobileField.onChange(formattedValue)
}}
/>
{errors.mobile && <p>{errors.mobile?.message}</p>}
</>
)}
<input type="submit" />
</form>
)
}
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)