[React] typescript 환경에서 form 만들기 : 시즌2 react-hook-form

sjoleee·2022년 7월 30일
3

지난이야기...

라이브러리의 도움없이 form을 구현하는 것이 굉장히 불편함을 깨달았기에, react hook form의 사용법을 공부하려고 하는데...

0. react hook form 설치

npm i react-hook-form

1. 뼈대 만들기

function ToDoList() {

  return (
    <form>
      <input placeholder="입력하세요"></input>
      <input placeholder="입력하세요"></input>
      <input placeholder="입력하세요"></input>
      <input placeholder="입력하세요"></input>
      <input placeholder="입력하세요"></input>
      <button>제출</button>
    </form>
  );
}

이렇게 여러 개의 input을 사용하는 경우를 생각해보자.
회원가입이라던지, 이력서 등록이라던지...
컴포넌트명이 투두인 것은 잠시 무시해주세요...
useForm이 갖고있는 method를 알아보면서 시작해보자!

2. register method

function ToDoList() {
  const { register } = useForm();
  
  return (
    <form>
      <input {...register("id")} placeholder="입력하세요"/>
      <input {...register("password")} placeholder="입력하세요" />
      <input {...register("name")} placeholder="입력하세요" />
      <input {...register("phone")} placeholder="입력하세요" />
      <input {...register("email")} placeholder="입력하세요" />
      <button>제출</button>
    </form>
  );
}

이런 식으로 사용하게 되는데, register 매서드에 대해 조금 더 알아보자.

register은 4개의 props를 가진다.
{onChange, onBlur, name, ref}
console에 찍어보면 이렇게 나온다.

console.log(register("name")); 실행 결과

input의 name을 등록해서 각각 다르게 사용할 수 있는데,
register의 인자로 input의 name을 넣어주면 된다.
그래서 이런 식으로 이용하게 되는 것.

function ToDoList() {
  const { register } = useForm();
  
  return (
    <form>
      <input {...register("id")} placeholder="입력하세요"/>
      <input {...register("password")} placeholder="입력하세요" />
      <input {...register("name")} placeholder="입력하세요" />
      <input {...register("phone")} placeholder="입력하세요" />
      <input {...register("email")} placeholder="입력하세요" />
      <button>제출</button>
    </form>
  );
}

근데 왜 spread 연산자를 사용하는가?

const { onChange, onBlur, name, ref } = register('firstName'); 
// include type check against field path with the name you have supplied.
        
<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>
// same as above
<input {...register('firstName')} />

라고 공식문서는 말하고 있다.
즉, register가 갖고 있는 props들을 일일히 넣어주기 힘드니까 spread 연산자를 사용해서 편하게 표현한 것이라고 생각한다.

이렇게 우리는 기존의 onChange와 state를 register 하나로 대체할 수 있었다.
그리고 이 register을 통해 validation이 가능한데(option), 뒤에서 다시 살펴보도록 하자.

3. watch method

watch는 form 입력값들의 변화를 관찰할 수 있게 해주는 함수.

function ToDoList() {
  const { register, watch } = useForm();
  console.log(watch("id"));
  //관찰하고자 하는 input의 name을 넣는다.
  
  return (
    <form>
      <input {...register("id")} placeholder="입력하세요"/>
      <input {...register("password")} placeholder="입력하세요" />
      <input {...register("name")} placeholder="입력하세요" />
      <input {...register("phone")} placeholder="입력하세요" />
      <input {...register("email")} placeholder="입력하세요" />
      <button>제출</button>
    </form>
  );
}

관찰하고자 하는 input의 name을 집어넣으면 입력값을 알려준다.
혹은, name을 넣지 않고 watch()로 실행하면 모든 input의 입력값을 보여준다.

4. handleSubmit method

기존에 form을 만들던 순서와 동일하게, onSubmit을 만들어줄 것이다.

function ToDoList() {
  const { register, handleSubmit } = useForm();
  const onVaild = (data: any) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onVaild)}>
      <input {...register("id")} placeholder="입력하세요" />
      <input {...register("password")} placeholder="입력하세요" />
      <input {...register("name")} placeholder="입력하세요" />
      <input {...register("phone")} placeholder="입력하세요" />
      <input {...register("email")} placeholder="입력하세요" />
      <button>제출</button>
    </form>
  );
}

handleSubmit에 대해 조금 더 알아보자.
handleSubmit는 두개의 함수를 인자로 받는데,
(필수) 첫번째 인자는 데이터가 유효할 때 호출될 함수,
(선택) 두번째 인자는 데이터가 유효하지 않을 때 호출될 함수이다.

필수인 첫번째 인자만 넣고 진행.
form 태그에 handleSubmit을 연결해주자.
데이터가 유효할 때 onVaild를 호출한다.
onVaild 함수의 인자로 data를 받는데, 이를 콘솔에 찍어보면 어떤 것이 나올지 보자.

우리가 input에 입력한 값이다.

사용자가 input에 막~ 입력을 한 후에, submit을 딱 누르면?
handleSubmit는 validation이나.. 해야 할 것들을 다~ 한 후에 onVaild를 호출한다.

이렇게 onSubmit함수를 직접 만들지 않고 handleSubmit으로 대체할 수 있다.

5. register method - validation

위에서 공부했던 register를 활용하면 input에 조건을 집어넣을 수 있다.
방법은... 조금 어색하게 느껴지긴 한다.

<input
  {...register("id", { required: true })}
  placeholder="입력하세요"
/>

그러니까... register의 인자로 name 뿐만 아니라 적용할 조건을 집어넣으면 된다.
중괄호 열고~ 조건넣기.
적용할 수 있는 조건은 공식문서를 참고!

6. formState method

formState를 통해 어떤 error가 발생했는지 확인할 수 있다.

function ToDoList() {
  const { register, handleSubmit, formState } = useForm();
  const onVaild = (data: any) => {
    console.log(data);
  };
  console.log(formState.errors);
  //

  return (
    <form onSubmit={handleSubmit(onVaild)}>
      <input
        {...register("id", {
          required: "아이디를 입력해주세요",
          minLength: 10,
        })}
        placeholder="입력하세요"
      />
  ...

위의 경우, required 조건을 만족하지 못할 경우 "아이디를 입력해주세요"라는 메시지가 formState.errors를 통해 나타난다.

이렇게 어떤 조건을 달성하지 못했는지 에러메시지를 볼 수 있다.
메시지는 조건에 중괄호 열고 message를 적어주면 되는데, 자세한 것은 공식문서를 참고하자!

이제 이 errors를 콘솔이 아니라 사용자에게 보여줘보자.

<input
  {...register("email", {
    required: true,
    pattern: {
      value: /^[A-Za-z0-9._%+-]+@naver\.com$/,
      message: "naver.com 이메일만 가능합니다.",
    },
  })}
  placeholder="입력하세요"
/>
<span>{formState.errors?.email?.message}</span>

이렇게 span태그 안에 에러 메시지를 넣어서 화면에 렌더링할 수 있다.

이메일 뒤에 붙은 ?은 옵셔널 체이닝으로, 조건에 잘 맞을 경우 formState.errors 안에 email이 없을 수 있어서 undifined일 수 있습니다~ 라고 말해주는 것이다.

근데 {formState.errors.email?.message}가 조금 길다고 생각된다면,
이렇게 사용할 수도 있다.

function ToDoList() {
  const {
    register,
    watch,
    handleSubmit,
    formState: { errors },
  } = useForm<IFormData>();
  
  ...
  
  <input
  {...register("email", {
    required: true,
    pattern: {
      value: /^[A-Za-z0-9._%+-]+@naver\.com$/,
      message: "naver.com 이메일만 가능합니다.",
    },
  })}
  placeholder="입력하세요"
/>
<p>{errors?.email?.message}</p>

이걸 뭐라고 불러야할지 모르겠네...
errors라고 적어도 되게 만들 수 있다!

그리고 아래처럼 타입을 미리 지정해두어야 사용할 수 있다.

interface IFormData {
  errors: {
    email: {
      message: string;
    };
  };
  id: string;
  password: string;
  name: string;
  phone: string;
  email: string;
}

defaultValues 지정도 가능하다.

const {
  register,
  watch,
  handleSubmit,
  formState: { errors },
} = useForm<IFormData>({
  defaultValues: {
    email: "@naver.com",
  },
  //이렇게 defaultValues를 지정해주면?
});


미리 input에 지정해둔 값이 들어가있다!

7. validation

register가 제공하는 조건말고, 내가 특정 조건을 만들어서 검사하고 싶다면 어떻게 해야할까?
예를 들면, "비밀번호"와 "비밀번호 확인" 두 값이 다르다면 에러 메시지를 띄우고 싶다.

  const onValid = (data: IFormData) => {
    if (data.password !== data.password2) {
      setError("password2", { message: "비밀번호가 다릅니다" });
    }
  };

이렇게 onValid 함수에 password와 password2를 비교하는 조건문을 만들고, setError로 password2에 에러를 만들어준다.
data에 위에서 만들었던 IFormData를 연결하여 타입을 지정해주면 값을 사용하기 편리하다.

근데, 아까 저~ 위에서 잠깐 얘기했던건데, 이 onValid 함수는 해야할 것들을 다~ 한 후에 호출되는 함수이므로, 뭐 이메일 길이, 이름 길이 등등 모든 validation이 마무리된 후에 password와 password2를 비교하게 된다.
다시말해, 다른 조건(ex. 패턴, 최소 길이 등)이 모두 충족된 이후에 해당 조건을 따져보게 된다는 것이다.
이걸 몰라서 왜 조건을 걸었는데 에러 메시지가 안뜨나~ 하고 한참 뜯어봤다...

  const onValid = (data: IFormData) => {
    if (data.password !== data.password2) {
      setError("password2", { message: "비밀번호가 다릅니다" }, {shouldFocus: true});
    }
  };

shouldFocus를 true로 해놓으면 에러 발생할 경우, 자동으로 해당 input에 커서가 옮겨진다.

이제 또다른 에러를 만들어보자.
특정 input이 아니라, 만약 서버 통신이 문제가 생겼을 경우 에러 메시지를 띄우고 싶다면?

inValid 함수 바깥에 이렇게 작성해보자.

setError("extraError", { message: "서버 응답이 없습니다" });

그리고 extraError를 새로 만들었기 때문에, 타입을 지정해주어야 한다.

interface IFormData {
  errors: {
    email: {
      message: string;
    };
  };
  id: string;
  password: string;
  name: string;
  phone: string;
  email: string;
  extraError: string;
  //이렇게 interface 안에 지정해주자.
}

물론 이렇게 만들면 조건없이 무조건 에러가 생기긴 하는데, 그냥 사용법을 알기 위함이니까 해보자.

그리고 저~ 아래쪽 제출버튼 옆에 이렇게 작성하면 메시지를 확인할 수 있다.

<span>{errors?.extraError?.message}</span>

이처럼 추가적인 에러를 발생시킬 수 있다.

그렇다면 이런건 어떨까?
입력한 id가 sangjo를 포함하고 있으면 에러를 발생하는 조건을 넣어보자.

<input
  {...register("id", {
    required: "아이디를 입력해주세요",
    validate: (value) =>
      value.includes("sangjo") ? "sangjo가 포함되면 안됩니다" : true,
  })}
  placeholder="입력하세요"
/>

이렇게 register가 제공하는 조건 중에 validate를 사용하면 콜백함수로 조건을 지정할 수 있다.
false나 메시지를 적으면 조건을 통과하지 못하는 것.
validate는 객체로 여러개의 함수를 전달해서 여러 조건을 걸수도 있다.

<input
  {...register("id", {
    required: "아이디를 입력해주세요",
    validate: {
      noSangjo: (value) =>
        value.includes("sangjo") ? "sangjo가 포함되면 안됩니다" : true,
      noSangjo2: (value) =>
        value.includes("sangjo2") ? "sangjo2가 포함되면 안됩니다" : true,
    },
  })}
  placeholder="입력하세요"
/>
profile
상조의 개발일지

0개의 댓글