react-hook-form 과 비제어 컴포넌트 👀

뮤진·2023년 4월 28일
2
post-thumbnail

🌼 제어 컴포넌트로 form 다루기

리액트의 모든 UI의 업데이트는 상태변경으로부터 발생해야한다.
그러나 입력폼의 경우 내부적으로 상태변경이 발생하지 않아도 바로 UI가 변경된다.
이를 uncontrolled component 즉 통제되지 않는 컴포넌트라고한다.

따라서 입력폼에 보여지고 있는 data - 리액트의 상태 가 matching 되도록 만들어 제어 컴포넌트로 폼을 다루는 것이 중요하다.

React 에서 제어 컴포넌트란 React를 통해 제어하게 되는 컴포넌트를 말한다.

제어 컴포넌트로 간단한 폼 만들기

  • 폼을 통해서 데이터 수집하기
    -> 이름, 아이디, 비밀번호, 전화번호, 이메일
  • 폼을 통해 수집된 데이터를 다루기 위한 state 만들기
import React, { useState } from 'react';

const ContorlledForm = () => {
  const [name, setName] = useState('');
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');
  const [phone, setPhone] = useState('');
  const [email, setEmail] = useState('');

  return <form></form>;
};

export default ContorlledForm;
  • input 요소를 생성하여 입력받고 변하는 값을 추적할 이벤트와 state를 연결
  • 전체코드 👇
import React, { useState } from 'react';

const ContorlledForm = () => {
  const [name, setName] = useState('');
  const [id, setId] = useState('');
  const [password, setPassword] = useState('');
  const [errors, setErrors] = useState({
    name: {
      invalid: false,
      message: '이름이 너무 깁니다.',
    },
    id: {
      invalid: false,
      message: 'id는 3글자 이상, 20글자 이하여야 합니다.',
    },
    password: {
      invalid: false,
      message: '비밀번호는 10자 이하여야 합니다.',
    },
    phone: {
      invalid: false,
      message: '전화번호 형식에 맞지 않습니다.',
    },
  });

  const handleName = (e) => {
    setName(e.currentTarget.value);
  };
  const handleId = (e) => {
    setId(e.currentTarget.value);
  };
  const handlePassword = (e) => {
    setPassword(e.currentTarget.value);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.length > 10) {
      setErrors((prev) => ({
        ...prev,
        name: {
          ...prev.name,
          invalid: true,
        },
      }));
    }
    if (id.length < 3 || id.length > 20) {
      setErrors((prev) => ({
        ...prev,
        id: {
          ...prev.id,
          invalid: true,
        },
      }));
    }
    if (password.length > 10) {
      setErrors((prev) => ({
        ...prev,
        password: {
          ...prev.password,
          invalid: true,
        },
      }));
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor='name'>name: </label>
      <input
        id='name'
        type='text'
        value={name}
        onChange={handleName}
        placeholder='이름을 입력해주세요'
      />
      {errors.name.invalid ? <p>{errors.name.message}</p> : null}

      <label htmlFor='id'>id: </label>
      <input
        id='id'
        type='text'
        value={id}
        onChange={handleId}
        placeholder='아이디를 입력해주세요'
      />
      {errors.id.invalid ? <p>{errors.id.message}</p> : null}

      <label htmlFor='pw'>password: </label>
      <input
        id='pw'
        type='text'
        value={password}
        onChange={handlePassword}
        placeholder='비밀번호를 입력해주세요'
      />
      {errors.password.invalid ? <p>{errors.password.message}</p> : null}

      <button>Submit</button>
    </form>
  );
};

export default ContorlledForm;

이렇게 제어 컴포넌트로 폼을 다루기 위해서 state를 모두 선언해주고, 해당 state를 다루기 위해 핸들링 함수를 만들고, 에러를 위한 state를 만들어주었다.
지금은 단순한 예제이기때문에 코드가 간소화 된 듯 하지만 실제로는 유효성 검증도 들어가기 때문에 더더욱 폭잡하고 긴 코드가 될 것 같다.

가장 큰 문제점은 모든 값이 state로 연결되어있어 하나의 값이 변할 때마다 해당 컴포넌트와 모든 자식 컴포넌트는 리렌더링이 발생하게 될 것이다.........💩
아주아주 불필요한 렌더링이라고 할 수 있다.

UI만 보면 정말 단순한 이 폼을 다루기 위해 너무 많은 state와 함수가 담겨져 있고
코드를 짤 때마다 이게 맞나 싶은 생각이 가장 많이 드는 부분이었던 것 같다.

그래서 form 라이브러리인 react-hook-form 을 사용해보기로 한다 !

🌼 react-hook-form

현재 React form 라이브러리로는 react-hook-formformik 이 사용되고 있다. react-hook-form의 지속적인 업데이트와 빠른 마운트 속도 탓인지 다운로드 횟수가 formik보다 50만 회나 높다. 그래서 나도 react-hook-form 을 사용해보기로 결정했다.

react-hook-form 설치

npm i react-hook-form
yarn add react-hook-form

useForm 훅 사용하기

👉 useForm 훅에는 파라미터로 객체를 넘길 수 있다.

export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = Partial<{
    mode: Mode;
    reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
    defaultValues: DefaultValues<TFieldValues>;
    resolver: Resolver<TFieldValues, TContext>;
    context: TContext;
    shouldFocusError: boolean;
    shouldUnregister: boolean;
    shouldUseNativeValidation: boolean;
    criteriaMode: CriteriaMode;
    delayError: number;
}>;

const model = useForm({
  mode: "onChange",
  defaultValues: {},

});
  • react-hook-form을 활용하는 데 여러가지 config 옵션이 들어갈 수 있다.
  • mode
    validation 전략을 설정하는데 활용
    onSubmit, onChange, onBlur, all 등의 옵션이 있음
    ❗️주의
    mode를 onChange앞에 두었을 때 다수의 렌더링이 발생할 수 있어 성능에 영향을 끼칠 수 있음!
  • defaultValues
    form에 기본값을 제공하는 옵션
    ❗️주의
    기본값을 제공하지 않는 경우 input의 초기값은 undefined로 관리됨

👉 훅을 통해 리턴받는 값의 타입

export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
    watch: UseFormWatch<TFieldValues>;
    getValues: UseFormGetValues<TFieldValues>;
    getFieldState: UseFormGetFieldState<TFieldValues>;
    setError: UseFormSetError<TFieldValues>;
    clearErrors: UseFormClearErrors<TFieldValues>;
    setValue: UseFormSetValue<TFieldValues>;
    trigger: UseFormTrigger<TFieldValues>;
    formState: FormState<TFieldValues>;
    resetField: UseFormResetField<TFieldValues>;
    reset: UseFormReset<TFieldValues>;
    handleSubmit: UseFormHandleSubmit<TFieldValues>;
    unregister: UseFormUnregister<TFieldValues>;
    control: Control<TFieldValues, TContext>;
    register: UseFormRegister<TFieldValues>;
    setFocus: UseFormSetFocus<TFieldValues>;

};
  • 다양한 함수와 객체들이 담겨있음

👉 직접 구현해보기

import React from 'react';
import { useForm } from 'react-hook-form';

const ReactHookForm = () => {
  const {
    register,
    formState: { errors },
    watch,
    reset,
    handleSubmit,
    getValues,
    setError,
    setFocus,
  } = useForm({
    mode: 'onSubmit',
    defaultValues: {
      id: '',
      name: '',
      password: '',
      email: '',
    },
  });
  return <div></div>;
};

export default ReactHookForm;

register

useForm을 통해 컨트롤할 폼 객체를 리턴받아서 구조분해 한다.
이 때 register 함수를 사용하며, 이 함수로 input 요소를 다룰 수 있다.

<input
   type='text'
   {...register('email', {
      pattern: {
         value:
           /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
         message: '이메일 형식에 맞지 않습니다!',
          },
        })}
      />

register (a, b)

  • a: name - 해당 필드를 다루게 될 key값으로 반드시 들어가야 하는 값!
  • b: options객체 - 유효성 검사를 위한 프로퍼티들이 들어갈 수 있음 !
    (required,min,max,minLength,maxLength,pattern etc.)
    참고 https://react-hook-form.com/get-started#Quickstart
  • 유효성 검사를 위해 value만 줄 수도 있지만, value-message로 구성된 객체를 넘김으로써 해당 에러에 대한 구체적인 메세지를 제공할 수도 있다!
register("name", {required: true, minLength: 10});
register("name", {required: "해당 필드는 필수입니다.", minLength: {
  value: 5,
  message: "5글자 이상 입력해주세요."
}
});

formState

register 함수에 validation을 넣어줬다면, 에러에 대한 정보는 formState 객체에서 찾을 수 있다.

  • 에러가 존재하지 않는다면 ? 👉 빈 객체
  • 에러가 발생한다면 ? 👉 객체 속에 error 타입과 메세지가 담김

formState는 이렇듯 errors에 대한 정보도 담기지만, 이 외에도 많은 유용한 정보를 담고있다.

  • submitCount: submit한 횟수를 알 수 있음
  • dirtyFields: 기본값이 수정된 필드들이 담김 (defaultValues 제공 필수)
  • touchedFields: 유저에 의해 수정된 필드들이 담김
  • isValid: 에러가 있는지 알 수 있음

참고 https://react-hook-form.com/api/useform/formstate/

watch

비제어 컴포넌트로 폼을 구현할 때 조건에 따라 필드를 다르게 노출해야하는 경우가 생긴다. 이런 경우 watch 함수를 사용할 수 있다.

  • 폼에 입력된 값을 구독하여 실시간으로 체크할 수 있게해주는 함수
const {id, name, password, ...watch} = watch();
// 전체 필드를 리턴

const id = watch('id');

❗️주의
관찰하려는 필드에 defaultValue 를 주지 않는다면 초기값이 undefined로 관리된다.

getValues

react-hook-form에서 값을 추적할 수 있는 방법은 두 가지다.
1. watch
2. getValues

watch vs getValues

  • watch : 입력된 값을 추적하고 반환하며 해당 값에 따라 리렌더링을 발생시킴
  • getValues: 값을 반환하지만 리렌더링을 발생시키지 않고, 해당 값을 추적하지 않음
const handleEvent = (e) => {
  const value = getValues('name');
  setState(value);
}

Reset

폼을 통해 create 뿐 아니라 edit기능도 구현하게된다. 유저가 edit 버튼을 클릭해 정보를 수정하려한다면 비동기 데이터를 활용해 기존의 데이터를 폼에 뿌려줘야 한다.

이런 경우 reset이라는 함수를 활용할 수 있다.

useEffect(() => {
	const data = fetch(api).then(res => res.json());
  	reset({
    	name: data.name,
      	id: data.id,
     	...data
    })
}, []);

handleSubmit, setError, setFocus

폼에서 데이터를 입력 후 '제출'버튼을 누른다.
이때 submit 이벤트가 발생하는데 서버에 데이터를 넘기기 전에 해당 데이터에 대한 검증을 할 필요가 있다.

이를 위해 form 요소의 onSubmit에 handleSubmit 함수를 넣어주고, 매개변수로 정의한 onSubmit함수를 넣어준다.

onSubmit 함수를 정의할 때 매개변수로 data라는 값을 받을 수 있는데
이 값은 사용자가 제출버튼을 클릭한 후 내려오는 최종으로 제출하는 데이터이다.

const { handleSubmit } = useForm();

const onSubmit = (data) => {
	// data: 최종데이터
  	console.log(data);
}

...

return <form onSubmit={handleSubmit(onSubmit)} />

onSubmit

  • e.preventDefault() 필요없음 !
  • 최종 데이터 검증을 진행하다가 에러가 체크되었다면 setError 함수를 활용할 수 있다.
  • setFocus를 활용해 해당 필드에 포커스를 줄 수 있다.
setError('name', {type: 'minLength', message: '3글자 이상 입력해주세요'});
setFocus('name');

이 외 더 유용하고 다양한 기능들이 많이 있다 👀👍

사용후기

  • 라이브러리에 의존성을 부여했기 때문에 로직이 줄었다.
  • 비제어 컴포넌트를 통해 폼을 다루기 때문에 input 요소를 다루기 위해 선언했던 수많은 state들이 없어졌다.
  • 이로 인해 컴포넌트의 불필요한 렌더링 횟수도 최소화 할 수 있었다.




해당 글을 참고하여 작성되었습니다.
https://tech.osci.kr/2023/01/02/introduce-react-hook-form/

profile
프론트엔드 공부기록 🫶 기록을 통해 성장하기

0개의 댓글