React Hook Form의 lazy evaluation

eeensu·2025년 9월 27일

기타 라이브러리

목록 보기
12/12

개요

흔하게 react hook form을 이용해 폼을 개발하던 중... 이상한 현상을 포착했다.
useForm 의 리턴값인 form을 props로 넘겨 받고, form.formState.isDirty 값을 update submit 함수 제출 전에 조건을 걸어서, 폼을 수정하지 않았으면 submit을 방지하려고 하였다.

 const onSumbit = () => {
    if (!form.formState.isDirty) return;

  	... 제출 함수 로직 ...
  };

그런데 놀랍게도 폼을 건들이지 않았는데도 (isDirty = false), 함수의 제출은 잘 작동했다. 뭐지? 싶어서, 컴포넌트 안에 console.log(form.formStatus.isDirty) 를 출력해보았다. 그런데 그제서야 onSubmit이 의도대로 잘 작동하는 것이었다...! 뭐지? 싶어서 다시 console.log 를 지워보니, 원상태로 돌아가 다시 작동하지 않았다.

아무래도 react hook form 내부 상태의 업데이트 로직에 원인이 있는 듯 하여 깊게 탐구해보고자 한다.




React Hook Form의 formState의 특성

formState는 Proxy로 감싸져 있어, 컴포넌트의 렌더링 과정에서 특정 상태값 (isDirty, isSubmitted) 등을 "구독"하지 않으면 해당 값의 변경이 일어나도 리렌더링을 트리거하지 않기 때문이다. 왜 이러한 현상이 발생할까?

React Hook Form의 가장 큰 장점 중 하나는 불필요한 리렌더링을 최소화하여 성능을 극대화 하는것이다. 만약 입력 필드가 20개인 폼에서 키를 한번 누를 때마다 전체 폼 컴포넌트가 리렌더링 된다면 매우 비효율적일 것이다. React Hook Form은 이 문제를 해결하기 위해 Proxy 패턴을 사용하여 formState를 관리한다.

console.log(formState.isDirty) 를 사용하면 잘 작동하는지, 원리를 살펴보자.

  1. formState는 그냥 객체가 아니다.formState는 내부적으로 js의 proxy 객체이다. 이 Proxy는 어떤 속성에 접근하지는 감시하는 감시자 역할을 한다.

  2. 렌더링 중 접근은 "구독" 을 의미한다. 컴포넌트가 렌더링될 때, jsx 내부나 useEffect 등에서 formState.isDirty 와 같은 특정 값에 접근하면, React Hook Form의 Proxy는 아, 이 컴포넌트는 isDirty 값의 변화에 관심이 있구나 라고 인지하고 해당 컴포넌트를 isDirty 상태의 구독자로 등록한다.

  3. 구독자에게만 알림 발송
    이후 사용자가 폼 필드를 수정하여 isDirty 상태가 false에서 true로 바뀌면, React Hook Form은 오직 isDirty를 "구독" 한 컴포넌트에게만 상태가 바뀌었으니 리렌더링하라는 신호를 보낸다.

공식문서에서도 확인할 수 있다.

formState is wrapped in a Proxy to reduce rerenders. You must explicitly read the formState fields you want to track so that React Hook Form knows to subscribe to them.



어? 나는 내 onSubmit 함수 안에 isDirty를 넣었는데??

  1. 나는 분명 onSubmit 함수안에 조건문으로 if (!form.formState.isDirty) return; 를 넣었다. 이러면 분명 구독한 것 아니야? 근데 왜 안돼? 라고 생각할 수 있고, 나역시 그렇게 생각했다...

  2. 하지만 중요한 것은, onSubmit 버튼을 클릭했을 때만 동작하는 함수라는 것이다. 즉, 렌더링 과정에서 form.formState.isDirty는 아무도 접근하지 않는다. 즉, 저 onSubmit 을 사용하는 컴포넌트는 isDirty를 구독하진 않는것이다.

  3. 즉, onSubmit 함수는 컴포넌트의 최초 렌더링 시점에 생성되고, 그때의 isDirty 는 당연히 초기값인 false 값을 가진 forState를 클로저로 가진다.

  4. 그렇기에 사용자가 입력 필드를 수정해도, isDirty 를 구독하지 않았기 때문에 리렌더링을 트리거하지 않고, submit 버튼을 눌러도 여전히 isDirtyfalse가 되게 되는것이다.


그리고, console.log(formState.isDirty) 가 있었을 때 잘 작동했던 이유는, onSubmit 바깥의, 그리고 컴포넌트 안에 console.log(formState.isDirty) 구문이 있었고, 이 뜻은 해당 컴포넌트가 isDirty 를 구독하여 값이 변경할 때 마다 리렌더링을 한다는 뜻이었다. 그러기에 onSubmit 함수도 새로 생성되기에, isDirty 가 최신의 값을 잘 반영한 formState를 클로저로 갖는 것이다.




해결방법이 그래서 뭐지..?

사실 해결방법은 간단하다. 위의 내용에서 설명했듯이, 접근하고 싶은 상태를 "구독" 하면 된다.

  1. 만약 form 을 props로 넘겨받았다면 아래와 같이 구조분해할당을 해주면 된다

    const { formState: { isDirty, isValid } } = form;

  2. 혹은 useFormState 훅을 사용해여 구독하는 방법도 있다.

    import { useFormState } from 'react-hook-form';
    
    const { isDirty } = useFormState({ control: form.control });

요약하자면, RHF의 formStateProxy이며, 성능을 위해 렌더링 시 구독된 상태에 대해서만 리렌더링을 유발한다. console.log는 렌더링 중에 isDirty 값을 읽어 우연히 구독을 만들었던 것이다. 이에 가장 좋은 해결책은 const { formState: { isDirty } } = form; 처럼 구조분해 할당을 통해 명시적으로 상태를 구독하고, disabled 속성처럼 UI에 직접 반영하는 것이다.

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글