React Hook Form 사용법 완결판 - 초급편
안녕하세요?
오늘은 React에서 Form 핸들링으로 가장 유명한 react-hook-form에 대해 알아보겠습니다.
** 목차 **
- 테스트 환경 구축
- React-Hook-Form의 useForm 사용하기
- React-Hook-Form의 Devtool 설치하기
- React-Hook-Form의 onSubmit 핸들러 제어 방법
- 유효성 검증(Validation)
- 에러 메시지 표시하기
- 커스텀 Validation 함수 만들기
1. 테스트 환경 구축
저는 Vite로 Typescript와 TailwindCSS를 이용하겠습니다.
npm create vite@latest react-hook-form-test
cd react-hook-form-test
npm install
npm i react-hook-form
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
tailwind.config.js 파일은 아래와 같이 하시면 됩니다.
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
그리고 App.css 파일은 지워버리고, index.css 파일은 다음과 같이 하시면 됩니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
label {
@apply mb-2 block text-sm font-medium text-gray-900;
}
input {
@apply mb-5 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500;
}
button {
@apply w-full rounded-lg border px-5 py-2.5 text-center text-sm font-medium hover:border-purple-700;
}
p {
@apply text-red-500;
}
}
HTML에 집중하기 위해 기본 label, input, button, p 태그에 대해 tailwindcss로 미리 스타일을 지정했습니다.
이제 App.tsx를 꾸며보겠습니다.
import MyForm from './components/MyForm'
export default function App() {
return (
<div className='flex flex-col p-4'>
<h1 className='text-2xl font-bold text-blue-700'>
React-Hook-Form 사용법 완결판
</h1>
<MyForm />
</div>
)
}
별거 없습니다.
이제 마지막으로 components 폴더에 MyForm.tsx 파일을 만들겠습니다
export default function MyForm() {
return (
<div className='w-full p-4'>
<form>
<div className='w-1/3'>
<label htmlFor='username'>Username</label>
<input type='text' id='username' name='username' />
<label htmlFor='email'>E-mail</label>
<input type='email' id='email' name='email' />
<label htmlFor='password'>Password</label>
<input type='password' id='password' name='password' />
<button>Submit</button>
</div>
</form>
</div>
)
}
"npm run dev"로 개발 서버를 돌리면 아래와 같이 나옵니다.
테스트 환경은 마무리가 되었네요.
2. React-Hook-Form의 useForm 사용하기
react-hook-form이 폼 제어를 위해 제공하는 Hook이 바로 useForm 훅입니다.
리액트에서는 훅도 함수이기 때문에 useForm() 처럼 그냥 실행하면 됩니다.
useForm()을 실행하면 리턴 되는게 객체인데, 우리는 이걸 form이라고 부르겠습니다.
이제 이 form 객체를 디컨스트럭쳐링 해서 react-hook-form 라이브러리가 제공하는 여러 가지 유용한 기능을 사용하면 되는 겁니다.
가장 먼저, react-hook-form과 HTML의 form 요소인 input과 button 등을 연결 시켜주는 함수가 바로 register 함수입니다.
import { useForm } from "react-hook-form";
export default function MyForm() {
const form = useForm();
const { register } = form;
// 한 줄로 쓰려면 아래와 같이 하셔도 됩니다.
// const { register } = useForm();
...
...
}
이제 register 함수를 이용해서 input이나 button 요소를 react-hook-form 라이브러리와 연결하면 되는데요.
register('username') 이렇게 함수를 실행하면 또 객체를 리턴하는데요.
이 객체가 리턴하는 항목을 input 항목과 연결시키면 됩니다.
export default function MyForm() {
const { register } = useForm();
const { name, ref, onChange, onBlur } = register("username");
return (
<div className="w-full p-4">
<form>
<div className="w-1/3">
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
name={name}
ref={ref}
onChange={onChange}
onBlur={onBlur}
/>
...
...
}
위와 같이 우리가 register로 만든 "usename" 부분을 input 태그와 연결시켰습니다.
그런데, 정말 쓸게 너무 많죠.
그래서 react-hook-form은 아래와 같은 간단한 방법을 제공해 줍니다
export default function MyForm() {
const { register } = useForm();
return (
<div className="w-full p-4">
<form>
<div className="w-1/3">
<label htmlFor="username">Username</label>
<input type="text" id="username" {...register("username")} />
...
...
}
{...register("username")}
처럼 register 함수의 첫 번째 인자로 해당 input의 name을 넣는 겁니다.
즉, input 태그에 있던 name="username" 이 부분이 바로 register 함수의 첫 번째 인자가 되는 거죠.
email, password도 마저 작성해 보겠습니다.
<form>
<div className='w-1/3'>
<label htmlFor='username'>Username</label>
<input type='text' id='username' {...register('username')} />
<label htmlFor='email'>E-mail</label>
<input type='email' id='email' {...register('email')} />
<label htmlFor='password'>Password</label>
<input type='password' id='password' {...register('password')} />
<button>Submit</button>
</div>
</form>
어떤가요? register 함수로 정말 깔끔하게 react-hook-form을 form에 적용할 수 있게 되었습니다.
3. React-Hook-Form의 Devtool 설치하기
React-Hook-Form은 Devtool을 제공하는데요.
Form 관리를 일일이 콘솔 창에 출력하지 않고 현재 일어나고 있는 상태를 쉽게 파악할 수 있습니다.
npm i -D @hookform/devtools
그리고, useForm() 이 사용되는 곳에 아래와 같이 Devtool 컴포넌트를 삽입하면 됩니다.
import { useForm } from 'react-hook-form'
import { DevTool } from '@hookform/devtools'
let renderCount = 0
export default function MyForm() {
const { register, control } = useForm()
renderCount++
return (
<div className='w-full p-4'>
<h1 className='mb-5 text-2xl'> Render count : {renderCount / 2}</h1>
<form>
<div className='w-1/3'>
<label htmlFor='username'>Username</label>
<input type='text' id='username' {...register('username')} />
<label htmlFor='email'>E-mail</label>
<input type='email' id='email' {...register('email')} />
<label htmlFor='password'>Password</label>
<input type='password' id='password' {...register('password')} />
<button>Submit</button>
</div>
</form>
<DevTool control={control} />
</div>
)
}
DevTool에는 control이라고 useForm의 컨트롤을 넘겨줘야 합니다.
그래야 DevTool이 useForm을 컨트롤하게 되는 거죠.
브라우저에서 화면 맨 위에 작은 Devtool 표시를 누르면 실행되는데요.
아래 그림과 같이 우리가 register 함수로 등록한 폼의 상태에 대해 여러 가지를 알려줍니다.
그리고 위 코드에 보시면 renderCount라는 변수를 이용해서 리액트가 다시 렌더링 되는 숫자를 한번 표현해 봤습니다
위 그림과 같이 input 필드에 입력하면 Devtool에서 여러 가지 상태를 직접적으로 보여줘서 코드 짤 때 아주 많은 도움이 될 겁니다.
4. React-Hook-Form의 onSubmit 핸들러 제어 방법
이제, 실제 submit 버튼을 눌러 폼을 제출했을 때 React-Hook-Form을 이용해서 어떻게 처리하는지 알아보겠습니다.
보통 리액트에서는 form-submit 처리를 handleSubmit라는 이름의 함수를 만들어서 사용하는데요.
react-hook-form에서도 이 이름의 함수를 사용합니다.
대신 handleSubmit 함수에는 사용자가 작성한 함수를 인자로 넣어줘야 합니다.
const { register, control, handleSubmit } = useForm();
const onSubmit = (data) => {
console.log("Form submitted.", data)
}
renderCount++;
return (
<div className="w-full p-4">
<h1 className="text-2xl mb-5"> Render count : {renderCount / 2}</h1>
<form onSubmit={handleSubmit(onSubmit)}>
...
...
위와 같이 form의 onSubmit 핸들러로 handleSubmit 함수를 지정하고 다시 react-hook-form의 handleSubmit 함수에는 우리가 만든 onSubmit 함수를 지정하면 됩니다
onSubmit 함수에는 인자가 들어갈 수 있는데요.
이 인자에 우리의 data가 들어 있습니다.
한 번 테스트해 보십시오.
위 그림과 같이 아주 잘 나오네요.
여기서 잠깐, 우리는 지금 Typescript를 쓰고 있는데요.
data 부분의 타입을 지정하겠습니다.
import { useForm } from "react-hook-form";
import { DevTool } from "@hookform/devtools";
let renderCount = 0;
type FormValues = {
username: string;
email: string;
password: string;
};
export default function MyForm() {
const { register, control, handleSubmit } = useForm<FormValues>();
const onSubmit = (data: FormValues) => {
console.log("Form submitted.", data);
};
renderCount++;
return (
...
...
)
}
위와 같이 FormValues라는 타입을 만들어서 useForm 함수를 실행할 때도 전달할 수 있습니다.
5. 유효성 검증(Validation)
Form의 유효성 검증으로 React-Hook-Form이 제공하는 방법은 다음과 같습니다.
required
minLength & maxLength
min & max
pattern
이렇게 6개를 제공하는데요.
각각 이름 그대로의 역할을 합니다. patter은 RegEx에서의 그 패턴입니다.
React-Hook-Form에서 유효성 검증(validation)을 하려면 register 함수의 두 번째 인자로 들어갈 객체 안에 넣어주면 됩니다.
<form onSubmit={handleSubmit(onSubmit)} noValidate>
먼저, 위와 같이 form에 noValidate 옵션을 주어서 HTML 기본적으로 유효성 검증하는 기능을 끄겠습니다.
이렇게 하면 유효성 검증은 오로지 React-Hook-Form이 담당하게 됩니다.
required 유효성 검증을 시현해 보겠습니다.
<input
type='text'
id='username'
{...register('username', {
required: true,
})}
/>
위와 같이 register 함수 안에 넣는 인자 중 에 두 번째 인자로 객체를 넣어주고 그 안에 required 항목을 넣어주면 됩니다.
위와 같이 해도 되는데요. 그러면 아래 그림처럼 ERROR 부분이 단순히 required 이라고만 나옵니다.
우리는 무엇이 문제인지 브라우저에 보여줘야 하는데요.
그래야 사용자가 뭐가 문제인지 바로 알 수 있으니까요.
아래와 같은 사용 방법을 더 선호합니다.
<input
type='text'
id='username'
{...register('username', {
required: 'Username is required.',
})}
/>
이렇게 되면 아래 그림과 같이 message 라는게 생기게 되는데요.
그리고,
required: 'Username is required.'
위 코드는 아래 코드의 축약 버전입니다.
required: {
value : true,
message: "Username is required."
}
축약 버전을 쓰는 게 훨씬 단순하니까 꼭 축약버전으로 써 주십시오.
email 부분은 RegEx의 패턴을 이용해야 하는데요.
<input
type='email'
id='email'
{...register('email', {
pattern: {
value:
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
message: 'Email is invalid.',
},
})}
/>
pattern은 축약 버전보다 value-message 방식으로 써야 합니다.
그리고 password는 minLength 8로 지정해 보겠습니다.
<input
type='password'
id='password'
{...register('password', {
minLength: {
value: 8,
message: 'Password must be at least 8 characters.',
},
})}
/>
참고로, min은 입력값의 최소값, 즉 숫자를 입력할 경우 입력할 숫자의 최소값을 지정할 수 있습니다.
max는 입력값의 최대값, 즉 숫자를 입력할 경우 입력할 숫자의 최대값을 지정할 수 있습니다.
이제, 모든 유효성 검증 로직이 완성되었습니다.
테스트 결과를 볼까요?
그런데, DevTool에 나타난다고 좋은 게 아닙니다.
사용자가 봐야 하는 거죠.
그럼, error 메시지를 어떻게 화면에 보여줄까요?
6. 에러 메시지 표시하기
에러 메시지를 표시하기 위해 React-Hook-Form에서 제공해 주는 게 바로 formState 객체인데요.
실제는 formState 안에 있는 errors 객체가 최종적으로 우리가 사용하는 겁니다.
const {
register,
control,
handleSubmit,
formState
} = useForm<FormValues>();
const { errors } = formState;
축약 버전으로 쓰려면 아래와 같이 하시면 됩니다.
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>();
이제 form 태그에 p 태그를 덧붙혀 보겠습니다.
<p>{errors.username?.message}</p>
<p>{errors.email?.message}</p>
<p>{errors.password?.message}</p>
위와 같은 식으로 p 태그를 만들고 각각의 input 태그 다음에 위치시켜 주면 됩니다.
에러 메시지도 위와 같이 정상 작동합니다.
7. 커스텀 Validation 함수 만들기
React-Hook-Form이 제공하는 유효성 검증 방법 말고 개발자가 직접 Validation을 만들 수 있는데요.
바로 register 함수에서 두 번째 인자로 전달했던 그 객체 안에 validate 항목을 추가하면 됩니다
email 부분에 커스텀 validation 로직을 구현해 보겠습니다.
"admin@fly.dev" 라는 이메일로는 가입을 하지 못하게 해 볼까요?
<input
type='email'
id='email'
{...register('email', {
pattern: {
value:
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
message: 'Email is invalid.',
},
validate: fieldValue => {
return fieldValue !== 'admin@fly.dev' || 'You can not use admin@fly.dev!'
},
})}
/>
validate 항목은 함수인데요. 위와 같이 OR(||) 연산자를 사용합니다.
OR 연산자는 앞에 항목이 false이면 뒤에 항목을 참조하니까요?
위 코드처럼 "admini@fly.dev"와 "다르다" 라는게 틀리면 에러 메시지를 리턴하게 됩니다.
위와 같이 에러 메시지가 잘 나옵니다.
커스텀 Validation 함수는 여러 개 만들 수 있는데요.
다음과 같이 만들면 됩니다.
<input type='email' id='email'
{...register('email', {
pattern: {
value:
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
message: 'Email is invalid.',
},
validate: {
noAdmin: fieldValue => {
return (
fieldValue !== 'admin@fly.dev' || 'You can not use admin@fly.dev!'
)
},
noBlackList: fieldValue => {
return (
!fieldValue.endsWith('daum.net') || 'This domain is not supported.'
)
},
},
})}
/>
위 코드에서는 noBlackList 항목으로 두 번째 커스텀 Validation을 넣었습니다.
즉, 이메일이 "daum.net"으로 끝나면 안 된다는 거죠.
실행 결과를 볼까요?
위와 같이 아주 잘 작동하네요.
지금까지 React-Hook-Form 사용법의 초급편을 살펴보았습니다.
다음 시간에는 고급편을 알아보겠습니다.
그럼.