My Boundary As Much As I Experienced

React-hook-form으로 유효성 검사하기 본문

FrontEnd/React

React-hook-form으로 유효성 검사하기

Bumang 2024. 8. 30. 00:45

배우게 된 계기

사람들이 하도 form 구현 시 React-hook-form이 좋다고 해서

언젠가 한 번 써봐야지 했다가 이번에 만드는 초기앱 회원가입에 써보기를 고려하였다.

 

사실 처음엔 useState와 useEffect가 가장 직관적이지 않나? 이걸로도 충분한데..

라는 생각이 지배적이었는데 알고보니 매우 개발자를 편하게 해주는 좋은 라이브러리였다.

 

React-hook-form의 최대 장점: 관리할 상태가 엄청나게 줄어든다.

아래는 내가 취업 전 부트캠프 파이널 프로젝트로 했던 코드이다.

호텔 예약 매물 양도 플랫폼이 컨셉이었는데, 예약 과정에서 관리해야될 상태들이 너무 많았다.

호텔 취소 물품의 1차 가격, 2차 가격, 2차 가격 설정 여부, 계좌등록여부, 은행, 계좌번호, 약관 동의1/2/3/4를 포함해서...

1차 가격이 구입가보다 낮은지, 2차 가격이 1차 가격보다 낮은지 등등 모든 정책들을 useState와 useEffect의 혼합으로 구현하였다.

const TransferWritingPrice = () => {
  // 현재 선택된 숙박
  const selectedItem = useSelectedItemStore((state) => state.selectedItem);

  // 유저 정보
  const userInfoQuery = useUserInfoQuery();
  const { data: userData } = userInfoQuery;

  // 1차 가격
  const [firstPrice, setFirstPrice] = useState("");
  const [is2ndChecked, setIs2ndChecked] = useState(false); // 2차 가격 설정하기 체크 여부
  // 1차 가격 HTMLElement
  const firstCheckRef = useRef(null); // 2차 가격 체크박스 ref
  const firstInputRef = useRef(null);

  // 2차 가격
  const [secondPrice, setSecondPrice] = useState("");
  const [downTimeAfter, setDownTimeAfter] = useState("");
  // 2차 가격 HTMLElement
  const secondPriceInputRef = useRef(null);
  const secondTimeInputRef = useRef(null);

  const [bank, setBank] = useState(userData?.bank ?? null);
  const [accountNumber, setAccountNumber] = useState(
    userData?.accountNumber ?? null,
  );

  // 약관 동의
  const [opt1, setOpt1] = useState(false);
  const [opt2, setOpt2] = useState(false);
  const [opt3, setOpt3] = useState(false);
  const [optFinal, setOptFinal] = useState(false);

  // 제출 가능 여부
  const [readyToSubmit, setReadyToSubmit] = useState(false);

  useEffect(() => {
    setBank(userData?.bank ?? null);
    setAccountNumber(userData?.accountNumber ?? null);
  }, [userData]);

  // HOOKS
  // 작성 중 나가면 경고하는 훅
  usePreventLeave(true);

  // state변화에 따라 제출 버튼을 활성화/비활성화 하는 훅
  useReadyToSubmit({
    setReadyToSubmit,
    firstPrice,
    opt1,
    opt2,
    opt3,
    optFinal,
    bank,
    accountNumber,
    is2ndChecked,
    secondPrice,
    downTimeAfter,
    userData,
  });

  // 계좌 등록 페이지 <-> 판매글 작성 페이지 왔다갔다 할 때
  // state상태에 따라 체크박스나 토글 풀려있는거 복구하는 훅
  const { accountSetting, setAccountSetting } = useChangePage({
    is2ndChecked,
    firstCheckRef,
  });

  // 판매글 작성 POST 리액트 쿼리 훅
  const { mutate } = usePostTransferItems({
    firstPrice,
    secondPrice,
    downTimeAfter,
    is2ndChecked,
    opt1,
    opt2,
    opt3,
    optFinal,
    bank,
    accountNumber,
    userData,
  });

  // 정책들을 모두 검토하고 마지막에 usePostTransferItems API를 POST요청하는 훅
  const [submitHandler] = useSubmitHandler({
    readyToSubmit,
    firstPrice,
    secondPrice,
    downTimeAfter,
    firstInputRef,
    secondPriceInputRef,
    secondTimeInputRef,
    is2ndChecked,
    userData,
    mutate,
    opt1,
    opt2,
    opt3,
    optFinal,
  });

  return (
    <S.Container layout>
    ...

https://bumang.tistory.com/125

 

그나마 커스텀 훅으로 유사한 로직끼리 묶어서 이 정도 정리가 된거지

개발 당시에는 너무 상태가 많아 이게 어떤 상태였는지 계속 헷갈렸다.

(커스텀 훅 안에서도 몇백줄의 if문으로 에러처리 로직들이 존재한다...)

 

react-hook-form을 사용하면 이 정도의 복잡성을 유지할 필요가 사라진다.

자세한 것은 사용방법을 보면서 알아보자.

 

사용방법(Web)

1. useForm을 임포트하고 원하는 formData의 타입을 명시해준다.

mport { useForm } from 'react-hook-form';

interface FormData {
  ...
}

 

2. register, handleSubmit, formState들을 꺼내서 세팅

register: 유효성 검사 로직을 각각의 input에 등록해주는 함수다.

 

handleSubmit: 말 그대로 제출하는 시점에 연결해줄 핸들러이다.

유효성 검사를 모두 돌려주고 진행할지, 진행하지 않을지 판단해준다.

 

이 핸들러 안에 패러미터로 onSubmit 콜백을 넣어주고,

onSubmit 안에는 개발자가 원하는 제출 후 이어 실행시킬 로직을 넣으면 된다.

function Component() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    mode: "onChange" // 
  });
  // mode 옵션은 입력 필드의 유효성 검사를 언제 트리거할지를 설정하는 데 사용됨.

  const onSubmit = (data) => {
    // submit 시 하고싶은 후속조치들을 쓰면 된다.
  };
  
  return {
    ...

 

formState: 입력 혹은 제출 시 form의 상태에 대해 출력해준다.

주로 errors 하나만 쓰는데, 말 그대로 현재 유효성 검사를 통과하지 못한 column의 데이터와 메시지를 보관한다.

 

3. register를 이용해 input태그에 유효성 검사 로직 주입

register로 타겟 input 안에 해당 input에서 검사할 유효성 조건들을 주입하자.

그러면 더 이상 모든 조건들을 useState로 만들어 놓고 useEffect로 계속 검사해주지 않아도 된다.

그러면 위에 useForm 초기화할 때 걸어놓은 mode에 따라 onBlur, onSubmit, onChange시 유효성 검사가 실행된다.

     <div>
        <label>아이디:</label>
        <input
          type="text"
          {...register('username', { // 첫 번째 인자로 name을 지정해주고, 두 번째 인자로 유효성 검사 로직을 넣어주면 된다. 
            required: '아이디는 필수 입력 사항입니다.',
            minLength: { value: 5, message: '아이디는 5자 이상이어야 합니다.' }, // message들은 위반 시 errors.[COLUMN명].message에 담기게 된다.
            maxLength: { value: 20, message: '아이디는 20자 이하이어야 합니다.' }
          })}
        />
        {formState.errors.username && <span>{formState.errors.username.message}</span>} // 에러 메시지를 꺼내서 에러 문구를 만들수도 있다.
      </div>

React-hook-form의 장점2: 에러처리 및 에러메시지 관리도 쉽다.

유효성 검사 후 error처리가 된 코드는 바로 에러 객체에 담긴다. 그리고 에러 메시지도 rule에 설정한대로 나오기 때문에

에러 메시지도 따로 상태를 만들거나 객체로 관리 안 해도 되어 편리하다.

그리고 에러객체도 유효성 검사가 실행될때마다 갱신되기 때문에, 이를 변수로 뽑아내서 쓸 수 있다는 것도 알아둬야한다.

  const passwordErrorMessage = errors.password?.message;
  const confirmErrorMessage = errors.passwordConfirm?.message;

 

 

 사용방법 (RN) - Controller를 이용해 TextInput에 유효성 검사 로직 주입

지금까진 웹에서 Input 태그에 훅폼을 적용하는 예시였고, 이제 RN에서 react-hook-form을 이용하는 방법을 알려주겠다.

input태그가 아닌 컴포넌트에서 유효성 검사를 해야된다면 Controller를 사용하면 된다. 

import { useForm, Controller } from "react-hook-form";

 

useForm 초기화는 위와 동일하다.

  const {
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    mode: "onChange",
  });

 

Controller라는 컴포넌트에

name(register의 첫 번째 인자에 해당) prop을 적고,

rules(register의 두 번째 인자에 해당)에 유효성 검사 로직을 적어주고,

render에 실제 렌더할 TextInput을 넣어주면 된다. (왠지 FlatList와 사용 방법이 비슷하다.)

          <Controller
            control={control}
            name="username"
            rules={{
              required: "아이디는 꼭 적어야 해요.",
              pattern: {
                value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                message: "정확한 이메일 주소를 입력해주세요",
              },
              minLength: {
                value: 5,
                message: "아이디는 적어도 5글자거나 더 길어야 해요.",
              },
              maxLength: {
                value: 20,
                message: "아이디는 길어도 20글자거나 더 짧아야 해요.",
              },
            }}
            render={({ field: { onChange, value } }) => (
              <CustomTextInput
                placeholder="이메일 입력"
                onChangeText={onChange}
                guideTip={errorMessage}
                ref={usernameRef}
                guideTipColor={
                  errors.username ? "red" : theme.colors.yellow.DEFAULT
                }
                value={value}
              />
            )}
          />

 

 

5. 만약 다른 인풋의 값을 참조해서 유효성 검사를 해야된다면? (ex: 비밀번호 확인)

참조할 인풋의 name을 watch함수에 제공하여 구독한다.

  const password = watch("password"); // 'password' 필드의 값을 실시간으로 추적

 

그리고 이 값을 rules의 validate 컬럼을 통해 검증하면 된다.

          <Controller
            control={control}
            name="passwordConfirm"
            rules={{
              required: "비밀번호 확인은 필수 입력 사항입니다.",
              validate: (value) =>
                value === password || "비밀번호가 일치하지 않습니다.", // 실시간 비교
            }}
            ...
          />

 

6. 특정 조건 시 (제출까진 하지않고) 유효성 검사만 실행하기 (trigger)

submit 시엔 관련된 모든 formData가 유효성 검사가 되고 로그인 시도가 일어나기 때문에...

차근차근 유저가 입력했던 field만 검사시키고 싶다면? trigger 함수를 쓰면 된다.

예를 들어 onBlur 시 검사하기 위해선 trigger를 쓰면 된다. 

useForm에서 꺼낼 수 있다.

function MyForm() {
  const { control, handleSubmit, trigger } = useForm();

 

특정 필드의 name을 제공하면 그 필드만 유효성 검사가 된다.

trigger('fieldName'); // 특정 필드에 대한 유효성 검사

 

여러 필드를 동시에 검사하려면 배열로 제공하면 된다.

trigger(['fieldName1', 'fieldName2']); // 여러 필드에 대해 유효성 검사
trigger(); // 전체 폼에 대해 유효성 검사

 

만약 특정 인풋에서 blur 시 검사를 발생시키고 싶다면? 이렇게 하면 된다.

          <input
            onBlur={() => {
              trigger('username');  // username 필드에 대해 유효성 검사 트리거
            }}
            placeholder="Username"
          />

 

7. 이렇게 검사한 컬럼값을 변수화해서 쓰고 싶다면? getValue, watch

  const {
    control,
    handleSubmit,
    trigger,
    getValues, // 꺼내기
    formState: { errors },
  } = useForm<FormData>({
    mode: "onBlur",
  });


const passwordValue = getValues("password") // 원하는 컬럼 구독!

 

공식문서

https://react-hook-form.com/docs/useform#resolver

 

useForm

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com