Navigation

React Hook Form에서 입력과 화면 상태 나누기

2023년 폼 리팩터링을 정리한 원문에서는 입력값마다 useState와 변경 함수를 두던 코드를 React Hook Form으로 옮기는 방법을 다뤘습니다. 이후 Culturepass의 로그인과 인증 화면에서도 React Hook Form을 사용했습니다.

다시 읽으며 “비제어 방식이므로 항상 더 빠르다”는 설명은 빼고 입력값, 화면 상태와 서버 결과를 어디에 둘지 정리했습니다. 아래 API 설명은 React Hook Form v7을 기준으로 했습니다.

먼저 폼의 값과 화면 상태를 나눴습니다

Culturepass 로그인은 아이디, 비밀번호와 자동 로그인 선택값을 defaultValues로 두었습니다. 비밀번호를 보여 줄지는 별도 React state였습니다. 제출할 값과 입력을 보여 주는 방식의 수명이 달랐기 때문입니다.

값둘 곳이유
아이디, 비밀번호, 자동 로그인폼 상태검증과 제출의 대상
비밀번호 표시 여부화면의 지역 state제출 값이 아닌 표현 상태
로그인한 사용자와 access token인증 상태로그인 완료 이후의 서비스 상태
잘못된 계정 정보 응답필드 오류 또는 요청 오류입력 검증과 서버 판단을 구분

입력값을 폼과 지역 state 양쪽에 보관하면 지우기, 초기화와 제출 때 두 값을 맞춰야 합니다. 특별한 이유가 없다면 한 입력값의 기준은 하나로 두는 편이 읽기 쉽습니다.

register와 Controller의 선택

기본 입력 요소는 register로 연결할 수 있습니다. 외부 UI나 값 전달 방식이 다른 제어형 입력은 Controller로 맞출 수 있습니다. 실제 로그인 화면은 Controller에서 입력, 지우기 버튼과 필드 오류 표시를 함께 조립했습니다. 이것이 모든 입력에 Controller를 써야 한다는 뜻은 아닙니다. register와 useForm, Controller

다음은 실제 구조를 줄이고 이름을 일반화한 예시입니다.

입력과 오류를 한 계약으로 연결tsx
const form = useForm<LoginFields>({
  defaultValues: { loginId: '', password: '', autoLogin: true },
})

<Controller
  name="loginId"
  control={form.control}
  rules={{ required: '아이디를 입력해 주세요.' }}
  render={({ field, fieldState }) => (
    <label>
      아이디
      <input {...field} aria-invalid={fieldState.invalid} />
      {fieldState.error && <span>{fieldState.error.message}</span>}
    </label>
  )}
/>

제어형 입력을 연결할 때는 value와 onChange만 살피지 않습니다. onBlur, 이름과 ref도 입력의 touched 상태나 오류 시 초점 이동에 관계합니다. 같은 입력을 register와 Controller로 이중 등록하지 않도록 확인합니다.

서버가 판단한 오류를 입력에 연결했습니다

빈 값이나 형식은 제출 전 검증할 수 있어도 아이디와 비밀번호가 맞는지는 서버 응답이 필요했습니다. 실제 화면은 응답 코드에 따라 setError로 입력 오류를 표시하고 잠금 상태에서는 별도 Activity로 이동했습니다.

flowchart LR
    Input["입력"] --> Validation["폼 검증"]
    Validation -->|통과| Request["로그인 요청"]
    Request --> Success["인증 상태와 화면 이동"]
    Request --> FieldError["계정 정보 오류 → setError"]
    Request --> Policy["계정 잠금 → 안내 화면"]
    Request --> Failure["통신과 시스템 오류 → 재시도 안내"]

모든 실패를 비밀번호 오류로 보이면 사용자는 맞는 값을 계속 고치게 됩니다. 입력 오류, 계정 정책과 시스템 오류를 나누는 이유입니다. 오류를 언제 지울지와 중복 제출을 어떻게 막을지도 제출 흐름에서 함께 정해야 합니다.

나중에 도착한 초기값은 별도 문제입니다

수정 화면처럼 서버에서 값을 읽어 오는 경우에는 첫 렌더링의 빈 값과 조회 완료 뒤의 값을 구분해야 합니다. defaultValues는 기준 초기값으로 캐시되므로 props가 달라졌다는 이유만으로 매번 새 초기값으로 바뀌는 것으로 가정하면 안 됩니다. 값을 다시 적용할 때는 reset이나 해당 버전의 반응형 values 정책을 확인합니다. 초기값, reset

재조회마다 무조건 reset하면 사용자가 입력 중인 값이 덮일 수 있습니다. 다른 항목으로 이동했는지, 처음 조회가 끝났는지, 변경된 필드를 유지할지에 따라 적용 시점을 정해야 합니다. 이 부분은 로그인 구현에 없던 수정 폼의 설계 고려사항으로 덧붙였습니다.

구독 범위로 렌더링을 살핍니다

원문에서 정리한 watch와 useWatch의 차이도 구독 위치의 문제로 다시 읽었습니다. 화면 최상위에서 폼 전체를 구독하면 그곳의 갱신 범위가 커질 수 있습니다. 필요한 하위 영역에서 useWatch로 필요한 필드를 구독하는 방법을 검토할 수 있습니다. 다만 부모의 갱신 등 다른 렌더링 원인까지 사라지는 것은 아닙니다. useWatch

폼 라이브러리를 바꿨다는 사실만으로 개선율을 만들지 않았습니다. 같은 입력 동작에서 어떤 컴포넌트가 다시 계산되는지 확인해야 결과를 설명할 수 있습니다. 측정할 기준은 React 렌더링과 성능 기록에 이어서 정리했습니다.