React-Hook-Form

summereuna🐥·2023년 10월 20일

일반적으로 form 작성하기


보통은 일반적으로 form을 작성한다고 해보자.
각각의 인풋마다 state를 만들어 관리해야 하고, 핸들러도 작성해야 하는데 특히 밸리데이션이 엄청나게 많아진다. ^^...

import { useState } from "react";

export default function Forms() {
  //input의 value 상태
  //보통은 각각의 인풋마다 state를 만들어 관리해야 한다.
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const [formErrors, setFormErrors] = useState("");

  //input 이벤트
  const onUsernameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    setUsername(event.currentTarget.value);
  };
  const onEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    setEmail(event.currentTarget.value);
  };
  const onPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    setPassword(event.currentTarget.value);
  };

  //form 이벤트
  const onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
    event.preventDefault();

    //밸리데이션
    if (username === "" || email === "" || password === "") {
      setFormErrors("모든 항목을 채워주세요.");
    }
    if (username.length < 5) {
      setFormErrors("username은 5글자 이상이어야 합니다.");
    }
    if (!email.includes("@")) {
      setFormErrors("이메일에는 @이 포함되어야 합니다.");
    }
    //등등...엄청 많은 에러 발생 가능성 ^^
    //서밋 핸들러가 엄청 커지게 된다.💥
    console.log(username, email, password);
  };

  return (
    <form className="flex flex-col" onSubmit={onSubmit}>
      <input
        required
        type="text"
        placeholder="Username"
        value={username}
        onChange={onUsernameChange}
        minLength={5}
      />
      <input
        required
        type="email"
        placeholder="Email"
        value={email}
        onChange={onEmailChange}
      />
      <input
        required
        type="password"
        placeholder="Password"
        value={password}
        onChange={onPasswordChange}
      />
      <input type="submit" value="가입하기" />
    </form>
  );
}

//하지만 이렇게 해놓으면 브라우저의 콘솔에서 사용자가 맘대로 required 없애고 type도 바꾸고 해버리면 브라우저가 그냥 허용해버려서 밸리데이션 무용지물 된다.
//따라서 밸리데이션을 따로 작성해줘야 한다.

✅ React-hook-form

📝 1. 적은 코드(Less code)


1. 모든 건 useForm Hook에서 나온다.

const { register, watch } = useForm();

2. input을 state에 등록하기 위해서 useForm이 반환하는 register 메서드를 사용한다.

register함수는 쉽게 말하면 input을 state와 연결시켜주는 역할을 한다.

register: (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })

이 메서드를 사용하면 input을 등록하거나 엘리먼트를 선택하고 React Hook Form에 유효성 검사 규칙을 적용할 수 있다. 유효성 검사 규칙은 모두 HTML 표준을 기반으로 하며 사용자 지정 유효성 검사 방법도 허용한다.

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

const { register, handleSubmit } = useForm();
< input {...register("firstName", { required: true })} placeholder="First name" />

3. useForm이 반환하는 watch 메서드를 사용하면 form을 볼 수 있다.

console.log(watch());

watch함수를 호출하면, form이 register함수에 입력한 이름을 key로 가지는 객체가 되는데 그것을 볼 수 있다.

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

export default function Forms() {
  //1. 모든 건 useForm Hook에서 나온다.
  const { register, watch } = useForm();

  //2. input을 state에 등록하기 위해서 useForm이 반환하는 register함수를 사용한다.
  //register 함수: input을 state와 연결시켜주는 역할

  //3. form을 보게해주는 useForm이 반환하는 watch함수 사용한다.
  //watch함수를 호출하면, form이 register함수에 입력한 이름을 key로 가지는 객체가 되는데 그것을 볼 수 있다.

  console.log(watch());
  return (
    <form className="flex flex-col">
      <input
        {...register("username")}
        type="text"
        placeholder="Username"
      />
      <input {...register("email")}
        type="email"
        placeholder="Email" />
      <input
        {...register("password")}
        type="password"
        placeholder="Password"
      />
      <input type="submit" value="가입하기" />
    </form>
  );
}

📝 2. 더 나은 유효성검증(Better validtaion)


React Hook Form은 양식 검증을 위한 기존 HTML 표준에 맞춰 form 검증을 쉽게 만든다.

4. form을 submit 하기 위해서는 useForm이 반환하는 handleSubmit 메서드를 사용한다.

const { register, handleSubmit } = useForm();

form의 onSubmit 속성에 handleSubmit() 메서드를 호출하는 방식으로 사용한다.

<form onSubmit={handleSubmit(onValid, onInvalid)}>

handleSubmit 함수는 인자로 두 가지 메서드, onValid, onInvalid를 가진다.

  • onValid(): 유효성 검증에 성공했을 때 호출되는 메서드이다.
  • onInvalid(): 유효성 검증에 실패했을 때 호출되는 메서드이다.

5. 유효성 검증 위해 validationRule 객체 사용하기

❓ 커다란 onSubmit 없이 각각의 filed를 어떻게 validate 할 수 있을까?

예를들면 input의 속성 값으로 준 required의 경우, 유저가 임의로 브라우저의 콘솔에서 검증 값을 삭제해 버리면 무용지물이 되어 버린다. 이런 변수 상황에서도 유효성 검증이 제대로 이루어질 수 있게 하려면, JS를 사용하여 validate을 작성해야 한다.
그렇기 때문에 수 많은 validate로 인해 onSubmit 핸들러는 검증을 하느라 커질 수 밖에 없다.

✔️ React-hook-form에서는 register()의 두 번째 인자에 validationRule를 자바스크립트로 적을 수 있다.

이 두 번째 인자는 아주 강력하다.
각각의 필드마다 register()의 두 번째 인자에 validationRule 규칙을 따로 정해줄 수 있다.
때문에 기존처럼 onSubmit 함수에 validation을 몰빵으로 넣어서 커질 일이 없다.

<input
  {...register("username", { required: true })}
  type="text"
  placeholder="Username"
  />
import { useForm } from "react-hook-form";

export default function Forms() {
  const { register, handleSubmit } = useForm();

  const onValid = () => {
    console.log("✅ 유효성 검증 완료!");
  };

  const onInvalid = () => {
    console.log("❌ 유효성 검증 실패!");
  };
  return (
    <form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
      <input
        {...register("username", { required: true })}
        type="text"
        placeholder="Username"
      />
      <input
        {...register("email", { required: true })}
        type="email"
        placeholder="Email"
      />
      <input
        {...register("password", { required: true })}
        type="password"
        placeholder="Password"
      />
      <input type="submit" value="가입하기" />
    </form>
  );
}

6. 유효성 규칙 종류

📝 List of validation rules supported:

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern: 정규식으로 입력값 필드를 검증할 때 사용
  • validate


    만약에 매우 특정한 규칙들이 필요한 상황이라면 여기에 있는 규칙들만으로는 필요한 유연성을 다 확보할 수는 없다. 다행히도 validate에 다 있으니 걱정하지 않아도 된다.

7. handleSubmit 함수의 인자인 onValid와 onInvalid로 data와 erorr받기

✔️ React-hook-form은 인자로 유효한 입력 데이터를 전달할 수 있다. 유효한 입력 데이터는 handleSubmit함수의 인자인 onValid로 받아들인다.

타입스크립트를 사용하고 있기 때문에 onValid가 받는 인자인 data의 타입을 지정해 줘야 한다.

  • interface로 LoginForm 타입을 만들고, useForm의 타입에 LoginForm 타입을 넣어준다.
  • 그러고 나서 onValid 메서드가 받는 인자인 data에도 LoginForm 타입을 넣어주자.
import { FieldError, useForm } from "react-hook-form";

interface LoginForm {
  username: string;
  email: string;
  password: string;
}

export default function Forms() {
  const { register, handleSubmit } = useForm<LoginForm>();

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

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  return (
    <form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
      <input
        {...register("username", { required: true })}
        type="text"
        placeholder="Username"
      />
      <input
        {...register("email", { required: true })}
        type="email"
        placeholder="Email"
      />
      <input
        {...register("password", { required: true })}
        type="password"
        placeholder="Password"
      />
      <input type="submit" value="가입하기" />
    </form>
  );
}
  • 실행해보면 콘솔에 이렇게 data가 잘 찍히는 것을 확인할 수 있다.

✔️ 마찬가지로, React-hook-form에서 validation을 사용할 때, validation은 에러와 연결된다. 에러는 handleSubmit 함수의 인자인 onInvalid로 받아들인다.

const onInvalid = (errors: FieldErrors) => {
  console.log(errors);
};
  • 이메일을 빼고 버튼을 눌러보면 이렇게 어디에 에러가 있는지 뜬다.
  • 보다시피 message도 있다. 즉, 에러 메세지를 보낼 수 있다.
    각 필드에서 register의 두 번째 인자로 { required: true }를 보내는 대신, { required: "에러 메시지" }를 보낼 수 있다.
<input
  {...register("email", { required: "Email is required" })}
  type="email"
  placeholder="Email"
  />

  • username의 minLenght를 5로 주고, 오류를 일으켜 보자.
<input
  {...register("username", {
    required: "Username is required",
    minLength: 5,
  })}
  type="text"
  placeholder="Username"
  />
  • 그러면 이렇게 어디에서 오류가 뜨는지 알려준다. username의 minLength 수가 5 보다 부족하기 때문에 오류가 뜬다. 이렇게 어떤 타입의 오류인지도 알려준다.

    그리고 보다시피 username에도 message가 있기 때문에 minLength에 객체를 보내 메시지와 값을 정할 수 있다.
<input
  {...register("username", {
    required: "Username is required",
    minLength: {
      message: "Username should be longer than 5 chars.",
      value: 5,
    },
  })}
  type="text"
  placeholder="Username"
  />

8. validate 객체 사용하여 사용자 정의 validation 로직 만들기

  • api와 통신하고 싶다거나
  • 원하는것을 확인해 보고 싶다면
    그건 onValid 함수에 없다. 이 함수에 오는 건 이미 유효성 검증을 통과한 유효한 데이터이기 때문이다.

그럴 때 validate 객체를 사용하면 된다.

validate 객체는 유효성을 검사할 인수로 콜백 함수를 전달하거나 콜백 함수의 개체를 전달하여 모든 유효성을 검사할 수 있다.

  • 예를 들어 gmail.com 이메일을 사용하는 유저들은 받지 않겠다고 해보자.
<input
  {...register("email", {
    required: "Email is required",
    validate: {
      notGmail: (value) => !value.includes("@gmail.com"),
    },
  })}
  type="email"
  placeholder="Email"
  />
  • 이메일 input에 @gmail.com을 입력하면 에러가 뜨고, 에러 타입이 notGmail로 뜬다.

  • 여기에도 에러 message를 넣어줄 수 있다.
    notGmail에 조건에 따른 반환값을 넣어주면 된다.

<input
  {...register("email", {
    required: "Email is required",
    validate: {
      notGmail: (value) =>
      !value.includes("@gmail.com") ? "" : "Gmail is not allowed",
      //또는 || (OR) 써서 같은 효과
      //!value.includes("@gmail.com") || "Gmail is not allowed",
    },
  })}
  type="email"
  placeholder="Email"
  />
  • 그러면 에러가 뜰 때 메세지도 같이 뜬다.

이런식으로 validate 객체를 사용하여 사용자 정의 유효성 검증을 만들 수 있다.

  • 예를 들면, 백엔드에 fetch 해서 입력한 이메일이 이미 가입되었는지 확인하거나, 이미 username이 있다거나 등의 검증을 할 수 있다.

📝 3. 더 나은 에러(Better Errors: set, clear, display)


React Hook Form은 form의 오류를 표시하는 오류 객체를 제공한다. 오류 유형은 주어진 유효성 검사 제약 조건을 반환한다.

현재 콘솔에서 에러를 보고 있는데, 에러를 화면의 input에서 보여줘 보자.

9. error를 display하기 위해서 useForm이 반환하는 FormState 객체를 사용한다.

FormStateerrors를 포함해 많은 것을 제공하는 객체이다.

  • 유저가 클릭하기를 기다리는 대신에 errors를 콘솔로그 할 수 있다.
  • 즉, onInvalid 함수 밖에서도 errors를 잡을 수 있고, 원한다면 에러를 화면에 표시할 수도 있다.
//...

export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors }, // formState 객체에 있는 errors를 사용해보자.
  } = useForm<LoginForm>();

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

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  //onInvalid 함수 밖에서도 errors 사용할 수 있게됨
  console.log(errors);
  
  return (
    <form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
      <input
        {...register("username", {
          required: "Username is required",
          minLength: {
            message: "Username should be longer than 5 chars.",
            value: 5,
          },
        })}
        type="text"
        placeholder="Username"
      />
      {errors.username?.message} //errors.username이 있으면 message 보여주기
      <input
        {...register("email", {
          required: "Email is required",
          validate: {
            notGmail: (value) =>
              !value.includes("@gmail.com") || "Gmail is not allowed",
          },
        })}
        type="email"
        placeholder="Email"
      />
      {errors.email?.message}
      <input
        {...register("password", { required: "Password is required" })}
        type="password"
        placeholder="Password"
      />
      {errors.password?.message}
      <input type="submit" value="가입하기" />
    </form>
  );
}

10. validation pattern


  1. 유저가 form을 제출할 때 validation이 일어남
  2. 유저가 입력할 때 즉시 validtaion이 일어남: 이를 form의 mode라고 부른다.

useForm의 mode에 옵션을 주어서, 원하는 바에 따라 여러 다른 mode로 form을 validate 할 수 있다.

mode의 옵션

  • all
  • onBlur: 작성하는 input을 벗어 날 때 validation 일어남
    • 입력 후 벗어났을 때
  • onChange: input이 변하면 validation 일어남
    • 입력 하나하나에 매번 validation
    • 그래서 주로 username이 이미 사용중인지 확인할 때 사용하기에 유용함
  • onSubmit: (*기본값) submit 버튼을 누를 때 validation 일어남
  • onTouched
export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginForm>({ mode: "" });

11. validation한 input에 스타일 주기

유효성 검증에 실패하면 input이 빨간 테두리가 생기게 해보자.

<input
  {...register("username", {
    required: "Username is required",
    minLength: {
      message: "Username should be longer than 5 chars.",
      value: 5,
    },
  })}
  type="text"
  placeholder="Username"
  className={`${Boolean(errors.username) ? "border-red-500" : ""}`}
/>

12. errors 수동으로 설정(setErrors)하기


setError()
이 함수를 사용하면 하나 이상의 오류를 수동으로 설정할 수 있다.

setErrors()로 전역(global) 에러를 설정할 수 있다.

예를 들어, 백엔드를 POST로 fetch한 후, 그에 대한 응답을 받았는데 DB가 오프라인이어서 에러가 난 상황이라고 해보자.

  • 이런 에러는 username,email,password 필드, 즉 특정한 필드의 에러가 아닌 form에 대한 에러이다.
  • form의 전역 에러 사실을 유저에게 표시하여 커뮤니케이션을 하려고 할 때 setErrors()를 사용할 수 있다.
  • setError()는 특정 필드에 관한 에러가 아니더라도 에러를 설정할 수 있게 해준다.
import { FieldErrors, useForm } from "react-hook-form";

interface LoginForm {
  username: string;
  email: string;
  password: string;
  errors?: string; //전역 errors 추가
}

export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
  } = useForm<LoginForm>({ mode: "onBlur" });

  const onValid = (data: LoginForm) => {
    console.log(data);
    //DB 오프라인 문제로 에러 발생한 상황
    setError("errors", { message: "Backend is offline sorry." });
    //어떤 것을 타입의 에러를 설정하고 싶은지 선택하면 됨
  };

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  return (
    //... 
      {errors.errors?.message} //form 마지막에 에러 메세지 표시하기
    </form>

setErrors()로 필드별로 에러를 설정할 수도 있다.

이번엔 onValid함수에 에러가 없는 상황을 가정해보자.
예를 들어 데이터를 백엔드로 보냈는데 내가 validation을 하지 않아서 username이 이미 존재한다고 메시지가 왔다고 해보자.

  • validation하지 않고 form을 백엔드로 보냈는데 백엔드에서 이미 이 username을 가진 유저가 있다는 메세지를 받은 경우, username을 위한 에러를 설정해야 한다.
  • 이때도 setErrors()를 사용하면 된다.
const onValid = (data: LoginForm) => {
  console.log(data);
  //백엔드에서 이미 이 username을 가진 유저가 있다는 응답 받은 경우
  setError("username", { //필드를 설정하고
    message: "이 username이 이미 존재합니다." //메세지 적으면 된다.
  });
};
  • 그리고 username input 옆에 {errors,username?.message}로 에러 메시지를 유저에게 표시하면 된다.

13. errors 수동으로 초기화(clearErrors)하기

reset()으로 form submit후, 전체 input을 초기화할 수 있다.

reset()
전체 form state 또는 form state의 일부를 리셋한다.

  • form에서 submit후, 전체 input 초기화할 때 사용하면 된다.
export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    reset,
  } = useForm<LoginForm>({ mode: "onBlur" });

  const onValid = (data: LoginForm) => {
    console.log(data);
    
    //form 제출 후, onValid 실행 되면 전체 input 초기화
    reset();
  };

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  return (
    //... 

resetField()로 form submit후, 특정 input만 초기화할 수 있다.

resetField()
개별 field state를 재설정한다.

  • form submit후, 특정 input만 초기화할 때 사용하면 된다.
export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    resetField,
  } = useForm<LoginForm>({ mode: "onBlur" });

  const onValid = (data: LoginForm) => {
    console.log(data);
    
    //form 제출 후, onValid 실행 되면 password input만 초기화 하기
    resetField("password");
  };

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  return (
    //... 

📝 4. input에 대한 제어(Have control over inputs)


📝 제어된 input 통합(Integrating Controlled Inputs)


📝 5. 이벤트 신경쓰기 싫음(Don't deal with events)



📝 6. input에 같은 코드 반복하기 싫음(Easier inputs)


📝 기존 양식 통합(Integrating an existing form)
기존 form을 통합하는 것은 간단해야 한다.
컴포넌트의 ref를 등록하고 관련 props를 input에 할당하면 된다.


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

interface LoginForm {
  username: string;
  email: string;
  password: string;
  errors?: string;
}

export default function Forms() {
  const {
    register,
    handleSubmit,
    formState: { errors },
    setError,
    reset,
  } = useForm<LoginForm>({ mode: "onBlur" });

  const onValid = (data: LoginForm) => {
    console.log(data);
    // setError("username", {
    //   //필드를 설정하고
    //   message: "이 username이 이미 존재합니다.", //메세지 적으면 된다.
    // });

    //form 제출 후, onValid 실행 되면 전체 input 초기화
    reset();
  };

  const onInvalid = (errors: FieldErrors) => {
    console.log(errors);
  };

  // console.log(errors);

  return (
    <form className="flex flex-col" onSubmit={handleSubmit(onValid, onInvalid)}>
      <input
        {...register("username", {
          required: "Username is required",
          minLength: {
            message: "Username should be longer than 5 chars.",
            value: 5,
          },
        })}
        type="text"
        placeholder="Username"
        className={`${Boolean(errors.username) ? "border-red-500" : ""}`}
      />
      <span>{errors.username?.message}</span>
      <input
        {...register("email", {
          required: "Email is required",
          validate: {
            notGmail: (value) =>
              !value.includes("@gmail.com") || "Gmail is not allowed",
          },
        })}
        type="email"
        placeholder="Email"
        className={`${Boolean(errors.email) ? "border-red-500" : ""}`}
      />
      <span>{errors.email?.message}</span>
      <input
        {...register("password", {
          required: "Password is required",
          minLength: {
            message: "password should be longer than 8 chars.",
            value: 8,
          },
        })}
        type="password"
        placeholder="Password"
        className={`${Boolean(errors.password) ? "border-red-500" : ""}`}
      />
      <span>{errors.password?.message}</span>
      <input type="submit" value="가입하기" />
      {errors.errors?.message}
    </form>
  );
}
profile
Always have hope🍀 & constant passion🔥

0개의 댓글