[React] React-Hook-Form 으로 폼(Form)을 구현하자 ‼️


왜 React-Hook-Form 사용을 결정하게 되었는지 ? 👇🏻
저번 프로젝트에서도 반려동물 정보 입력 페이지를 담당 했었는데 그때는 리액트 훅 폼을 사용하지 않았다..!
그러다보니 모든 입력 필드를 useState
로 관리했고 코드도 어마무시하게 길어졌다. (입력 + 수정 + 추가) 페이지 코드가 모두 260줄이 넘는 이슈…!! 이게 웃긴게 그 당시 하면서 느꼈지만 입력 + 수정 + 추가 모두 같은 폼을 사용하고 있다.(+같은 코드)
그때도 그랬지만 지금 생각해보면 말이 더 안 된다…🙂↔️(부정)
추가적으로 에러랑 예외처리가 힘들었다. 유효성 검사는 petValidation.js
파일을 utils
로 분리해둬서 사용했고 에러를 처리하는 방식은 조건문 또는 별도 상태로 에러 추적해줬다..😾
비효율이적이다.. 굉장히..엄청나게… 🤦🏻♀️
이런 불편함을 겪고
반드시 이번에는 React-Hook-Form
을 사용해서 폼을 구현하고 싶다라는 생각을 갖게 되었고 다행히 팀원도 같은 생각을 갖고 있어서 사용하기로 결정했다.
🧩 React-Hook-Form
코드를 바로 설명하기보다는 먼저 React Hook Form이 무엇인지 그리고 어떻게 사용하는지 간단히 소개한 뒤 실제 구현 코드를 보여주는 것이 더 이해하기 쉬울 것 같아 이 순서로 작성하겠다.!
React Hook Form은 React에서 폼을 간편하고 효율적으로 다룰 수 있게 도와주는 라이브러리이다.
React의 기본 폼 처리 방식은 대부분 useState
를 활용해 상태를 관리하는데 이 방식은 입력 필드가 많아질수록 코드가 길어지고 매 입력마다 컴포넌트가 리렌더링되어 성능 문제가 발생할 수 있다.
하지만 React Hook Form은 "비제어 컴포넌트(uncontrolled components)" 기반으로 동작해서 최소한의 리렌더링으로 성능을 최적화 해주는 장점이 있다. 또한 유효성 검사와 에러 처리 그리고 서브밋 관리까지 아주 간단하게 할 수 있다는 장점이 있다👍🏻
🛠️ 주요 특징
입력 필드를
register
함수 하나로 연결useForm()
으로 폼 상태, 에러, 유효성 검사 관리선언형 유효성 검사 (
required
)내부적으로
ref
를 활용해 값과 상태를 추적하기 때문에 입력할 때마다 리렌더링되지 않는다.
// useState로 관리할 경우
<input value={value} onChange={(e) => setValue(e.target.value)} />
// react-hook-form의 register
<input {...register("name")} />
💡 사용 방법
설치방법
//npm
npm install react-hook-form
//yarn
yarn add react-hook-form
기본 사용법
- 가장 기본적인 형태는
useForm
과register
를 사용하는 방식이다.
const { register, handleSubmit, formState: { errors } } = useForm();
<input {...register("name", { required: "이름은 필수입니다" })} />
register
를 통해 인풋에 바로 연결하면 되는데 이게 정말 간단하다. 유효성 검사도 객체 형태로 같이 전달하면 된다.
그런데 onChange 안에 로직이 필요하다면 register
만으로는 부족하다.
그래서 등장한 것이 useController
✨
import { Controller } from 'react-hook-form'
const { control } = useForm();
const { field } = useController({
name: "username",
control,
rules: { required: "유저 이름은 필수입니다" }
});
<input
{...field}
onChange={(e) => {
console.log("입력된 값:", e.target.value);
field.onChange(e); // 기존 연결 유지
}}
/>
여기서 field.onChange
를 꼭 호출해줘야 React Hook Form과의 연결이 유지된다.
이 외에도 React-Hook-Form
특징이 있는데
watch()
로 실시간 값 추적 가능하다특정 필드의 값을 실시간으로 감지할 수 있다.
ex) 비밀번호 확인, 조건부 렌더링, 디버깅 등
// 'name' 필드 값이 바뀔 때마다 최신 값 반환
const name = watch("name");
useWatch
– 특정 필드값 실시간으로 감시watch()
는 전체 rerender를 유발하지만,useWatch()
는 선택된 필드만 감시하며 리렌더링도 최소화된다.
const name = useWatch({ control, name: "name" });
.
reset
,setValue
,trigger
등의 유틸 함수(커스텀 버튼이나 외부 이벤트로 폼 상태를 조작할 때 꼭 필요하다)reset()
– 폼 초기화setValue()
– 특정 필드 값 수동 설정trigger()
– 특정 필드 또는 전체 폼의 유효성 검사 실행
Controller
vsuseController
차이 : 기능은 같지만 사용 방식이 다르다Controller
: 컴포넌트 형태- JSX 안에서 사용
useController
: 훅 형태- 로직 안에서 사용
// Controller 방식
<Controller
name="email"
control={control}
render={({ field }) => <input {...field} />}
/>
// useController 방식
const { field } = useController({ name: "email", control });
<input {...field} />
✅ 직접 사용해보자
나는 이번에 우리 프로젝트에서 메인 기능인 구인 & 구직 & 자유게시판 글 등록 폼 구현을 맡았다.
입력 해야 하는 내용들이 적지 않다.
(나는 현재 contents
부분에 editor
를 사용하고 있다)
자유게시판
- useForm 초기 세팅
const WriteCommunityPostForm = () => {
const {
register,
handleSubmit,
reset,
setValue,
control,
formState: { errors },
} = useForm({
defaultValues: {
nickname: '',
title: '',
text: EditorState.createEmpty(),
},
})
register
: 일반 input을 RHF에 등록할 때 사용handleSubmit
: 유효성 검사 후 폼 제출을 처리reset
: 폼 초기화setValue
: 특정 필드 값을 프로그래밍적으로 설정(우리는 자유게시판에서 닉네임 값을 수정할 수 있어서 넣어줌)control
:Controller
를 사용할 때 필요errors
: 각 필드의 유효성 검사 에러 정보
- onSubmit: 글 작성 시 처리 로직
const onSubmit = async (data) => {
const rawHtml = draftToHtml(convertToRaw(data.text.getCurrentContent()))
const htmlText = DOMPurify.sanitize(rawHtml)
const postData = {
nickname: data.nickname,
title: data.title,
text: htmlText,
}
}
DOMPurify.sanitize
: HTML 내 악성 스크립트 제거convertToRaw
→draftToHtml
: HTML로 변환
폼 구조 (렌더링 부분)
1) 닉네임 입력
<JobInputBox {...register('nickname', { required: true })} placeholder='닉네임을 입력하세요' /> {errors.nickname && <span>닉네임은 필수입니다</span>}
register
로 닉네임 input을 등록required: true
유효성 검사만약 required: false이면 유효성 검사를 안 한다!
react-hook-form
에서useForm()
훅을 사용할 때formState.errors
안에 유효성 검사를 통과하지 못한 필드의 정보가 들어간다.errors.nickname
은nickname
입력 필드에 에러가 존재할 경우에만 값이 존재만약에 입력 폼을 다 채우지 않고 등록을 눌러주면 이런식으로 밑에 에러 text가 나오는 것을 확인할 수 있다.
2) 에디터 (글 내용)
<Controller name='text' control={control} rules={{ required: '내용은 필수입니다', validate: (value) => { const content = value.getCurrentContent() return content.hasText() || '내용은 필수입니다' }, }} render={({ field }) => ( <WriteEditor editorState={field.value} onEditorStateChange={field.onChange} placeholder='내용을 입력하세요' /> )} />
커스텀 컴포넌트는
register
로 직접 제어 불가 →Controller
사용했다register
는 일반적인<input>
,<textarea>
,<select>
같은 기본 HTML 요소에만 직접 사용할 수 있다.나는 현재 editor를 사용하고 있어서 register로 직접 제어가 불가능한 상태이다
🔗 연결해주는 부분
render={({ field }) => (
<WriteEditor
editorState={field.value} // 현재 에디터 상태를 넘겨줌
onEditorStateChange={field.onChange} // 에디터 상태가 바뀔 때 react-hook-form에게 알려줌
placeholder='내용을 입력하세요'
/>
)}
나는 submit-button
이 다른 파일에 있어서 form id
로 연결해주었다.
//WriteCommunityPostForm.jsx
<form id='community-post-form' onSubmit={handleSubmit(onSubmit)}>
//WriteCommunityPost.jsx
<PinkButton label='등록' type='submit' form='community-post-form' />
완성 !! 👍🏻
🌱 배운 점
React Hook Form은 기존의 form 관리 방식을 훨씬 간단하고 효율적으로 만들어준다는 걸 체감했다.
특히register
로 input에 바로 연결하고Controller
를 통해 커스텀 컴포넌트와도 쉽게 연동할 수 있어서 편리했다.Controller
를 사용하면 Draft.js 같은 복잡한 에디터 상태도 리액트 훅폼과 잘 통합할 수 있다. 이를 통해 에디터의 값도 쉽게 관리하고 유효성 검사도 유연하게 구현할 수 있었다.유효성 검사에서
required: true
만으로 필수 입력 체크를 간편하게 처리할 수 있어서 굉장히 편리하다는 점을 배웠다.
✅ 느낀 점
React Hook Form을 처음 접했을 때는
Controller
같은 개념이 조금 어렵게 느껴졌다. 하지만 실제로 구현하면서 점차 구조가 명확해지고 관리가 훨씬 편해져서 매우 만족스러웠다.특히 커스텀 에디터와 같은 복잡한 입력 컴포넌트와 연동할 때 별도의 상태 관리 코드를 줄일 수 있어 개발 효율성이 크게 올랐다.
form 상태와 유효성 관리를 직접 구현할 때 생기는 실수를 줄일 수 있다는 점도 마음에 들었다. 덕분에 코드가 더 깔끔해지고 유지보수가 쉬워졌다.
앞으로 React 프로젝트에서 form이 필요할 때마다 React Hook Form을 적극적으로 활용하고 더 고급 기능들도 공부하고 싶다는 동기부여가 되었다(아직은 기본만 사용할 줄 아는 수준…인 느낌..?!)
Subscribe to my newsletter
Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
