React Hook Form
은 실무에서 처음 접하게 되었다. 당시 팀에서 이미 사용 중이었고, 유지보수와 고도화 작업을 맡으며 가볍게 활용하기 시작했다. 폼 관리가 간편하고, 다양한 기능을 제공해 편리하다고 생각하며 사용하고 있었다.
이후 복잡한 신규 폼을 개발하게 되면서 자연스럽게 React Hook Form
을 선택했고, 본격적으로 기능을 깊게 사용하기 시작했다.
초기에는 기존 사용 방식을 따라가면 충분할 것이라 생각했지만, 폼이 점점 복잡해지고 validation
과 다이나믹 필드가 추가되면서 예상하지 못한 문제들을 하나씩 마주하게 되었다.
이 과정에서 React Hook Form이 어떻게 동작하는지, 왜 이런 구조를 권장하는지 더 깊이 이해할 필요를 느꼈다. (물론 당시 일정상 충분히 살펴보지는 못했다. 😢)
이번 시리즈에서는 React Hook Form을 실무에 적용하며 경험한 내용을 기술적으로 정리하며 단순한 사용법을 넘어 실무에서 마주한 고민과 문제를 어떻게 해결했는지, 그리고 공식 문서를 기반으로 React Hook 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은 기존 Controlled 방식의 한계를 명확히 짚고 Uncontrolled Component를 기본으로 사용한다.
Uncontrolled Component?
Uncontrolled Component는 input의 상태를 React가 직접 관리하지 않고, input DOM 자체에서 값을 관리하는 방식이다. 즉, React는 input의 값을 상태로 들고 있지 않고, 필요할 때 ref를 통해 input의 값을 가져오는 구조다.
<input {...register('username')} />
🔍 React Hook Form 공식 FAQ 페이지에서는 다음과 같이 설명하고 있다.
✔️ React Hook Form은 input의 값을 브라우저가 직접 관리하도록 두고, React는 ref를 통해 값을 추적하는 구조로 설계되어 있다. 이로 인해 입력 시 리렌더링이 대폭 줄어드는 퍼포먼스 최적화 효과를 가져온다.
React Hook Form은 Uncontrolled 컴포넌트를 기반으로 리렌더링을 최소화하는 구조를 설계했다. 폼 입력 시 React state를 직접 갱신하지 않고, 필요한 경우에만 정밀하게 상태를 구독하며 퍼포먼스를 최적화한다.
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
를 초기화하려면 반드시useForm
의defaultValues
옵션을 사용해야 한다.const { register } = useForm({ defaultValues: { username: '예림' } });
input에
value
와onChange
를 직접 바인딩하면 Uncontrolled 컴포넌트가 Controlled로 전환되어 React Hook Form의 퍼포먼스 이점이 사라질 수 있다.
반드시 register를 통해 바인딩해야 한다.
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; // 비권장 (구독 누락 가능)
더 세밀하게 구독하고 싶을 경우
useFormState
나useWatch
를 사용하면 특정 필드나 상태만 구독할 수 있다.const { isDirty } = useFormState({ control }); const username = useWatch({ name: 'username' });
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은 퍼포먼스를 최적화하기 위한 다양한 API를 제공한다. 하지만 모든 API를 항상 함께 사용할 필요는 없으며 상황에 따라 적절히 선택하는 것이 더 효율적인 방법이다.
<input {...register('username')} />
register만으로 Uncontrolled input을 생성하면 퍼포먼스 이점도 충분하며 별도의 상태 추적 없이 빠르게 관리 가능하다.
formState
사용const { formState: { isValid } } = useForm();
formState.isValid
formState.isDirty
formState.touchedFields
useWatch
사용const username = useWatch({ control, name: 'username' });
useFieldArray
+ useWatch
조합const { fields, append, remove } = useFieldArray({ control, name: 'users' });
const { isDirty } = useFormState({ control, name: ['username'] });
Uncontrolled 기반의 퍼포먼스 최적화 구조라는 큰 강점 보다는 React Hook Form은 단순히 폼 관리가 편리한 라이브러리로만 사용하고 있었는데,,🙂
Controlled 컴포넌트의 리렌더링 병목을 피하고, 상황에 맞는 API를 적절히 선택하는 것만으로도 복잡한 폼의 퍼포먼스를 충분히 개선할 수 있을 것이라 생각한다. 💪🏻
이 글은 공식 문서를 기반으로 내용을 정리한 포스팅입니다.
혹시 내용 중 틀린 부분이나 보완할 부분이 있다면 댓글로 남겨주시면 감사하겠습니다. 🙏🏻