체크히어: 폼 성능 개선을 위한 React Hook Form 도입기

nemo0824·2025년 7월 31일
1
post-thumbnail

체크히어: 폼 성능 개선을 위한 React Hook Form 도입기

최근 체크히어 프로젝트에서 기술 스택과 구조 개선을 진행했습니다.
그중 입력 폼 성능 개선을 중점적으로 다뤘으며, 후기를 정리했습니다.

개선 작업 목록

  1. 전역 상태 기반 입력 폼 → React Hook Form 전환
  2. Next.js 14 → 15 업그레이드
  3. React 18 → 19 업그레이드
  4. Recoil → Zustand 상태 관리 전환
  5. Yarn → pnpm 패키지 매니저 전환
  6. ESLint v9 업그레이드 예정

1. 도입 배경: 왜 RHF를 도입했나?

체크히어는 강의 등록/수정 페이지에서
다양한 조건과 반복 구조를 가진 수십 개의 input 필드를 사용하는 프로젝트입니다.
특히 강의 시간표, 요일별 반복, Wi-Fi 설정 등 입력 필드 수가 많고 구조도 복잡했습니다.

입력 필드가 많아지면서 불필요한 리렌더링 문제가 빈번히 발생했고,
기존에는 Recoil을 통해 모든 입력값을 전역 상태로 관리하고 있었기 때문에
필드 하나만 바꿔도 전체 폼이 리렌더링되는 비효율이 있었습니다.

이러한 성능 병목을 해결하기 위해
불필요한 리렌더링을 최소화할 수 있는 React Hook Form으로 구조를 전환하게 되었습니다.

2. 제어 컴포넌트 vs 비제어 컴포넌트

제어 컴포넌트

const [value, setValue] = useState('');
<input value={value} onChange={(e) => setValue(e.target.value)} />
  • React가 value 상태를 직접 관리
  • 입력값이 바뀔 때마다 전체 컴포넌트 리렌더링

비제어 컴포넌트

<input ref={inputRef} defaultValue="hi" />
  • DOM이 value를 직접 관리
  • 필요할 때 ref로 값을 읽어옴 (React state X)

3. React Hook Form의 동작 방식

RHF는 기본적으로 비제어 컴포넌트 기반입니다. 그러나 아래처럼 제어 방식처럼도 동작 가능하며, 구독 기반 렌더링 최적화를 제공합니다.

작동 흐름 요약

const { register, formState: { errors } } = useForm({ mode: 'onChange' });
  1. register() 호출 시 input에 ref, onChange, onBlur 주입
  2. 사용자가 입력 → onChange 이벤트 감지
  3. RHF가 ref.current.value로 실제 DOM 값 읽어옴
  4. 내부적으로 formValues 업데이트
  5. mode가 'onChange'일 경우 → 유효성 검사
  6. 에러나 값이 바뀌면 → 해당 필드를 구독 중인 부분만 리렌더링

이 구조 덕분에 "비제어 기반"이지만 "제어처럼 동작"하며 불필요한 리렌더링을 방지할 수 있습니다.


4. 리렌더링 최적화의 핵심: 구독 시스템

React에서 리렌더링을 유발하는 건 stateprops의 변경이지만, RHF는 자체적으로 구독(subscription) 기반 시스템을 사용합니다.

핵심 개념 요약

구분설명
내부 상태RHF는 formState, errors 등을 일반 객체로 유지 (React state X)
구독 시스템formState.errors.name, useWatch('name')처럼 특정 값에 구독 등록
리렌더링값이 바뀌면, 해당 필드에 한해 forceUpdate강제 리렌더링

즉, input은 리렌더링 되지 않고, 예: formState.errors.name을 사용하는 p 태그만 리렌더링됩니다.


5. 예시 비교

RHF 비제어 컴포넌트 예시

function LoginForm() {
  const { register, formState: { errors } } = useForm({ mode: 'onChange' });

  return (
    <form>
      <input {...register('id', { required: 'ID 필수' })} />
      {errors.id && <p>{errors.id.message}</p>}

      <input {...register('password', { required: 'PW 필수' })} />
      {errors.password && <p>{errors.password.message}</p>}
    </form>
  );
}
구분설명
input비제어, 리렌더링 X
p 태그errors 구독 중 → 변경 시 리렌더링
컴포넌트 전체리렌더링 X

제어 컴포넌트 예시

const [id, setId] = useState('');
<input value={id} onChange={e => setId(e.target.value)} />
구분설명
inputReact가 value 직접 관리
모든 입력state 변경 → 컴포넌트 전체 리렌더링 발생

6. RHF 도입으로 얻은 효과

1. 성능 최적화

  • 수백 개 필드가 있어도 입력값 변경 시 전체 리렌더링 없음
  • 리렌더링 병목 해결

2. 유연한 제어

  • 필요 시 useWatch, Controller를 사용해 제어 컴포넌트처럼도 사용 가능

3. 폼 조작 편의성

  • reset, setValue, getValues, useFieldArray 등 유틸 제공

기존- 전역상태관리 사용

개선- rhf 사용


참고 자료

profile
개발 일기

0개의 댓글