반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • Next.js
  • 토비의 스프링
  • 자바스크립트
  • item8
  • rust
  • Javadoc 자바독 자바주석 주석 Comment
  • 이펙티브 자바
  • 싱글턴
  • 메이븐 라이프사이클
  • 스프링 검증
  • 메이븐 골
  • item7
  • 이펙티브자바
  • 참조 해제
  • 자바스크립트 면접
  • 슬로우 쿼리
  • 디자인패턴
  • 자바 디자인패턴
  • serverless computing
  • 작업기억공간
  • 느린 쿼리
  • 객체복사
  • Pre-rendering
  • 도커공식문서
  • Java
  • 추상 팩터리 패턴
  • prerendering
  • 외래키 제약조건
  • 서버리스 컴퓨팅
  • 팩터리 메서드 패턴
  • 싱글톤
  • NEXT JS
  • MSSQL
  • 자바 검증
  • 알고리즘
  • 자료구조
  • 자바
  • 프로그래머의 뇌
  • 싱글톤 패턴
  • pnpm
  • 플라이웨이트패턴
  • 메이븐 페이즈
  • 자바스크립트 인터뷰
  • 빈 검증
  • 이펙티브 자바 item9
  • 러스트
  • item9
  • next js app
  • try-with-resources
  • bean Validation

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

프론트엔드/React

React Hook Form 이 해결하는 문제들과 사용법

2022. 7. 2. 18:49

기존의 React Form 코드

import React, { useState } from "react";

export default function NormalForm() {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [formErrors, setFormErrors] = useState("");

  const onUsernameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    const {
      currentTarget: { value },
    } = event;

    setUsername(value);
  };

  const onEmailChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    const {
      currentTarget: { value },
    } = event;

    setEmail(value);
  };

  const onPasswordChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
    const {
      currentTarget: { value },
    } = event;

    setPassword(value);
  };

  const onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
    event.preventDefault();

    console.log(username, email, password);

    // FIXME: input 엘리먼트에 attribute 로 건 제약조건은 개발자모드에서 제거할 수 있어서 보안에 취약하다.

    // TODO: 제출 전 추가적인 Validation 이 필요하다. ex) username is too short.
    if (username.length < 6) {
      setFormErrors("Username is too short!"); // how many errors are gonna happen?
    }

    // FIXME: 좋은 사용자 경험을 위해서 6글자 이상 username 이 입력된다면, UX 를 위해 즉시 경고메세지는 지워지게 만들자.
    //  -> 할 일이 너무 많다!
  };

  return (
    <form onSubmit={onSubmit}>
      <input
        value={username}
        onChange={onUsernameChange}
        type="text"
        placeholder="Username"
        minLength={5}
        required
      />
      <input
        value={email}
        onChange={onEmailChange}
        type="email"
        placeholder="Email"
        required
      />
      <input
        value={password}
        onChange={onPasswordChange}
        type="password"
        placeholder="Password"
        required
      />
      <input type="submit" value="Create Account" />
    </form>
  );
}

문제 1: 보일러 플레이트 코드가 생기고, 코드가 길어진다.

  • onChange 에 들어갈 모든 콜백 함수를 각각 작성해주어야 한다.
    • 사실 내용은 React Hooks 의 set... 메서드를 이용해 값을 설정하는 것 밖엔 없다.
    • HTML 코드에도 다 각각 연동시켜주어야 한다.

문제 2: 검증을 하기 까다롭다.

검증은 보통 2가지로 나뉜다.

  • HTML Attribute 에 required, minLength 등을 입력하여 적용할 수 있는 마크업에서 자체적으로 지원하는 검증
    • 그러나 이 검증은 사용자가 개발자도구에서 Attribute 를 지워버리면 무효화된다. 그래서 아래의 검증이 추가적으로 필요하다.
    • 간혹 브라우저가 지원하지 않는 경우도 있을 수 있다.
  • Submit 전에 콜백 메서드에서 최종적으로 올바른 값이 들어왔는지 확인하는 검증
    • HTML Attribute 로 값을 검증하는데는 한계가 있고, 클라이언트가 조작해버릴 수 있기 때문이다.

이 과정에서 또 매우 많은 코드를 작성하게 된다.

문제 3: UX 를 고려하면 코드가 더 길어진다.

값을 검증만 하면 안되고, 결국엔 사용자에게 검증 결과를 알려주어야 한다. 즉, 에러 메세지를 표출해주어야 한다. 이를테면, username 에 6글자 이하가 들어온 상태에서는 경고 메세지를 띄우고 있고, 6글자가 넘으면 즉시 경고 메세지가 사라지게 만든다면, 이로 인해 또 추가적인 코딩이 필요하게 된다.

문제 해결 1: 보일러 플레이트 코드 제거

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

// TODO: Less code (*)
// TODO: Better validation
// TODO: Better Errors (set, clear, display)
// TODO: Have control over inputs
// TODO: Don't deal with events (*)
// TODO: Easier inputs (*)

export default function ReactHookForm() {
  const { register, watch } = useForm();

  console.log(watch()); // 입력된 값을 실시간으로 지켜보는 역할

  return (
    <form>
      <input
        {...register("username")}
        type="text"
        placeholder="Username"
        required
      />
      <input {...register("email")} type="email" placeholder="Email" required />
      <input
        {...register("password")}
        type="password"
        placeholder="Password"
        required
      />
      <input type="submit" value="Create Account" />
    </form>
  );
}

useForm() 이 반환하는 register() 라는 메서드를 제공하여 문제를 해결한다.

  • register("username") 코드는 다음과 같은 4가지 반환 값을 갖게 된다.
{
    name: "username",
    onBlur: async (event) => {…},
    onChange: async (event) => {…},
    ref: (ref) => {…}
}

onBlur 이벤트 는 포커스가 사라졌을 때 발생하는 이벤트이다.

위와 같은 객체를 반환하는데, 각 프로퍼티는 HTML Input Element 에 Attribute 로서 들어갈 것들을 정의해놓은 것이다. 위의 jsx 예제 소스코드처럼 멋진 spread 문법과 jsx 문법을 통해 HTML 마크업에 적용될 수 있다.

<input
  {...register("username")}
  type="text"
  placeholder="Username"
  required
/>

결과적으로 코드는 적어졌지만, 코드가 하는 역할 자체는 이전에 작성했던 코드와 같은 역할을 한다.

공식 문서 를 통해 Register Fields 를 좀 더 자세히 살펴볼 수 있다.

문제 해결 2: Validation 에서 생기는 문제 해결

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

// TODO: Less code (*)
// TODO: Better validation -> 너무 큰 onSubmit 함수를 갖고 싶지 않다.
// TODO: Better Errors (set, clear, display)
// TODO: Have control over inputs
// TODO: Don't deal with events (*)
// TODO: Easier inputs (*)

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

  const onValid = () => {
    console.log("I am valid baby!");
  };

  return (
    <form onSubmit={handleSubmit(onValid)}>
      <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="Create Account" />
    </form>
  );
}

handleSubmit() 메서드를 통해, 검증 문제의 일부를 해결한다. 기존의 검증에서는 검증을 2번 해줘야 하는 문제가 있었다.

  • HTML Input Attribute 를 이용한 검증
  • onSubmit 콜백 메서드에서의 검증

React Hook Form 에서는 {required: true}와 handleSubmit() 이 반환한 콜백 메서드를 통해 이를 한번에 해결해준다. required 외에도 공식문서 를 보면 max, min, pattern 등 많은 검증을 지원한다. 검증에 실패하면 HTML 요소로 다시 포커싱을 하는 등의 간단한 부가기능도 같이 제공한다.

export declare type RegisterOptions<TFieldValues extends FieldValues = FieldValues, TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = Partial<{
    required: Message | ValidationRule<boolean>;
    min: ValidationRule<number | string>;
    max: ValidationRule<number | string>;
    maxLength: ValidationRule<number>;
    minLength: ValidationRule<number>;
    pattern: ValidationRule<RegExp>;
    validate: Validate<FieldPathValue<TFieldValues, TFieldName>> | Record<string, Validate<FieldPathValue<TFieldValues, TFieldName>>>;
    valueAsNumber: boolean;
    valueAsDate: boolean;
    value: FieldPathValue<TFieldValues, TFieldName>;
    setValueAs: (value: any) => any;
    shouldUnregister?: boolean;
    onChange?: (event: any) => void;
    onBlur?: (event: any) => void;
    disabled: boolean;
    deps: InternalFieldName | InternalFieldName[];
}>;

handleSubmit 메서드는 2가지 콜백 함수를 받는데, 성공 시 실행할 콜백과 실패 시 실행할 콜백을 받는다. 성공, 실패시 알맞은 콜백을 넣어주면 된다.

문제 해결 3: 필드 에러 메세지 표기 관련 문제 해결

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

// TODO: Less code (*)
// TODO: Better validation (*)
// TODO: Better Errors (set, clear, display)
// TODO: Have control over inputs
// TODO: Don't deal with events (*)
// TODO: Easier inputs (*)

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

export default function ReactHookForm() {
  // setValue, watch, clearError, setError
  const {
    register,
    handleSubmit,
    // 에러에 대한 정보를 가지고 있다.
    formState: { errors },
  } = useForm<LoginForm>({
    // formState 로 에러에 대한 정보가 언제 갈지 결정한다.
    // API 를 이용하여 해당 아이디가 중복인지 계속 체크하며 진행하는 것도 가능하다.
    mode: "onChange",
    defaultValues: {
      email: "defaultemail@gmail.com",
    },
  });

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

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

  console.log("errors", errors);

  return (
    <form onSubmit={handleSubmit(onValid, onInvalid)}>
      <input
        {...register("username", {
          required: "username is required",
          minLength: {
            message: "The username should be longer than 5 chars.",
            value: 5,
          },
        })}
        type="text"
        placeholder="Username"
      />
      <input
        {...register("email", {
          required: "email is required",
          validate: {
            notGmail: (value) =>
              // API 를 통한 검증이든 뭐든 할 수 있다.
              !value.includes("@gmail.com") || "Gmail is not allowed.",
          },
        })}
        type="email"
        placeholder="Email"
        className={`${errors.email?.message ? `bg-red-100` : `bg-green-100`}`}
      />

      {
        // formState 에 있는 오브젝트를 통해 쉽게 에러를 UI 에 표기하는 것도 가능하다.
        errors.email?.message
      }
      <input
        {...register("password", { required: "password is required" })}
        type="password"
        placeholder="Password"
      />
      <input type="submit" value="Create Account" />
    </form>
  );
}

LoginForm 타입스크립트 인터페이스를 만들어 우리가 넘길 값에 대한 청사진을 주었다.

useForm 메서드를 호출할 때, defaultValues 를 이용하면 기본 값도 줄 수 있다.

위와 같이 onInvalid 콜백 메서드에 인자를 받으면, 에러 시 에러가 난 필드명과 에러 메세지를 확인할 수 있다.

// errors
username: {type: 'minLength', message: 'The username should be longer than 5 chars.', ref: input}

이렇게 에러 타입과 에러 메세지가 담긴 객체를 반환해주어, 이를 에러 표기에 이용할 수 있다.

에러 메세지를 입력하는 법은 간단한데, required 와 같이 매개변수가 필요 없을 때는 보통 바로 메세지를 value 로 작성하면 되고, minLength 와 같이 매개변수가 필요할 때는 객체를 만드는 방식으로 작성하면 된다.

required: "username is required",
minLength: {
  message: "The username should be longer than 5 chars.",
  value: 5,
},

위와 같이 입력된 message 는 사용자의 입력이 유효하지 않을 때, formState.errors 객체 내부에 들어간다.

errors: {
  email: {type: 'required', message: 'email is required', ref: input.bg-red-100},
  password: {type: 'required', message: 'password is required', ref: input},
  username: {type: 'required', message: 'username is required', ref: input}
}

위는 값이 유효하지 않을 때 errors 오브젝트 내부 필드에 들어있는 값이다. 값이 유효하다면 errors 객체는 어떠한 프로퍼티도 갖고 있지 않게 된다.

이렇게 errors 내부에 값이 들어간다는 것을 이용하면 다양한 방법으로 사용자에게 에러를 알릴 수 있따.

예제 코드 1: errors 상태에 따라 생겼다가 사라졌다 하는 HTML Element 를 만들기

{
  // 이 텍스트는 `formState` 내부 `errors` 의 상태에 따라 표기될 수도 있고 표기되지 않을 수도 있다.
  errors.email?.message
}

errors 내부 email 필드에 에러 메세지가 있다면, 위의 텍스트가 표기되고 없다면 표기되지 않을 것이다.

예제 코드 2: HTML Element 에 class 값을 줘서 디자인적으로 표현하기

<input
  {...register("email", {
    required: "email is required"
  })}
  type="email"
  placeholder="Email"
  className={`${errors.email?.message ? `bg-red-100` : `bg-green-100`}`}
/>

errors 내부 email 필드에 에러 메세지가 있다면, tailwind css 를 이용해 디자인을 바꿔주도록 설정했다.

예제 코드 3: 필드에 커스텀 검증 로직 만들기

<input
  {...register("email", {
    validate: {
      notGmail: (value) =>
        // API 를 통한 검증이든 뭐든 할 수 있다.
        !value.includes("@gmail.com") || "Gmail is not allowed.",
    },
  })}
  type="email"
  placeholder="Email"
/>

위와 같이 validate 속성을 이용해 내부에 프로퍼티를 만들고, 프로퍼티의 메서드가 문자열 값을 반환하게만 만들면, 커스텀 검증을 할 수 있다. 문자열이 들어있다면, 검증에 실패한 것으로 보고 에러 메세지까지 같이 전달할 수 있는 형태가 된다.

문제 해결 4: 여러 필드에 걸친 에러나 모든 필드에 걸친 에러 처리하기

export default function ReactHookForm() {
  const {
    register,
    handleSubmit,
    // 에러에 대한 정보를 가지고 있다.
    formState: { errors, isValid },
    setError,
  } = useForm<LoginForm>();

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

    // 하나의 필드에서 발생한 에러가 아니라, 전체 필드에서 발생한 에러 혹은 API 검증 실패 등을 처리할 수 있다.
    if(await isDuplicateAPI()) {
      setError("username", {
        message: "이미 존재하는 username 입니다.",
      });
    }


    if (data.username === data.password) {
      setError("password", {
        message: "username 과 password 가 동일하면 안 됩니다.",
      });
    }

    // 기타, 성별은 '남'에 체크했는데, 주민번호가 '2' 로 시작하는 경우 등 하나의 필드가 아닌 다수의 필드에 대한 검증이 가능하다.
    // setError 가 지원하는 types 에서 MultipleFieldErrors 를 이용하면 된다.
  };
}

onValid() 콜백에서 기본 단일 필드 검증을 통과한 데이터에 대해 API 등으로 혹은 여러 필드에 대한 검증을 여러번 더 거치게 만들 수 있다.

setError() 메서드를 통해 필드 에러를 직접 세팅할 수 있다. 아니면 인터페이스에 allFields 등 필드를 몇개 더 추가해서 그쪽으로 에러를 보내줘도 될 것 같다.

문제 해결 5: 다양한 유틸 메서드 (필드 초기화 등)

유틸 메서드들을 잘 활용하면 개발에서 귀찮았던 여러가지들을 쉽게 구성할 수 있다.

  • watch(): Form 이 가지고 있는 데이터를 실시간으로 모니터링할 수 있다.
  • reset(): Form 에 입력된 값을 초기화할 수 있다.
  • resetField(): Form 의 특정 필드의 값을 초기화할 수 있다.
  • formState 내부
    • isSubmitSuccessful: 성공적으로 submit 이 되었는지 알 수 있다.
    • isValid: 현재 유효한 값인지 알 수 있다.
    • isDirty: 입력된 값이 기본 값인지 체크한다.
    • formState 관련 공식 API 문서 에서 어떠한 값을 지원하는지 잘 나와있다.

유틸 메서드에 대해 더 잘 알고 싶다면, API 문서 를 잘 살펴보는 것이 도움이 될 것 같다.

문제 해결 6: 타입스크립트로 정적 타입 체크하기

interface EnterForm {
  email?: string;
  phone?: string;
}

const Enter: NextPage = () => {
  const { register } = useForm<EnterForm>();
  // ...
}

위와 같이 인터페이스를 정의 후에 useForm 의 제네릭 타입인 TFieldValues 로 설정해주면, react-hook-form 에서 사용하는 타입을 헷갈리지 않을 수 있다. 또한 intellisense 도 지원받을 수 있어서 더 많은 실수를 예방할 수 있다.

팁

팁 1: custom input 에 react hook form 적용시키기

아래와 같은 Input 커스텀 컴포넌트가 있다고 가정할 때,

<Input
  {...register("email")}
  name="email"
  label="Email address"
  type="email"
  required
  register={register("email")}
/>

위와 같이 Input 컴포넌트에 바로 {...register("email")} 을 주면, 결과적으로 이 attributes 가 어떤 태그에 바인딩되는지 알 수 없다. 이 attributes 는 primitive input 엘리먼트를 사용할 때와 동일하게 바인딩 되어야 한다. 그러기 위해서는 아래와 같이 register 를 따로 받은 후

export default function Input({
  register,
  label,
  name,
  kind = "text",
  ...rest
}: InputProps) {

  <input
  id={name}
    {...rest}
    {...register}
    className="appearance-none w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-orange-500 focus:border-orange-500"
  />

}

별개의 primitive input 에 spread operator 를 이용하여 위와 같이 뿌려주어야 한다. 결과적으로 prop 으로 useForm() 의 결과 자체를 가져와서 ... spread operator 로 뿌려주는 것이다.

타입스크립트에서는 이렇게 하기 위해서 interface 를 하나 정의해야 한다.

interface InputProps {
  label: string;
  name: string;
  kind?: "text" | "phone" | "price";
  [key: string]: any;
}

[key: string]: any 는 어떠한 것이 props 를 통해 넘어오더라도 받아들이겠다는 뜻이다.

interface InputProps {
  label: string;
  name: string;
  kind?: "text" | "phone" | "price";
  type: string;
  register: UseFormRegisterReturn;
  required: boolean;
}

물론 위와 같이 [key:string]: any 와 같은 코드를 없애고 모든 props 에 대해 명확히 정의해준다면, 범용성은 작아지지만 더 의도가 명확한 코드가 된다. 이게 실수할 일은 더 적은 코드이다.

레퍼런스

  • https://react-hook-form.com/api/useform/register
반응형
저작자표시 비영리 (새창열림)

'프론트엔드 > React' 카테고리의 다른 글

React Effect Hook 이란?  (0) 2022.10.19
리액트 컴포넌트 밖 변수 선언의 의미 (Feat. 리액트에서 절대 하면 안되는 것 1가지)  (0) 2022.10.14
React 컴포넌트 어떻게 나누고 재사용할 것인가?  (0) 2022.07.24
React Hook Form 소개  (0) 2022.07.02
    '프론트엔드/React' 카테고리의 다른 글
    • React Effect Hook 이란?
    • 리액트 컴포넌트 밖 변수 선언의 의미 (Feat. 리액트에서 절대 하면 안되는 것 1가지)
    • React 컴포넌트 어떻게 나누고 재사용할 것인가?
    • React Hook Form 소개
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바