React Hook Form 핵심 정리

이은지·2024년 1월 21일
7
post-thumbnail

공식문서 집착녀 이은지...
RHF(React Hook Form)의 편리함에 대한 자자한 칭찬에 냅다 도입을 시도했지만, RHF의 너무나도 구린 공식문서에 좌절해버리고 말았따. 급기야는 자기가 공식문서를 적기 시작하는데...

(바로 본론)

React Hook Form이란?

복잡하고 짜증나는 폼 구현을 간편하게 만들어주는 라이브러리다.

폼 구현이 성가신 이유는 크게 두 가지다.

  1. 입력값별로 리액트 상태를 선언해줘야 한다. 코드가 길고 복잡해진다.
  2. 에러 표시, 저장 버튼 활성화 등 입력값의 상태에 따라 보여주어야 하는 자잘한 UI 요소들이 많다. 그리고 이 UI 요소들은 보통 여러 폼에서 반복적으로 등장한다.

RHF은 위 두 가지의 성가심을 해결해주는 도구라고 할 수 있다.
기존에 우리가 구현해온 방식을 간단히 살펴보자.


function App() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState("");
  const [formError, setFormError] = useState("");

  const handleNameChange = (event) => { setName(event.target.value) };

  const handleAgeChange = (event) => { setAge(event.target.value) };

  const handleEmailChange = (event) => { setEmail(event.target.value) };

  const handleSubmit = (event) => {
    event.preventDefault();
    
    if (!name.trim() || !age || !email.trim()) {
      setFormError("All fields are required");
      return;
    }
    console.log("Submitted:", { name, age, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder="이름"
        type="text"
        value={name}
        onChange={handleNameChange}
      />
      <input
        placeholder="나이"
        type="number"
        value={age}
        onChange={handleAgeChange}
      />
      <input
        placeholder="이메일"
        type="text"
        value={email}
        onChange={handleEmailChange}
      />
      {formError && <p style={{ color: "red" }}>{formError}</p>}
      <button type="submit">Submit</button>
    </form>
  );
}

export default App;

간단한 validation까지만 구현했음에도 코드가 무척 긴 것을 알 수 있다. 이처럼 코드가 길어지는 핵심적인 이유는, 각 input 태그의 입력값과 에러 상태를 리액트 state로 관리하고 있기 때문이다. 요구사항이 추가될수록 코드는 점점 더 복잡해질 것이다.

가령 변경사항이 있을 때만 제출 버튼을 활성화하는 요구사항이 추가된다면...

import React, { useState, useEffect } from "react";

function App() {
  const [name, setName] = useState("");
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState("");
  const [formError, setFormError] = useState("");
  const [isFormDirty, setFormDirty] = useState(false);

  const handleNameChange = (event) => {
    setName(event.target.value);
    setFormDirty(true);
  };

  const handleAgeChange = (event) => {
    setAge(event.target.value);
    setFormDirty(true);
  };

  const handleEmailChange = (event) => {
    setEmail(event.target.value);
    setFormDirty(true);
  };

  const handleSubmit = (event) => {
    event.preventDefault();

    if (!name.trim() || !age || !email.trim()) {
      setFormError("All fields are required");
      return;
    }
    
    console.log("Submitted:", { name, age, email });
    setFormDirty(false);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder="이름"
        type="text"
        value={name}
        onChange={handleNameChange}
      />
      <input
        placeholder="나이"
        type="number"
        value={age}
        onChange={handleAgeChange}
      />
      <input
        placeholder="이메일"
        type="text"
        value={email}
        onChange={handleEmailChange}
      />
      {formError && <p style={{ color: "red" }}>{formError}</p>}
      <button type="submit" disabled={!isFormDirty}>
        Submit
      </button>
    </form>
  );
}

export default App;

이렇게나 코드가 길어진다.

RHF은 이처럼 길어지고 반복되는 폼 구현을 보다 간편하게 만들어준다. RHF의 핵심 API인 useForm을 살펴봄으로써 RHF이 이러한 문제를 어떻게 해결해주고 있는지 살펴보자. 스포하자면, RHF를 사용할 경우 코드에서 저 모든 리액트 state들이 사라진다.

RHF의 핵심: useForm

useForm은 그 이름에서 알 수 있듯 리액트 커스텀 훅이고, 다른 커스텀 훅들처럼 인자를 받아 값을 리턴한다. useForm의 핵심 골자는 다음과 같다.

  1. 폼을 구현할 컴포넌트에 useForm을 호출한다.
  2. 호출 시 인자로 폼에 대한 설정값을 넘겨준다.
  3. useForm이 반환하는 값들을 활용해 폼을 구현한다.

인자와 반환값들을 하나하나 살펴보며 useForm에 대해 좀 더 알아보자.

(공식문서에는 총 6가지의 훅이 소개되어 있지만, 당신이 React를 사용할거라면 + RHF를 처음 도입하는거라면 useForm 하나만 알아도 충분하다. 만약 당신이 React Native나 MUI, Antd 등의 UI 라이브러리를 사용한다면 useForm와 useController 이렇게 두 가지만 알면 된다.)

반환값

인자보다는 반환값이 핵심이라 이것부터 살펴보겠다. useForm은 객체 하나를 리턴한다. 객체의 각 프로퍼티들을 이용하면 매우 간편하게 폼을 구현할 수 있다. 핵심 프로퍼티들을 살펴보자.

register

register는 말그대로, input 태그를 RHF에 등록하는 메서드이다. RHF의 핵심은 기존에 개발자가 직접 리액트 state로 관리하던 폼의 입력값들을 대신 관리해주는 것이다. 이를 위해서는 input 태그를 RHF에 등록하는 작업이 반드시 필요하다.

이렇게 사용하면 된다.


function App() {
  const { register } = useForm()
  
  return (
    <form>
    	<input {...register("name", { required: true })} />
		<input {...register("age")} />
		<input {...register("email", { pattern: { value: /이메일 정규식/, message: '이메일 양식 틀렸어요'})} />
	</form>
	)
}

{...register("name")}란 결국 RHF에 해당 input 태그(의 값)를 "name"이라는 이름으로 등록하겠다는 뜻이다. 이를 통해 이제 이 input 태그의 값은 RHF에 의해 관리된다.

⭐️ 이로써 별도의 리액트 state를 선언할 필요 없이 RHF가 알아서 입력값을 관리해주게 된다. ⭐️


참고로 validation을 구현하기 위해서는 register 호출 시 두 번째 인자를 넘겨주면 된다. 두 가지 타입의 인자를 받는다.

  • validation 규칙만 넘겨주기
{ maxLength: 2 }
  • validation 규칙과 에러 메시지를 함께 넘겨주기
{ maxLength: {
	value: 2, // validation 조건 값
    message: '땡 틀렸습니다' // 에러 메시지
    }
}

RHF에서 지원하는 validation으로는 required, maxLength, minLength, max, min, pattern이 있다. 그 외의 validation을 구현해야 한다면 validate 라는 이름의 콜백 함수를 넘겨줄 수 있다.

handleSubmit

handleSubmit: ((data: Object, e?: Event) => Promise<void>) => Promise<void>

폼을 제출하기 위해 필요한 메서드다. 서버에 데이터를 제출하는 함수를 인자로 받는다. 인자로 받은 콜백함수에 폼 데이터를 넘겨준다. 다음과 같이 사용한다.


function App() {
  const { register, handleSubmit } = useForm()
  
  const onSubmit = (data) => {
    axios.post('url', data) // 서버에 제출
  }
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
    	<input {...register("name", { required: true })} />
		<input {...register("age")} />
		<input {...register("email", { pattern: { value: /이메일 정규식/, message: '이메일 양식 틀렸어요'})} />
	</form>
	)
}

한 가지 주의해야 할 점은 이 handleSubmit 함수는 사용자의 입력값이 모든 validation을 통과했을 때만 호출된다는 것이다. 사용자가 제출 버튼을 클릭한 시점에 어떤 작업을 하고 싶다면 handleSubmit이 아닌 다른 곳에서 해야 한다.

formState

폼에 대한 다양한 상태를 담고 있는 객체다.폼의 자잘한 구현사항들을 간편하게 구현할 수 있게 해주는 값들이 여기에 모여있다.

  • isDirty
    입력값의 수정 여부를 의미한다. 입력값이 수정되었을 때에만 제출 버튼을 활성화해야 하는 경우에 사용한다. 해당 값을 사용하기 위해서는 useForm 호출 시 useForm의 인자로 폼의 기본값인 defaultValues를 반드시 넘겨주어야 한다.

  • isValid
    모든 필드에 에러가 없을 경우 true가 된다.

  • errors
    validation 에러 발생 시 에러가 발생한 필드를 확인할 수 있다. 주로 에러 UI를 띄워줄 때 사용한다. 이렇게 생겼다.

{
  ['register에서 등록한 필드 이름']: { // name, age, email
  	 type: 'validation 종류', // required, maxLength, minLength, max, min, pattern
     message: '아까 register에서 등록한 에러 메시지',
}
     
// 예시
     
{
  'name': {
    type: 'required'
  },
  'email': {
    type: 'pattern',
    message: '이메일 양식 틀렸어요'
  }
}
       

register와 formState에서 제공하는 값들을 사용하는 것으로 코드의 길이가 정말 많이 줄어들었음을 알 수 있다.


한 가지 주의해야 할 점은, formState의 프로퍼티들을 객체구조할당 방식으로 꺼내어 써야지만 제대로된 값을 리턴한다는 점이다. 닷 노테이션(.으로 객체의 프로퍼티에 접근)을 사용하면 올바른 최신 값을 리턴하지 않는다.

공식문서에는 formState가 Proxy로 구현되어 있기 때문이라고 설명하고 있다. 해당 내용을 100% 이해하지는 못해서, 관련 내용이 담긴 공식문서 페이지를 링크해둔다. (링크)

// ❌ formState.isValid is accessed conditionally, 
// so the Proxy does not subscribe to changes of that state
return <button disabled={!formState.isDirty || !formState.isValid} />;
  
// ✅ read all formState values to subscribe to changes
const { isDirty, isValid } = formState;
return <button disabled={!isDirty || !isValid} />;

이 외에도 formState에는 다음과 같은 프로퍼티들이 있다. 필요에 맞게 사용해보자.

- dirtyFields
- touchedFields
- defaultValues
- isSubmitted
- isSubmitSuccessful
- isSubmitting
- isLoading
- submitCount
- isValidating

watch

입력값을 다른 곳에 보여줘야 할 때 watch를 사용하면 된다. watch('name') 이렇게 호출하면 'name' 필드의 최신 값을 리턴한다.

clearErrors

에러가 표시된 후 유저가 입력창에 포커스를 했을 때 에러를 없애주고 싶다면 이 메서드를 사용하면 된다. 모든 에러를 한 번에 없앨 수도 있고, 인자로 에러를 없애고 싶은 필드 이름을 넘겨줄 수도 있다.

인자

인자로는 폼에 대한 설정값이 담긴 객체를 넘겨줄 수 있다. 필수값은 아니기에 생략해도 무방하다. 객체에는 다음과 같은 프로퍼티들이 있다.

- mode
- revalidationMode
- defaultValues
- values
- errors
- resetOptions
- criteriaMode
- shouldFocussError
- delayError
- shouldUseNativeValidation
- shouldUnregister

이 중 살펴볼만한 건 mode, defaultValues 이렇게 두 가지이다.

  • mode
    validation의 실행 시점을 설정할 수 있다. 기본값은 onSubmit으로, 유저가 제출 버튼을 클릭할 때 실행된다. 그 외에는 onChange, onBlur 등의 옵션을 제공한다. 다만 onChange의 경우 성능 이슈가 발생할 수 있다고 명시되어 있다.

  • defaultValues

    • 폼의 기본값을 설정할 수 있는 옵션이다. 필수값은 아니지만, 설정해두는 것을 권한다. 공식문서 정말 여러 부분에 걸쳐 defaultValues의 설정을 권장하는 내용이 등장한다. 특히 isDirty 등의 프로퍼티는 해당 값 없이 제대로 동작하지 않는다.
    • 객체로 설정할 수도 있고, Promise를 리턴하는 함수를 등록할 수도 있다.

나머지 프로퍼티에 대한 상세한 설명은 여기에서 확인할 수 있다.

핵심 정리

왜 쓰냐 이거

  • 폼 쉽게 구현하려고 쓴다. 각 입력값 리액트 state로 관리할 필요 없고, 각종 derived state들을 알아서 계산 및 제공해 준다.
  • (본문엔 한 번도 안나왔지만) 리렌더링 최적화도 알아서 해준다.

어떻게 쓰냐

  • RHF에 input 태그를 등록한다. (본문에선 input 태그만 다뤘지만 select 태그도, 커스텀 리액트 컴포넌트도 등록할 수 있다.)
  • useForm이 반환하는 값들을 잘 활용한다.

공식 문서 보는 작은 팁

공식 문서 위계가 좀 구리게 되어있다...!!

나머지는... 머리만 복잡해지니까 처음엔 보지 마셔요

useForm 페이지에 딱 들어가면 등장하는 내용이 인자에 대한 내용이고, 반환값들은 별도의 페이지로 구분되어 있다.

마무리

나름 핵심만 정리해봤는데, RHF을 처음 도입하는 누군가에게는 도움이 되었으면 좋겠다! 공식 문서 구리다고 엄청 욕하면서 글을 적기 시작했는데 막상 내가 설명하려니 쉽지는 않았다 😇 ...

사실 리액트에서 RHF를 도입했다면 그렇게 이해가 어렵진 않았을 것 같다. 내 경우 React Native에서 RHF를 사용했는데, 관련 자료가 없어서 많이 헤맸다. 하지만 그 과정에서 라이브러리를 샅샅이 이해할 수 있었다. (이 글을 쓸 수 있었던 이유기도 하다.) 다음번에는 React Native에서 RHF를 활용하는 방법에 대해 다뤄보도록 하겠다.

0개의 댓글