Logo

React Hook Form 라이브러리 사용법

지난 포스팅에서는 커스텀(custom) React Hook을 사용하여 양식(form) UI를 구현해보았는데요. 이번 포스팅에서는 이와 유사한 방식으로 React Hook 기반의 API를 제공하는 React Hook Form 라이브러리에 대해서 알아보려고 합니다.

새로운 라이브러리를 배우는 가장 효과적인 방법은 그 라이브러리를 이용해서 무언가를 만들어보는 것이 겠죠? 본 포스팅을 끝까지 따라오시면 아래와 같은 로그인 폼(form)을 만드실 수 있으실 거 에요 😁

패키지 설치

React Hook Form 라이브러리는 자바스크립트 패키지 매니저인 npm을 사용하여 간편하게 React 프로젝트에 설치할 수 있습니다.

터미널
$ npm install react-hook-form

JSX 마크업

실습 예제로 웹사이트에서 흔하게 볼 수 있는 로그인 폼(form)을 구현해보도록 하겠습니다. 이메일 입력란과 비밀번호 입력란, 그리고 로그인 버튼으로 이루어진 양식을 표현하는 JSX 코드를 작성해보겠습니다.

function LoginForm() {
  return (
    <form>
      <label htmlFor="email">이메일</label>
      <input id="email" type="email" placeholder="test@email.com" />
      <label htmlFor="password">비밀번호</label>
      <input id="password" type="password" placeholder="****************" />
      <button type="submit">로그인</button>
    </form>
  );
}

export default Form;

딱히 특별할 것이 없는 전형적인 2개의 입력란과 하나의 제출 버튼으로 이루어진 양식입니다.

React Hook Form 연결

React Hook Form는 어떤 양식 UI에도 연결하기 쉬운 API를 제공하고 있습니다. 얼마나 쉬운지, 한번 이 로그인 폼에 React Hook Form을 연결해볼까요?

react-hook-form 패키지로 부터 useForm() 훅(hook) 함수를 불러온 후, 컴포넌트 함수 안에서 이 함수를 호출합니다. 그러면 결과 객체로 부터 register() 함수와 handleSubmit() 함수를 얻을 수 있는데요.

register() 함수를 이용하여 각 입력란을 등록하고, handleSubmit() 함수를 이용하여 form 요소에서 발생하는 submit 이벤트를 처리하도록 해줍니다.

import { useForm } from "react-hook-form";
function LoginForm() {
  const { register, handleSubmit } = useForm();
  return (
    <form onSubmit={handleSubmit((data) => alert(JSON.stringify(data)))}>
      <label htmlFor="email">이메일</label>
      <input
        id="email"
        type="email"
        placeholder="test@email.com"
        {...register("email")}
      />
      <label htmlFor="password">비밀번호</label>
      <input
        id="password"
        type="password"
        placeholder="****************"
        {...register("password")}
      />
      <button type="submit">로그인</button>
    </form>
  );
}

export default Form;

이제 로그인 폼에서 이메일과 비밀번호를 입력 후에 로그인 버튼을 클릭하면 입력한 내용이 브라우저에서 알람(alert) 메시지로 뜰 것입니다.

중복 제출 방지

로그인폼에서 제출 이벤트 처리가 미처 종료되기 전에 사용자가 다시 로그인 버튼을 클릭할 경우 양식이 중복해서 제출되는 문제가 발생할 수 있습니다. 따라서 사용자가 로그인 버튼을 클릭하지 마자, 해당 버튼을 비활성화 시켰다가, 이벤트 처리가 완료되었을 때, 제출 버튼을 다시 활성화 시켜주는 것이 안전합니다.

useForm() 훅(hook) 함수가 반환하는 객체의 formState 속성은 양식이 현재 어떤 상태인지를 담고 있는데요. 이 formState으로 부터 isSubmitting 속성을 읽어서 양식이 현재 제출 중인 상태인지 아닌지를 알아낼 수 있습니다.

따라서 로그인 버튼의 disabled 속성에 이 isSubmitting 값을 설정해주면 로그인 버튼이 양식의 제출 처리가 끝날 때까지 비활성화가 될 것입니다. 시각적으로 로그인 버튼이 비활성화되는 것을 확인하기 위해서 handleSubmit 함수에 넘기는 코드에 일부로 1초 지연을 발생시켰습니다.

React Hook Form이 워낙 빠르기 때문에 이렇게 하지 않으면 유관으로 파악하기 어렵더라고요… 😆

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

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { isSubmitting },  } = useForm();

  return (
    <form
      onSubmit={handleSubmit(async (data) => {        await new Promise((r) => setTimeout(r, 1000));        alert(JSON.stringify(data));      })}
    >
      <label htmlFor="email">이메일</label>
      <input
        id="email"
        type="email"
        placeholder="test@email.com"
        {...register("email")}
      />
      <label htmlFor="password">비밀번호</label>
      <input
        id="password"
        type="password"
        placeholder="****************"
        {...register("password")}
      />
      <button type="submit" disabled={isSubmitting}>        로그인
      </button>
    </form>
  );
}

export default Form;

자바스크립트에서 프로그램의 실행을 지연시키는 방법에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고 바랍니다.

입력값 검증

이 번에는 로그인 폼에 기본적인 입력값 검증을 추가해보도록 하겠습니다.

당연히 이메일과 비밀번호는 모두 필수 입력이겠죠? 뿐만 아니라, 이메일 경우에는 유효한 형식이 있을 것이고, 비밀번호의 경우 최소한의 길이가 있다고 가정해보겠습니다.

이러한 입력값 검증은 입력란을 등록할 때, register() 함수의 두 번째 인자로 옵션을 넘기면 되는데요.

HTML에서 입력란 검증을 위해 기본적으로 제공되는 required, pattern, minLength와 같은 검증 타입을 사용할 수 있으며, 각 검증 타입이 실패했을 때 보여줄 오류 메시지도 설정할 수 있습니다.

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

function LoginForm({
  onSubmit = async (data) => {
    await new Promise((r) => setTimeout(r, 1000));
    alert(JSON.stringify(data));
  },
}) {
  const {
    register,
    handleSubmit,
    formState: { isSubmitting, isSubmitted, errors },  } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label htmlFor="email">이메일</label>
      <input
        id="email"
        type="text"
        placeholder="test@email.com"
        aria-invalid={          isSubmitted ? (errors.email ? "true" : "false") : undefined        }        {...register("email", {
          required: "이메일은 필수 입력입니다.",          pattern: {            value: /\S+@\S+\.\S+/,            message: "이메일 형식에 맞지 않습니다.",          },        })}
      />
      {errors.email && <small role="alert">{errors.email.message}</small>}      <label htmlFor="password">비밀번호</label>
      <input
        id="password"
        type="password"
        placeholder="****************"
        aria-invalid={          isSubmitted ? (errors.password ? "true" : "false") : undefined        }        {...register("password", {
          required: "비밀번호는 필수 입력입니다.",          minLength: {            value: 8,            message: "8자리 이상 비밀번호를 사용하세요.",          },        })}
      />
      {errors.password && <small role="alert">{errors.password.message}</small>}      <button type="submit" disabled={isSubmitting}>
        로그인
      </button>
    </form>
  );
}

export default Form;

이제 입력란에 유효하지 않은 값을 입력했을 경우 검증이 실패하여 로그인 폼이 제출되지 않습니다. 대신 formState 속성의 errors 객체에 오류 내용이 저장되는데요. 이 값을 읽어서 각 입력란 아래에 오류 메시지가 나오도록 해주었습니다.

스크린 리더(screen reader) 사용자를 위해서 각 입력란에 aria-invalid 속성을 사용하고, 에러 메시지를 표시해주는 영역에는 role="alert"을 사용했습니다. 웹 접근성(accessibility) 측면에서 매우 중요한 부분입니다.

테스트 작성

마지막으로 우리가 구현한 로그인 폼이 정상적으로 동작하는 테스트해보면 좋을 것 같습니다.

Testing Library를 사용하여 로그인폼이 검증이 되고 제출이 되는지 확인하는 테스트 코드를 작성해보겠습니다.

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LoginForm from "./LoginForm";

test("validates form", async () => {
  const onSubmit = jest.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  const button = screen.getByRole("button", { name: "로그인" });

  userEvent.click(button);

  const alerts = await screen.findAllByRole("alert");
  expect(alerts).toHaveLength(2);
  expect(onSubmit).not.toHaveBeenCalled();
});

test("submits form", async () => {
  const onSubmit = jest.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  const email = screen.getByRole("textbox", { name: "이메일" });
  const password = screen.getByLabelText("비밀번호");
  const button = screen.getByRole("button", { name: "로그인" });

  userEvent.type(email, "test@email.com");
  userEvent.type(password, "Test1234");

  userEvent.click(button);

  await waitFor(() =>
    expect(onSubmit).toHaveBeenCalledWith(
      {
        email: "test@email.com",
        password: "Test1234",
      },
      expect.anything()
    )
  );

  expect(screen.queryAllByRole("alert")).toHaveLength(0);
});

전체 코드

본 포스팅에서 작성한 모든 코드는 아래에서 확인하고 직접 실행해보실 수 있습니다.

마치면서

지금까지 React Hook Form 라이브러리를 활용해서 실제 웹사이트에서도 사용할 수 있을 법한 수준의 로그인 폼을 큰 노력을 들이지 않고 구현해보았는데 어떠셨나요?

사실 양식 UI를 밑바닥부터 직접 구현한다는 것은 상당히 고려할 부분이 많아서 의외로 쉽지 않은 작업입니다. 그래서 실무에서는 본 포스팅에서 다룬 React Hook Form이나 Formik과 같은 라이브러리를 사용하는 경우가 많은데요.

그래도 학습을 위해서 라이브러리를 사용하지 않고 직접 양식 UI를 구현해보고 싶으시다면 아래 포스팅이 도움이 되실 겁니다.