Frontend/Projects

[Toy Project 기록하기 3] React Hook Form 활용하기

돌잡이개발자 2023. 3. 4. 22:36

프로젝트에 form을 사용하는 일이 많아지면서 폼 관리 라이브러리인 React Hook Form을 도입하게 되었습니다.

이번에는 React Hook Form을 활용하는 방법을 알아보겠습니다.

 

 


 

 

feed를 업로드하는 과정에서 form을 사용해 개발했습니다.

이 과정을 라이브러리 없이 form을 제어하는 방법과 React Hook Form을 써서 form을 제어하는 방법으로 모두 구현해 보겠습니다.

 

 

라이브러리 없이 Form 다루기

feed를 업로드할 때 받는 form으로부터 제목, 내용, 사진을 받겠습니다.

입력받은 값의 상태를 담을 state를 선언하고 마크업을 작성했습니다.

handleChange, handlePhoto를 통해 input의 state를 갱신하고, handleSubmit을 통해 form의 submit 이벤트가 발생되도록 합니다.

export default function UploadFeed() {
  const [form, setForm] = useState({ title: '', text: '' });
  const [photo, setPhoto] = useState();
  const [preview, setPreview] = useState();
  const navigate = useNavigate();

  const handleChange = (event) => {
    const { name, value } = event.target;
    setForm({ ...form, [name]: value });
  };

  const handlePhoto = async (event) => {
    const files = event.target.files;

    if (!files) {
      return;
    }

    const file = files[0];
    const reader = new FileReader();

    reader.onload = (event) => {
      setPreview(event.target?.result as string);
    };
    reader.readAsDataURL(file);
    setPhoto(file);
  };

  const handleSubmit = async (event) => {
    event.preventDefault();
    const { title, text } = form;

    uploadFeed({ title, text, photo });
    navigate(APP.HOME); 
  };
    
  return (
    <form
      method="POST"
      encType="multipart/form-data"
      onSubmit={handleSubmit}
    >
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          type="text"
          name="title"
          onChange={handleChange}}
        />
      </div>
      <div>
        <label htmlFor="text">text</label>
        <input
          id="text"
          type="text"
          name="text"
          onChange={handleChange}
        />
      </div>
      <div>
        <label htmlFor="photo">Photo</label>
        <input
          id="photo"
          type="file"
          accept="image/*"
          onChange={handlePhoto}
          name="photo"
        />
      </div>
      <div>
        {preview && (
          <img style={{ width: '300px' }} src={preview} alt="preview"></img>
        )}
      </div>
      <button type="submit" className="uploadBtn">
        올리기
      </button>
    </form>	
  );
}

 

error와 validation에 관련해서는 error state를 만들고 handleSubmit시 validation을 체크하는 별도의 처리가 필요합니다.  

export default function UploadFeed() {
  // ...
  const [errors, setErrors] = useState({
    title: {
      invalid: false,
      message: "제목은 2글자 이상, 10글자 이하여야 합니다.",
    },
    text: {
      invalid: false,
      message: "본문은 2글자 이상이여야 합니다.",
    }    
  });

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    if(title.length < 2 || title.length >10 ) {
    	setErrors((prev) => {
          ...prev,
          title: {
            ...prev.title,
            invalid: true,
          }
      })
    }
    // ...
  };

  return (
    <form
      method="POST"
      encType="multipart/form-data"
      className="uploadForm"
      onSubmit={handleSubmit}
    >
      <div>
        <label htmlFor="title">Title</label>
        <input
          id="title"
          type="text"
          name="title"
          onChange={handleChange}
        />
      </div>
      {errors.title && <span>{errors.title}</span>}
      <div>
        <label htmlFor="text">text</label>
        <input
          id="text"
          type="text"
          name="text"
          onChange={handleChange}
        />
      </div>
      {errors.text && <span>{errors.text}</span>}
      // ...
    </form>	
  );
}

폼의 필드가 많아지고 validation 조건이 많아질수록 코드는 점점 더 길어질 것입니다. 또한 폼의 필드 값들이 state로 연결되어 있어 하나의 값이 변경될 때마다 다른 자식 컴포넌트에서 리렌더링이 발생하게 됩니다.

 

 


 

 

React Hook Form 활용하기

React Hook Form 설치하기

npm install react-hook-form

 

register

먼저 useForm 함수를 호출하여 React Hook Form에서 필요한 기능을 가져옵니다.

그리고 useForm에서 반환된 register 함수를 사용하여 입력 필드를 등록합니다. register 함수는 해당 필드의 key값으로 반드시 name이 필요합니다.

export default function UploadFeed() {
  const navigate = useNavigate();

  const {
    register,
    handleSubmit,
  } = useForm();

  const onValid = data => {
    console.log(data);
  };

  return (
    <form
      method="POST"
      encType="multipart/form-data"
      onSubmit={handleSubmit(onValid)}
    >
      <div>
        <label htmlFor="title">
          제목
        </label>
        <input
          id="title"
          type="text"
          {...register('title')}
        />
      </div>
      <div>
        <label htmlFor="text">
          내용
        </label>
        <textarea
          {...register('text')}
        />
      </div>
      <div>
        <label htmlFor="photoFile">
          사진
        </label>
        <input
          id="photoFile"
          type="file"
          accept="image/*"
          {...register('photoFile')}
        />
      </div>
      // ...
    </form>
  );
}

 

Validation

register 함수에 key에 이어 두 번째 파라미터로 options 객체가 들어갈 수 있고 아래 항목에 대한 validation 체크할 수 있습니다.

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate
export default function UploadFeed() {
  const {
    register,
    handleSubmit,
  } = useForm();

  // ...
  
  return (
    <form
      method="POST"
      encType="multipart/form-data"
      onSubmit={handleSubmit(onValid)}
    >
      <div>
        <label htmlFor="title">
          제목
        </label>
        <input
          id="title"
          type="text"
          {...register('title', {
            required: '제목은 필수입니다.',
            maxLength: {
              value: 30,
              message: '30글자 이하로 작성해주세요.',
            },
          })}
        />
      </div>
      <div>
        <label htmlFor="text">
          내용
        </label>
        <textarea
          {...register('text', {
            required: '내용은 필수입니다.',
            minLength: {
              value: 3,
              message: '3글자 이상 작성해주세요.',
            },
          })}
        />
      </div>
      <div>
        <label htmlFor="photoFile">
          사진
        </label>
        <input
          id="photoFile"
          type="file"
          accept="image/*"
          {...register('photoFile', {
            onChange: handlePhotoFileChange,
          })}
        />
      </div>
      // ...
    </form>
  );
}

 

Handle errors

formState의 객체에서 errors를 통해 에러에 대한 정보를 받을 수 있습니다. 

export default function UploadFeed() {
  const {
    register,
    formState: { errors },
    handleSubmit,
  } = useForm();
  
  // ...

  return (
    <form
      method="POST"
      encType="multipart/form-data""
      onSubmit={handleSubmit(onValid)}
    >
      <div>
        <label htmlFor="title">
          제목
        </label>
        <input
          // ...
        />
      </div>
      {errors?.title && (
        <p>{errors.title?.message}</p>
      )}
      <div className={styles.inputWrapper}>
        <label htmlFor="text">
          내용
        </label>
        <textarea
          // ...
        />
      </div>
      {errors?.text && (
        <p>{errors.text?.message}</p>
      )}
      <div>
        <label htmlFor="photoFile">
          사진
        </label>
        <input
          // ...
        />
      </div>
      // ...
    </form>
  );
}

이 외에도 유용하게 사용할 수 있는 다양한 기능들은 공식 문서를 통해 확인할 수 있습니다. 
https://react-hook-form.com/

 

 

같은 form을 만드는데 react-hook-form 라이브러리를 사용하니 같은 기능을 쉽고 빠르게 구현할 수 있었습니다. 또한 컴포넌트가 관리해야 할 state의 수가 적어지면서 컴포넌트의 리렌더링 횟수도 줄어든 것을 확인할 수 있습니다. 

 

 

🧗‍♀️ 제가 진행한 프로젝트가 궁금하다면

🔽 프론트엔드는 이곳에서 확인하실 수 있습니다.

https://github.com/Team-Madstone/doljabee-fe

 

GitHub - Team-Madstone/doljabee-fe: Climbing Community

Climbing Community. Contribute to Team-Madstone/doljabee-fe development by creating an account on GitHub.

github.com

 

🔽 백엔드는 이곳에서 확인하실 수 있습니다.

https://github.com/Team-Madstone/doljabee-be

 

GitHub - Team-Madstone/doljabee-be: Climbing Community

Climbing Community. Contribute to Team-Madstone/doljabee-be development by creating an account on GitHub.

github.com

 

참고 자료

https://react-hook-form.com/

반응형