React Hook Form: #1 React Hook Form을 선택한 이유

ε( ε ˙³˙)з ○º·2025년 6월 27일
0
post-thumbnail

Intro

React Hook Form은 실무에서 처음 접하게 되었다. 당시 팀에서 이미 사용 중이었고, 유지보수와 고도화 작업을 맡으며 가볍게 활용하기 시작했다. 폼 관리가 간편하고, 다양한 기능을 제공해 편리하다고 생각하며 사용하고 있었다.

이후 복잡한 신규 폼을 개발하게 되면서 자연스럽게 React Hook Form을 선택했고, 본격적으로 기능을 깊게 사용하기 시작했다.

초기에는 기존 사용 방식을 따라가면 충분할 것이라 생각했지만, 폼이 점점 복잡해지고 validation과 다이나믹 필드가 추가되면서 예상하지 못한 문제들을 하나씩 마주하게 되었다.

이 과정에서 React Hook Form이 어떻게 동작하는지, 왜 이런 구조를 권장하는지 더 깊이 이해할 필요를 느꼈다. (물론 당시 일정상 충분히 살펴보지는 못했다. 😢)

이번 시리즈에서는 React Hook Form을 실무에 적용하며 경험한 내용을 기술적으로 정리하며 단순한 사용법을 넘어 실무에서 마주한 고민과 문제를 어떻게 해결했는지, 그리고 공식 문서를 기반으로 React Hook Form의 핵심 개념을 더 깊이 이해하고자 한다. 💪🏻


React에서 Form 관리가 어려운 이유

React에서 Form을 다룰 때 가장 기본이 되는 개념이 Controlled Component다.

Controlled Component란?
input의 값을 React의 상태(useState)로 직접 관리하는 컴포넌트

const [name, setName] = useState('');
<input value={name} onChange={(e) => setName(e.target.value)} />
  • 여기서 input의 현재 값이 React state인 name 이며 사용자가 입력할 때마다 setName이 호출되고 React는 상태가 바뀌었으니 컴포넌트를 다시 리렌더링한다.

React가 input의 모든 상태를 "컨트롤"하고 있으니까 Controlled Component다.

⚙️ 예제: Controlled 방식으로 구현된 여러개의 input을 가지고 있는 간단한 폼의 예제다.

const [form, setForm] = useState({
  name: '',
  phone: '',
  email: ''
});

<input
  name="name"
  value={form.name}
  onChange={(e) => setForm({ ...form, name: e.target.value })}
/>

<input
  name="phone"
  value={form.phone}
  onChange={(e) => setForm({ ...form, phone: e.target.value })}
/>

<input
  name="email"
  value={form.email}
  onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
  • 필드가 많아질수록 setForm 호출이 복잡해지고 사용자가 한 글자만 입력해도 상태가 변경되며 컴포넌트 전체가 리렌더링된다.

다이나믹 필드가 생기면 useState 구조를 매번 확장해야 한다.

React에서 Form 데이터를 다룰 때 대부분 Controlled Component를 사용한다.
Controlled 방식은 input의 상태를 useState로 관리하면서, React가 input의 값을 직접 제어하는 구조다.


React Hook Form은 어떻게 해결했을까?

React Hook Form은 기존 Controlled 방식의 한계를 명확히 짚고 Uncontrolled Component를 기본으로 사용한다.

Uncontrolled Component?
Uncontrolled Component는 input의 상태를 React가 직접 관리하지 않고, input DOM 자체에서 값을 관리하는 방식이다. 즉, React는 input의 값을 상태로 들고 있지 않고, 필요할 때 ref를 통해 input의 값을 가져오는 구조다.

<input {...register('username')} />
  • Controlled 방식은 사용자가 입력할 때마다 React state를 업데이트하고 컴포넌트가 리렌더링된다. Uncontrolled 방식은 브라우저가 input의 값을 스스로 관리하며, React는 단순히 관찰만 하기 때문에 입력 시 리렌더링이 발생하지 않는다.

🔍 React Hook Form 공식 FAQ 페이지에서는 다음과 같이 설명하고 있다.

✔️ React Hook Form은 input의 값을 브라우저가 직접 관리하도록 두고, React는 ref를 통해 값을 추적하는 구조로 설계되어 있다. 이로 인해 입력 시 리렌더링이 대폭 줄어드는 퍼포먼스 최적화 효과를 가져온다.


React Hook Form의 최적화를 위한 내부 동작 원리

React Hook Form은 Uncontrolled 컴포넌트를 기반으로 리렌더링을 최소화하는 구조를 설계했다. 폼 입력 시 React state를 직접 갱신하지 않고, 필요한 경우에만 정밀하게 상태를 구독하며 퍼포먼스를 최적화한다.

1. register로 input 연결: 리렌더링 최소화

React Hook Form의 register는 Uncontrolled 컴포넌트를 생성하는 핵심 API다. register는 input 요소를 React Hook Form의 내부 레지스트리에 등록하면서,
자동으로 ref를 바인딩하고 input의 값을 React가 직접 관리하지 않는 구조를 만든다.

<input {...register('username')} />

register를 사용하면 input 요소의 값은 React state와 연결되지 않고 사용자가 값을 입력하더라도 React는 상태를 갱신하지 않으며, input DOM의 현재 값을 React Hook Form이 직접 추적한다.

이 구조를 통해 입력 시 리렌더링이 발생하지 않는 Uncontrolled 컴포넌트가 생성된다.

👀 주의할 점
register를 통해 연결된 input은 React Hook Form이 Uncontrolled 방식으로 추적하기 때문에, input의 defaultValue를 초기화하려면 반드시 useFormdefaultValues 옵션을 사용해야 한다.

const { register } = useForm({
  defaultValues: { username: '예림' }
});

input에 valueonChange를 직접 바인딩하면 Uncontrolled 컴포넌트가 Controlled로 전환되어 React Hook Form의 퍼포먼스 이점이 사라질 수 있다.
반드시 register를 통해 바인딩해야 한다.

2. formState를 통한 상태 관리: 필요한 시점에만 리렌더링

formState는 React Hook Form이 내부에서 관리하는 폼 상태 메타 데이터다. 대표적으로 isDirty, isValid, touchedFields 등이 있으며 이 값들은 필요한 경우에만 구독하여 업데이트되는 구조로 설계되어 있다.

React Hook Form은 formState 객체를 Proxy 객체로 감싸 리렌더링을 최적화한다. 구조적으로 컴포넌트가 구독하지 않은 속성은 변경되더라도 리렌더링을 트리거하지 않는다.

Proxy 객체란
React Hook Form 공식 문서

“Returned formState is wrapped with a Proxy to improve render performance and skip extra computation if specific state is not subscribed.”

  • Proxy는 JavaScript 문법 중 하나로 객체의 속성 접근을 가로채서 동작을 조작할 수 있는 기능이다.

React Hook Form은 formState를 Proxy로 감싸,
컴포넌트가 실제로 접근한 속성만 ‘구독 (subscribe)’하도록 만든다.

const { formState: { isDirty } } = useForm();

위와 같이 작성하면 해당 컴포넌트는 isDirty만 구독하고, isValid, errors와 같은 다른 속성은 구독하지 않는다. 따라서 구독하지 않은 상태가 변경되더라도 컴포넌트는 불필요하게 리렌더링되지 않는다.

이러한 구독 방식으로 필요한 상태만 정교하게 추적하며 폼의 퍼포먼스를 최적화할 수 있다.

👀 formState 구독 시 주의할 점
formState는 반드시 컴포넌트가 렌더링될 때 구조 분해로 참조해야 구독이 활성화된다. 단일 속성으로 직접 접근하면 구독이 누락될 수 있으므로 아래와 같이 작성하는 것이 안정적이다.

const { isDirty, isValid } = formState; // 권장 방식
const isDirty = formState.isDirty; // 비권장 (구독 누락 가능)

더 세밀하게 구독하고 싶을 경우 useFormStateuseWatch를 사용하면 특정 필드나 상태만 구독할 수 있다.

const { isDirty } = useFormState({ control });
const username = useWatch({ name: 'username' });

3. useWatch로 필드 구독: 필드 단위 리렌더링 최적화

useWatch는 React Hook Form에서 특정 input 필드만 선택적으로 구독할 수 있는 훅이다. 기본적으로 Controlled 방식은 input의 값을 React 상태로 관리하기 때문에, 폼에 하나의 input 값이 바뀌어도 전체 컴포넌트가 리렌더링되는 경우가 많다.

useWatch를 사용하면 폼 전체가 리렌더링되지 않고, 해당 필드를 구독한 컴포넌트만 부분적으로 리렌더링된다.

const username = useWatch({ name: 'username' });
  • 이 코드는 username 필드만 추적하며 다른 필드 값이 변경되더라도 해당 컴포넌트는 영향을 받지 않는다.

🔍 useWatch의 동작 원리
useWatch는 React Hook Form 내부의 subscription model을 활용해 선택한 필드의 값이 바뀔 때만 해당 컴포넌트를 업데이트한다. 이를 통해 Controlled 방식에서 발생하는 불필요한 리렌더링을 구조적으로 차단할 수 있다.

const MyComponent = () => {
  const { control } = useForm();
  
  // username 필드만 구독
  const username = useWatch({ control, name: 'username' });
  
  return <div>현재 사용자명: {username}</div>;
};
  • 위 코드에서는 username 필드만 구독하며, 다른 input이 바뀌어도 이 컴포넌트는 리렌더링되지 않는다.

👀 주의할 점
useWatch는 반드시 control 객체를 전달해야 정확하게 동작한다.control을 넘기지 않으면 기본적으로 context에서 찾지만 복잡한 폼 구조에서는 의도하지 않은 리렌더링이 발생할 수 있다.

// ✅ 권장
const username = useWatch({ control, name: 'username' });
// ❌ 비권장
const username = useWatch({ name: 'username' }); // context에 의존 (복잡한 폼에서는 위험)

useWatch는 복잡한 컴포넌트 구조에서 필드 단위 퍼포먼스 최적화에 가장 효과적이지만, 너무 과도하게 쪼개면 오히려 관리가 어려울 수 있으니 리렌더링 병목이 발생하는 컴포넌트 위주로 사용하는 것이 적절하다.

😲 formState는 컴포넌트가 구독한 폼의 상태 메타데이터를 기준으로 리렌더링을 판단하며, useWatch는 구독한 input 필드의 값 변화를 기준으로 리렌더링을 판단한다.


React Hook Form 언제, 무엇을 써야 할까?

React Hook Form은 퍼포먼스를 최적화하기 위한 다양한 API를 제공한다. 하지만 모든 API를 항상 함께 사용할 필요는 없으며 상황에 따라 적절히 선택하는 것이 더 효율적인 방법이다.

📝 단순 입력 폼: register만 사용해도 충분한 경우

  • 필드 수가 적고, 단순히 input 값만 관리하는 경우
  • 기본적인 리렌더링 최소화 구조만 필요할 경우
<input {...register('username')} />

register만으로 Uncontrolled input을 생성하면 퍼포먼스 이점도 충분하며 별도의 상태 추적 없이 빠르게 관리 가능하다.

  • 로그인 폼, 간단한 검색창, 비밀번호 재설정 입력 폼

📝 상태 기반 UI가 필요한 경우: formState 사용

  • 버튼 활성화, 에러 메시지 표시 등 폼의 상태 메타데이터가 필요한 경우
const { formState: { isValid } } = useForm();
  • 버튼 활성화 formState.isValid
  • 수정 여부 감지 formState.isDirty
  • 필드 터치 여부 formState.touchedFields

📝 필드 값 실시간 반영이 필요한 경우: useWatch 사용

  • input의 값 변화에 따라 UI가 즉시 반응해야 하는 경우
const username = useWatch({ control, name: 'username' });
  • 실시간 유효성 검사 결과 보여주기
  • 실시간 미리보기 UI
  • 입력값을 다른 input에 즉시 반영하는 UI

📝 복잡한 폼 / 다이나믹 필드: useFieldArray + useWatch 조합

  • 다중 스텝 폼, 반복 input 그룹, 동적 input 추가/삭제가 필요한 경우
const { fields, append, remove } = useFieldArray({ control, name: 'users' });
  • 다이나믹 항목 추가
  • 반복 입력 폼 컴포넌트
  • 동적으로 생성되는 필드 관리

📝 필드 단위 상태 구독이 필요한 경우: useFormState 사용

  • formState를 더 세밀하게 구독하고 싶을 때
const { isDirty } = useFormState({ control, name: ['username'] });
  • 특정 필드만 상태 실시간 구독
  • 특정 필드의 isDirty, isValid 상태만 구독하고 싶을 때

💭 마무리하며

Uncontrolled 기반의 퍼포먼스 최적화 구조라는 큰 강점 보다는 React Hook Form은 단순히 폼 관리가 편리한 라이브러리로만 사용하고 있었는데,,🙂

Controlled 컴포넌트의 리렌더링 병목을 피하고, 상황에 맞는 API를 적절히 선택하는 것만으로도 복잡한 폼의 퍼포먼스를 충분히 개선할 수 있을 것이라 생각한다. 💪🏻


📚 Reference


이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻

profile
console.log(🐛🔨🧐🍀)

0개의 댓글