react-hook-form

react-hook-form 의 다양한 모습

안녕하세요! 오픈소스컨설팅 Playce Dev 팀에서 프론트엔드 개발을 하고있는 강동희 입니다. 이번 포스팅은 React 의 form 라이브러리인 react-hook-form 시리즈의 마지막 포스팅 이며, react-hook-form 을 프로젝트에 도입한 후 5달 동안 react-hook-form 을 사용하면서 어려움을 겪었던 부분, 개선을 해봤던 부분, 다양하게 사용해봤던 부분에 대한 경험을 여러분들께 공유 해볼까 합니다.

모든 Open Source 가 그렇듯이, 저의 케이스가 정답은 아닙니다! 더 좋은 예제나 케이스를 알고 계신다면 댓글로 남겨주시거나 사례를 공유해주시면 감사하겠습니다😁

생성 폼과 공존 하는 수정 폼

다른 프로젝트들과 마찬가지로, 저희 FE 팀에서는 생성을 위한 폼과, 수정을 위한 폼을 하나의 컴포넌트 에서 작업을 합니다.

props 를 통해서 현재의 form 이 Edit 모드 인지, create 모드 인지 판단한 후, edit 모드일 경우, url 의 path param 에서 id 를 꺼내 해당 id 의 데이터를 서버에서 패치 해온 후 폼에 주입하고 있습니다.

Playce Dev 팀의 FE 파트에서는 서버 데이터를 관리하는데 있어서 react-query 라는 라이브러리를 사용하고 있는데, 기존에 데이터를 주입하는 경우 useQuery 가 반환하는 data 를 활용해 useEffect 내부에서 form 의 필드를 reset 하고 있었습니다.

const { data } = useQuery(["list"], getList, {
  enabled: !!id,
});

useEffect(() => {
  if (mode === "edit" && data) {
    reset({
      ...data,
    });
  }
}, [mode, data, reset]);

추후 폼을 개선하는 작업을 하는 도중, useEffect 내부에서 폼에 데이터를 주입 했던 과정을 useQueryonSuccess 옵션에서 하는것이 어떨까 하는 생각이 들었습니다.

useEffect 는 React 에서 부수효과를 일으키는 훅 중 하나로써, 의존배열에 많은 값이 들어가게 된다면 해당 값이 예측하지 못한 순간에 변한다면 개발자가 예측하지 못한 부수효과가 일어난다는 단점이 있습니다.

또한 useQuery 를 통해 반환받는 data 의 초기값과 만약 데이터 패칭에 실패한다면 데이터의 값은 undefined 이기에 useEffect 내부에서 dataundefined 인지를 체크해야 하는 조건문도 들어가야 합니다. (mode 를 비교하는 것 까지 조건에 들어가게 될테구요.)

해당 로직을 onSuccess 에 넣어주게 된다면 코드는 아래와 같아집니다.

useQuery(["list"], getList, {
  enabled: !!id,
  onSuccess: (data: IForm) => {
    reset(data);
  },
});

코드의 길이가 절반이 줄어들었습니다. 또한 부수효과를 일으키는 useEffect 의 사용이 줄었으며, data 의 값이 undefined 인지 체크하는 조건문도 필요가 없게 되었습니다. onSuccessuseQuery 의 결과가 성공일 때 만 실행이 되기 때문입니다. mode 를 체크할 필요가 없는게, path param 을 통해서 받아오는 id 가 없는 경우에 해당 쿼리를 실행하지 않기 때문입니다.

enabled: !!id

동일한 Query key 를 가진 쿼리

아래 예제는 react-query 의 기본 옵션 중 queries – staleTime 을 Infinity 로 설정해놓은 상황에서 발생할 수 있는 예제입니다.

현재 페이지가 어떤 리스트의 상세항목 페이지라고 가정해 봅시다! 현재 페이지에서 사용자에게 데이터를 보여주기 위해 [“list”, 1] 이라는 키를 가진 Query 를 패칭해왔습니다. 만약 해당 Detail 페이지에서 정보를 수정하기 위해 Edit 버튼을 클릭하게 된다면 어떤 일이 발생할까요? Edit 버튼을 클릭하는 순간, Edit 화면으로 라우팅이 되면서 폼에 데이터를 주입하기 위해 쿼리를 패칭해야 합니다.

여기서 문제가 발생합니다. Edit 버튼을 클릭을 했음에도 불구하고, 데이터 패칭이 발생하지 않습니다. 원인은 과도한 api 요청을 막기 위해 staleTimeinfinity 로 설정 해놨다는 데에 있습니다. 하지만 해당 옵션은 프로젝트 전체에 적용하기 위해 설정 해놓은 옵션이기에 해당 Query 만의 옵션으로 해결해야 할 필요성이 있습니다.

위의 상황을 해결하기 위해 useQuery 의 파라미터로 들어가는 옵션 객체의 refetchOnMount 프로퍼티에 always 라는 값을 넣어줍니다. 해당 옵션을 넣어주면 컴포넌트가 마운트 되는 시점에 항상 데이터를 리패칭 할 수 있게 됩니다.

네트워크를 살펴보면 list 라는 이름을 가진 요청을 Edit 폼이 열렸을 때 잘 패칭해온다는 것을 알 수 있습니다.

react-hook-form 의 다양한 Validation

Form 을 통해서 사용자에게 데이터를 입력받는다면 입력받은 데이터를 기획된 내용을 바탕으로 데이터를 검증해야 할 필요가 있습니다. 기본적인 필수입력사항, 최소 길이, 최대값 등을 제외하고, Custom Validation 이 필요하다면 rules 객체에 validate 프로퍼티를 통해서 검증 함수를 넣어줄 수 있습니다.

<InputText
  control={control}
  name={"CustomValidation"}
  rules={{
    validate: (value) => {
      if (condition === 'A') {
        return (value as string).startsWith("A")
          ? undefined
          : "A 조건에 위배되었습니다.";
      }
      if (condition === 'B') {
        return (value as string).startsWith("B") &&
          (value as string).endsWith("B")
          ? undefined
          : "B 조건에 위배되었습니다.";
      }
    },
  }}
/>;

condition 이라는 조건에 따라서 검증 로직에 분기가 발생합니다. 값을 검증한 이후에 데이터에 문제가 없다면 명시적으로 undefined 를 리턴, 문제가 있다면 실패 메세지를 리턴 합니다. undefined 로 리턴 하는 이유는, errors 객체 내부에 에러를 존재하지 않게 하기 위함 입니다. (입력된 값이 검증이 된 경우 errors 객체는 빈 객체가 됩니다. 따라서 해당 name 의 프로퍼티는 undefined 입니다.)

validation 프로퍼티에는 함수 이외에 객체가 들어갈 수 있는데, 객체를 통해서 다양한 Custom Validation 을 구현할 수 있습니다.


deps

A 필드의 데이터에 따라서 B 필드가 영향을 받아야 한다는 상황이 있다면 어떤 방식으로 구현할 수 있을까요?

react-hook-form 은 위 상황을 쉽게 해결할 수 있도록 도와줍니다. rules 에 들어가는 객체에는 deps 라는 프로퍼티가 있는데, 해당 프로퍼티를 의존 배열처럼 활용할 수 있습니다.

// A 필드
<Radio
  control={control}
  options={A}
  rules={{ deps: ["B"] }}
  name={"Name"}
/>;

단순히 B 필드가 A 필드에 영향을 받기를 원한다면 deps: [“B”] 를 통해 해결할 수 있습니다. 해당 필드는 이제 A 필드의 상태에 영향을 받게 됩니다. A 필드에 선택된 값이 바뀐다면 B 필드 또한 같이 영향을 받게 됩니다. (예를 들면 B 필드의 Validation 상태가 true 일 지라도, A 필드의 상태가 바뀐다면 B 필드의 Validation 을 다시 체크하게 됩니다.)


hookform/resolvers

rules 를 통해 Validation 을 체크하는 방법 이외에 Validation 라이브러리를 사용하는 방법도 있습니다.

인프랩의 홍시 님께서 작성해주신 글에는 Validation 을 위해서 Class-Validator 을 활용 했지만, 여기서는 간단하게 yup 이라는 라이브러리를 통해서 Validation 라이브러리를 접목해보도록 하겠습니다.

🤔인프랩 글 보러가기 >

먼저 @hookform/resolvers 를 설치해줍니다. 또한 검증 라이브러리인 yup 을 설치해줍니다.

npm install @hookform/resolvers yup
yarn add @hookform/resolvers yup

yup 사용은 생각보다 간단합니다. 먼저 사용을 위해 라이브러리를 import 해옵니다

import * as yup from "yup";

이후 Validation 을 위한 스키마를 정의합니다.

const schema = yup.object().shape({
  name: yup
    .string()
    .required("반드시 입력해주세요.")
    .max(10, "최대 10글자까지 입력 가능합니다."),
  email: yup
    .string()
    .required("반드시 입력해주세요.")
    .matches(
      /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/,
      "이메일 형식에 맞지 않습니다."
    ),
  phone: yup.string().when("occupation", {
    is: "professor",
    then: yup.string().required("직업이 교수님이라면 반드시 입력해주세요!!!!"),
  }),
});

이후 정의한 스키마useForm 에 넣어줍니다.

import { yupResolver } from "@hookform/resolvers/yup";

useForm<IForm>({
  resolver: yupResolver(schema),
});

이렇게 yup 혹은 다른 Validation 라이브러리를 활용한다면 파편화 되어있는 Validation 로직을 한번에 모아서 관리할 수 있다는 장점이 생깁니다.

DefaultValue 를 useForm 에 넣지 않고 useEffect 에서 reset 을 한 이유

useForm 을 사용하는 경우에는 반드시 defaultValue 를 넣어줘야 useForm 을 통해 관리되는 필드들의 초기값이 undefined 로 관리되지 않습니다. 그렇기 때문에 defaultValue 에 기본 값들을 넣어줍니다.

const defaultValues = {
  id: "",
  pwd: "",
  name: "",
  phone: "",
  email: "",
  occupation: "professor",
  identity: "child",
  score: "major",
  level: "first",
};

const {
  control,
  handleSubmit: onSubmit,
  setValue,
  reset,
  watch,
} = useForm<IForm>({
  defaultValues,
  mode: "all",
});

위 코드를 통해서 실행된 화면입니다. 확실한 확인을 위해서 Network 탭에서 쓰로틀링을 걸어줬습니다 (Fast 3g)

라디오 버튼에 초기값이 주입 되었다가, 수정을 위해 기존의 입력된 값이 필드에 다시 재 주입 됩니다. 이는 비효율적인 연산이며, 사용자에게 버그라고 인식될 소지 도 있습니다.

이를 위해 초기 기본 값은 빈 string 을 넣어서 undefined 를 막아주고, Create 모드 일 경우에만 useEffect 를 통해 기본 값을 주입시켜 줍니다. 그리고 Edit 모드인 경우는 위에 소개드린 방식대로 onSuccess 에서 그대로 값을 주입합니다.

const defaultValues = {
  id: "",
  pwd: "",
  name: "",
  phone: "",
  email: "",
  occupation: "",
  identity: "",
  score: "",
  level: "",
};

const {
  control,
  handleSubmit: onSubmit,
  setValue,
  reset,
  watch,
} = useForm<IForm>({
  defaultValues,
  mode: "all",
});

// Edit 모드를 위한 Query Fetching
useQuery(["list"], getList, {
  enabled: !!id,
  refetchOnMount: "always",
  onSuccess: (data: IForm) => {
    reset(data);
  },
  select: (data: IForm[]) => data.find((data) => data.id === id) as IForm,
});

// Create 모드를 위해 폼에 데이터 초기 주입
useEffect(() => {
  if (mode === "create") {
    reset({
      ...watch(),
      occupation: "professor",
      identity: "father",
      score: "grand",
      level: "first",
    });
  }
}, []);

결과를 확인해보면 Edit 모드일 때 값이 변화 없이 기존의 값만 화면에 노출이 되는걸 확인할 수 있습니다!

마무리하며

길다면 길고, 짧다면 짧은 다섯 달의 react-hook-form 경험을 기록해 봤습니다. 이번 시리즈를 통해서 react-hook-form 의 장점을 캐치 하여 많은 프로젝트에서 react-hook-form 을 도입 했으면 좋겠다는 생각을 해봅니다.

또한 이번 시리즈를 통해서 react-hook-form 에 대한 다양한 케이스가 공개 되어서 많은 레퍼런스들이 생겼으면 좋겠습니다.

세 편의 시리즈를 읽어주셔서 감사합니다! 작성된 코드를 확인해보고 싶으시다면 아래 Github 을 참고해주세요!

Github >

시리즈 글 살펴보기

  1. react-hook-form 을 활용해 효과적으로 폼 관리하기
  2. react-hook-form 과 MUI 함께 사용하기
강동희
Developer

오픈소스컨설팅 Playce Dev Team 소속으로 오늘을 바탕으로 내일이 더 기대되는 프론트엔드 개발자 강동희입니다. 빠르게 변화하는 기술들에 관심이 많으며 지식 공유에 큰 가치를 두고 있습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *

One reply on “react-hook-form 다양하게 사용하기”

  • 아라한사
    2023년 01월 18일 at 12:30 am

    감사합니다^^