React Hook Form에 첫 기여하기
![]()
문제 발견
react-hook-form에 기여를 하고 싶어 이슈를 들락날락 하다가 찾았습니다. React Hook Form 7.63.0 버전부터 발생한 버그였는데, reset() 메서드를 호출한 직후 watch() 메서드가 undefined를 반환하는 문제였습니다.
정상적인 상황이라면 reset()으로 폼 값을 초기화한 후 watch()를 호출하면 초기화된 값이 반환되어야 하는데, 특정 조건에서 undefined가 반환됐습니다.
특히 이 문제는 사용자가 필드와 상호작용하기 전에 reset()을 호출했을 때 발생했습니다.
원인 분석
React Hook Form의 내부 구조를 깊이 분석한 결과, 문제의 근본 원인을 찾을 수 있었습니다.
_state.mount의 역할
React Hook Form은 내부적으로 _state.mount라는 플래그를 사용하여 폼이 "마운트"되었는지 추적합니다. 이 플래그는 다음과 같은 역할을 합니다:
- 초기값: false
 - 사용자가 필드와 상호작용(입력, 포커스 등)하면 true로 변경됨
 - watch() 메서드가 어떤 값을 반환할지 결정하는 중요한 조건
 
버그가 발생한 이유
watch() 메서드 내부의 _getWatch 함수는 다음과 같은 로직으로 동작합니다:
// 간소화된 _getWatch 로직
function _getWatch(names, defaultValue) {
  // _state.mount가 false면 defaultValue 반환
  if (!_state.mount) {
    return defaultValue;
  }
 
  // _state.mount가 true면 실제 폼 값 반환
  return getFieldValue(names);
}문제는 reset() 메서드에서 발생했습니다:
- 사용자가 필드와 상호작용하기 전에 reset()을 호출
 - reset()은 내부적으로 폼 값을 업데이트하지만, _state.mount 는 여전히 false
 - 이후 watch()를 호출하면 _getWatch가 _state.mount === false를 확인
 - 업데이트된 폼 값 대신 defaultValue(undefined)를 반환
 
재현 조건
// 버그 재현 코드
const { reset, watch } = useForm({
  defaultValues: { name: "initial" },
});
 
// 사용자가 필드와 상호작용하기 전에 reset 호출
reset({ name: "updated" });
 
// undefined 반환! (기대값: 'updated')
console.log(watch("name"));이 버그는 7.63.0 버전의 리팩토링 과정에서 reset() 함수가 _state.mount를 적절히 업데이트하지 않으면서 발생했습니다.
버그 발생 플로우
flowchart TD
    A[useForm 초기화] --> B[_state.mount = false]
    B --> C{사용자 액션}
    C -->|필드 상호작용 전| D[reset 호출]
    C -->|필드 입력| E[_state.mount = true]
    D --> F[폼 값 업데이트]
    F --> G[_state.mount는 여전히 false]
    G --> H[watch 호출]
    H --> I{_state.mount 체크}
    I -->|false| J[❌ undefined 반환]
    E --> K[watch 호출]
    K --> L{_state.mount 체크}
    L -->|true| M[✅ 실제 값 반환]해결 방법
해결책은 reset() 함수 내부에서 _state.mount의 값을 결정하는 조건부 로직을 수정하는 것이었습니다.
기존 코드 (버그 발생)
// createFormControl.ts의 _reset 함수 내부
_state.mount =
  !_proxyFormState.isValid ||
  !!keepStateOptions.keepIsValid ||
  !!keepStateOptions.keepDirtyValues;기존 로직은 폼의 validation 상태와 keep 옵션에만 의존하고 있었습니다. 이로 인해 reset()으로 값을 설정해도 _state.mount가 false로 남아있는 경우가 발생했습니다.
수정된 코드
_state.mount =
  !_proxyFormState.isValid ||
  !!keepStateOptions.keepIsValid ||
  !!keepStateOptions.keepDirtyValues ||
  (!_options.shouldUnregister && !isEmptyObject(values)); // 이 조건 추가해결책은 간단합니다. 기존 조건에 (!_options.shouldUnregister && !isEmptyObject(values)) 조건을 추가했습니다.
이 조건은 "필드 등록을 해제하지 않으면서 값이 비어있지 않은 경우"를 의미합니다. reset()으로 값을 설정하면 values가 비어있지 않게 되므로, _state.mount가 true로 설정되어 watch()가 올바른 값을 반환하게 됩니다.
테스트 케이스 추가
또한 이 버그가 재발하지 않도록 테스트 케이스도 추가했습니다:
it("should return correct value after reset when _state.mount is false", () => {
  const { watch, reset, getValues } = useForm({
    defaultValues: { name: "initial" },
  });
 
  reset({ name: "updated" });
 
  expect(watch("name")).toBe("updated");
  expect(getValues("name")).toBe("updated");
});리뷰 과정
PR을 올린 후 메인테이너인 @bluebill1049님으로부터 빠른 피드백을 받았습니다.
처음에는 dirty values와 valid state에 대한 잠재적인 리그레션에 대한 우려가 있었습니다. 이는 매우 합리적인 지적이었고, 기존 코드의 조건부 마운트 체크를 유지하면서도 버그를 해결할 수 있도록 솔루션을 개선했습니다.
메인테이너의 빠르고 명확한 피드백 덕분에 금방 문제를 해결할 수 있었고, 결과적으로 PR은 머지됐습니다.
참고 링크: