react-hook-form

react-hook-form

안녕하세요. Playce Dev 팀에서 FE 개발을 하고 있는 강동희 입니다. 웹 프론트엔드를 개발할 때 데이터를 다루는 방법은 여러가지가 있습니다. (차트를 통해 데이터를 시각화, 폼을 통해 데이터를 수집, 테이블을 통해 데이터를 나열 등) 이번 시리즈에서는 그 중에서 사용자로부터 데이터를 수집하는 방법인 form 에 대해서 다뤄볼 생각입니다.
Playce Dev 팀의 Frontend 파트의 새로운 아키텍처 에서는 form 컴포넌트 구현에 있어서 React 프로젝트에 react-hook-form 라이브러리를 접목했습니다. 기존에는 제어 컴포넌트로 폼을 다루었는데, 해당 방식에서 벗어나서 비제어 컴포넌트 방식으로 form 을 관리하고 개발하고 있습니다. 이번 시리즈에서는 지난 5달 동안 react-hook-form 라이브러리를 다루며 공부 했고, 저희 FE 파트가 어떻게 react-hook-form 을 사용하고 있는지에 대한 경험을 공유하도록 하겠습니다.

이번 포스팅은 시리즈의 첫 번째 포스팅이며, react-hook-form 을 도입하게 된 계기를 간단한 예제를 만들어보며 알아보고, react-hook-form 의 핵심인 useForm 에 대해서 알아보도록 하겠습니다!

라이브러리 없이 제어 컴포넌트로 폼을 다뤄보자

먼저 라이브러리를 사용하지 않고 제어 컴포넌트 로 폼을 다뤄보겠습니다.

React 에서 제어 컴포넌트란 React 를 통해 제어하게 되는 컴포넌트를 말합니다.

간단한 폼을 만들어봅시다.
우리는 이 폼을 통해서 유저로부터 직업, 이름, 아이디, 비밀번호, 전화번호, 이메일을 받을 생각입니다.
React 에서 폼을 다루려면 무엇을 해야할까요?
먼저 입력되는 값을 다루기 위한 state 가 필요합니다. 입력받을 값을 담을 state 들을 선언 해줍니다.

import React, { useState } from "react";

function NoHookForm() {
  const [occupation, setOccupation] = useState(Occupation.Professor);
  const [name, setName] = useState("");
  const [id, setId] = useState("");
  const [pwd, setPwd] = useState("");
  const [phone, setPhone] = useState("");
  const [email, setEmail] = useState("");

  return (
    <form></form>
  );
}


export default NoHookForm;

state 를 선언했다면, 입력되는 값을 <input/> 태그에 연결해주고, 태그에 입력받고 변하는 값을 추적할 이벤트 함수를 선언 해줍니다.

import React, { useState } from "react";
import InputText from "../component/InputText";
import { Label, Row } from "../style";

enum Occupation {
  Student = "student",
  Professor = "professor",
}

function NoHookForm() {
  const [occupation, setOccupation] = useState(Occupation.Professor);
  const [name, setName] = useState("");
  const [id, setId] = useState("");
  const [pwd, setPwd] = useState("");
  const [phone, setPhone] = useState("");
  const [email, setEmail] = useState("");

  const handleName = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.currentTarget.value);
  };
  const handleId = (event: React.ChangeEvent<HTMLInputElement>) => {
    setId(event.currentTarget.value);
  };
  const handlePwd = (event: React.ChangeEvent<HTMLInputElement>) => {
    setPwd(event.currentTarget.value);
  };
  const handlePhone = (event: React.ChangeEvent<HTMLInputElement>) => {
    setPhone(event.currentTarget.value);
  };
  const handleEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(event.currentTarget.value);
  };
  const handleOccupation = (event: React.ChangeEvent<HTMLInputElement>) => {
    setOccupation(event.currentTarget.value as Occupation);
  };

  return (
    <form>
      <Row>
        <Label>Occupation: </Label>
        <input
          type="radio"
          name="occupation"
          value="professor"
          defaultChecked
          onChange={handleOccupation}
        />
        <input
          type="radio"
          name="occupation"
          value="student"
          onChange={handleOccupation}
        />
      </Row>
      <Row>
        <Label>name: </Label>
        <InputText value={name} onChange={handleName} />
      </Row>
      <Row>
        <Label>id: </Label>
        <input type="text" value={id} onChange={handleId} />
      </Row>
      <Row>
        <Label>pwd: </Label>
        <input type="text" value={pwd} onChange={handlePwd} />
      </Row>
      <Row>
        <Label>phone: </Label>
        <input type="text" value={phone} onChange={handlePhone} />
      </Row>
      <button onClick={handleSubmit}>Submit</button>
    </form>
  );
}


export default NoHookForm;

e-mail 필드는 occupation 에 따라서 Professor 일 때만 값을 입력받을 수 있게 해달라는 기획자의 요청이 들어왔습니다! occupation 필드의 값을 추적해 값이 Professor 일때만 노출될 수 있도록 조건을 부여 해줍니다.

{ occupation === Occupation.Professor ? (
  <Row>
    <Label>e-mail: </Label>
    <input type="text" value={email} onChange={handleEmail} />
  </Row>
) : null 
}

이번엔 입력받은 값을 검증할 수 있는 validation rule 이 필요하다고 기획자의 요청이 들어왔습니다. 여러가지 검증 규칙이 있지만, 모든 validation 을 처리하려면 예제가 너무 길어기에 여기선 단순하게 입력받는 이름, id 의 글자 수 만 체크하도록 하겠습니다.

validation 을 위해서 에러 상태를 담고있는 state 를 선언해주고, validation 체크는 유저가 제출 버튼을 누르는 시점에서 체크할 수 있도록 handleSubmit 함수에서 검증하도록 하겠습니다.

const [errors, setErrors] = useState({
  name: {
    invalid: false,
    message: "이름이 너무 깁니다.",
  },
  id: {
    invalid: false,
    message: "id는 3글자 이상, 20글자 이하여야 합니다.",
  },
  pwd: {
    invalid: false,
    message: "비밀번호는 10자 이하여야 합니다.",
  },
  phone: {
    invalid: false,
    message: "전화번호 형식에 맞지 않습니다.",
  },
  email: {
    invalid: false,
    message: "이메일 형식에 맞지 않습니다.",
  },
});

const handleSubmit = (event: React.SyntheticEvent) => {
  event.preventDefault();
  if (name.length > 10) {
    setErrors((prev) => ({
      ...prev,
      name: {
        ...prev.name,
        invalid: true,
      },
    }));
  }
  if (id.length < 3 || id.length > 20) {
    setErrors((prev) => ({
      ...prev,
      id: {
        ...prev.id,
        invalid: true,
      },
    }));
  }
  // ...등.. 에러 처리 이후 로직 필요.

};

이렇게 폼을 완성했을 때 폼의 상태는 아래와 같습니다. (빠르게 스킵 하셔도 되는 코드입니다!)

import React, { useState } from "react";
import InputText from "../component/InputText";
import { Label, Row } from "../style";

enum Occupation {
  Student = "student",
  Professor = "professor",
}

function NoHookForm() {
  const [occupation, setOccupation] = useState(Occupation.Professor);
  const [name, setName] = useState("");
  const [id, setId] = useState("");
  const [pwd, setPwd] = useState("");
  const [phone, setPhone] = useState("");
  const [email, setEmail] = useState("");
  const [errors, setErrors] = useState({
    name: {
      invalid: false,
      message: "이름이 너무 깁니다.",
    },
    id: {
      invalid: false,
      message: "id는 3글자 이상, 20글자 이하여야 합니다.",
    },
    pwd: {
      invalid: false,
      message: "비밀번호는 10자 이하여야 합니다.",
    },
    phone: {
      invalid: false,
      message: "전화번호 형식에 맞지 않습니다.",
    },
    email: {
      invalid: false,
      message: "이메일 형식에 맞지 않습니다.",
    },
  });

  const handleName = (event: React.ChangeEvent<HTMLInputElement>) => {
    setName(event.currentTarget.value);
  };
  const handleId = (event: React.ChangeEvent<HTMLInputElement>) => {
    setId(event.currentTarget.value);
  };
  const handlePwd = (event: React.ChangeEvent<HTMLInputElement>) => {
    setPwd(event.currentTarget.value);
  };
  const handlePhone = (event: React.ChangeEvent<HTMLInputElement>) => {
    setPhone(event.currentTarget.value);
  };
  const handleEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(event.currentTarget.value);
  };

  const handleOccupation = (event: React.ChangeEvent<HTMLInputElement>) => {
    setOccupation(event.currentTarget.value as Occupation);
  };

  const handleSubmit = (event: React.SyntheticEvent) => {
    event.preventDefault();
    if (name.length > 10) {
      setErrors((prev) => ({
        ...prev,
        name: {
          ...prev.name,
          invalid: true,
        },
      }));
    }
    if (id.length < 3 || id.length > 20) {
      setErrors((prev) => ({
        ...prev,
        id: {
          ...prev.id,
          invalid: true,
        },
      }));
    }
    // ...등.. 에러 처리 이후 로직 필요.
  };

  return (
    <form>
      <Row>
        <Label>Occupation: </Label>
        <input
          type="radio"
          name="occupation"
          value="professor"
          defaultChecked
          onChange={handleOccupation}
        />
        <input
          type="radio"
          name="occupation"
          value="student"
          onChange={handleOccupation}
        />
      </Row>
      <Row>
        <Label>name: </Label>
        <InputText value={name} onChange={handleName} />
        {errors.name.invalid ? (
          <p className="error">{errors.name.message}</p>
        ) : null}
      </Row>
      <Row>
        <Label>id: </Label>
        <input type="text" value={id} onChange={handleId} />
        {errors.id.invalid ? (
          <p className="error">{errors.id.message}</p>
        ) : null}
      </Row>
      <Row>
        <Label>pwd: </Label>
        <input type="text" value={pwd} onChange={handlePwd} />
      </Row>
      <Row>
        <Label>phone: </Label>
        <input type="text" value={phone} onChange={handlePhone} />
      </Row>
      {occupation === Occupation.Professor ? (
        <Row>
          <Label>e-mail: </Label>
          <input type="text" value={email} onChange={handleEmail} />
        </Row>
      ) : null}
      <button onClick={handleSubmit}>Submit</button>
    </form>
  );
}

export default NoHookForm;

제어 컴포넌트로 폼을 다루기 위해서 하나하나 state 를 선언해주고, 해당 state 를 다루기 위해서 또 핸들링 함수를 만들어야 하고, 에러를 위한 state, 또 검증을 위한 함수.. 지금은 아주 단순한 validation check 만 했기 때문에 코드가 간소화 되었지만, 모든 유효성 검증을 한다면 코드는 더더욱 길어질 것입니다.

코드의 길이도 문제지만, 또 다른 문제점이 있습니다. React 에서 컴포넌트 리랜더링이 발생하는 조건 중 하나는 state 의 값이 변했을 때 입니다. 현재 폼에선 모든 값이 state 로 연결되어 있으며 하나의 값이 변할때 마다 여러개의 자식 컴포넌트 들에서 무수히 많은 리랜더링이 발생합니다. 이는 개발자가 예측한 랜더링이 아닌, 불필요한 랜더링으로서 불필요한 연산으로 생각할 수 있습니다.

여러분들은 이 단순한 폼을 처리하기 위해서 너무 많은 state 와 함수가 담겨져 있다고 생각하지 않나요?

react-hook-form (with. Typescript)

제어 컴포넌트를 통해 폼을 다뤄봤으니, 이제 form 라이브러리를 활용해 폼을 다뤄보도록 하겠습니다!

현재 React 진영에서 가장 많이 사용하고 있는 라이브러리는 react-hook-form 입니다. 해당 라이브러리 외에 formik 이라는 라이브러리가 있지만, 트렌드를 살펴보면 현재 다운로드 횟수를 50만 회를 앞지를 정로도 react-hook-form 의 사용 횟수가 늘어났습니다.

npm-trends-react-hook-form-with-formik

formik 이 먼저 나왔지만, react-hook-form 의 지속적인 업데이트와 더욱 빠른 마운트 속도 차이가 현재의 차이를 만들지 않았나 싶습니다.


react-hook-form 톺아보기

먼저 react-hook-form 을 설치합니다.

npm i react-hook-form

yarn add react-hook-form

컴포넌트에서 react-hook-form 을 사용해봅시다.

먼저 useForm 훅을 사용합니다. 해당 훅에 파라미터로 객체를 넘길 수 있는데 해당 객체의 타입을 보도록 합시다.

export type UseFormProps<TFieldValues extends FieldValues = FieldValues, TContext = any> = Partial<{
    mode: Mode;
    reValidateMode: Exclude<Mode, 'onTouched' | 'all'>;
    defaultValues: DefaultValues<TFieldValues>;
    resolver: Resolver<TFieldValues, TContext>;
    context: TContext;
    shouldFocusError: boolean;
    shouldUnregister: boolean;
    shouldUseNativeValidation: boolean;
    criteriaMode: CriteriaMode;
    delayError: number;
}>;

const model = useForm({
  mode: "onChange",
  defaultValues: {},

});

react-hook-form 을 활용하는 데 여러가지 configuration 옵션이 들어갈 수 있습니다. 그 중 우리는 modedefaultValues 를 가장 많이 활용합니다.

mode 옵션은 validation 전략을 설정하는 데 활용합니다. onSubmit, onChange, onBlur, all 등의 옵션이 있습니다. 주의해야 할 점은 modeonChange 에 놨을 때 다수의 리렌더링이 발생할 수 있어 성능에 영향을 끼칠 수 있다고 합니다.

defaultValues 옵션은 form 에 기본 값을 제공하는 옵션입니다. 주의해야 할 점은 react-hook-form 을 사용할 때 기본값을 제공하지 않는 경우 input 의 초기값은 undefined 로 관리가 됩니다.

훅을 통해서 리턴 받는 값의 타입을 알아봅시다!

export type UseFormReturn<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
    watch: UseFormWatch<TFieldValues>;
    getValues: UseFormGetValues<TFieldValues>;
    getFieldState: UseFormGetFieldState<TFieldValues>;
    setError: UseFormSetError<TFieldValues>;
    clearErrors: UseFormClearErrors<TFieldValues>;
    setValue: UseFormSetValue<TFieldValues>;
    trigger: UseFormTrigger<TFieldValues>;
    formState: FormState<TFieldValues>;
    resetField: UseFormResetField<TFieldValues>;
    reset: UseFormReset<TFieldValues>;
    handleSubmit: UseFormHandleSubmit<TFieldValues>;
    unregister: UseFormUnregister<TFieldValues>;
    control: Control<TFieldValues, TContext>;
    register: UseFormRegister<TFieldValues>;
    setFocus: UseFormSetFocus<TFieldValues>;

};

훅을 통해서 리턴 받는 객체에는 다양한 함수와 객체들이 담겨있습니다! 자주 사용하는 여러 기능들을 컴포넌트를 구현 하면서 하나하나 알아보도록 합시다.

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

interface IForm {
  occupation: string;
  id: string;
  name: string;
  pwd: string;
  email: string;
  phone: string;
}

function ReactHookForm() {
  const {
    register,
    formState: {errors},
    watch,
    reset,
    handleSubmit,
    getValues,
    setError,
    setFocus
  } = useForm<IForm>({
    mode: "onSubmit",
    defaultValues: {
      occupation: "student",
      id: "",
      name: "",
      pwd: "",
      email: "",
      phone: "",
    },
  });
  return ();

}

register

먼저 useForm 을 통해서 컨트롤 할 폼 객체를 리턴받아서 destructuring 합니다. register 함수를 꺼내서 사용할 것인데, 해당 함수를 통해서 우리는 input 태그를 다룰수 있습니다.

<input
  type="text"
  {...register("email", {
    pattern: {
      value:
        /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
      message: "이메일 형식에 맞지 않습니다.",
    },
  })}

/>;

register 함수의 첫 번째 매개변수로는 name 을 줍니다. 해당 필드를 다루게 될 key 값으로써 반드시 들어가야 하는 값입니다. 두 번째 값으론 options 객체가 들어가는데, 해당 객체에는 유효성 검사를 위한 프로퍼티들이 들어갈 수 있습니다. (required, min, max, minLength, maxLength, pattern 등..)

유효성 검사를 위해 value 만을 줄수도 있지만, value, message 로 구성된 객체를 줌으로써 해당 에러에 대한 구체적인 메세지를 제공할 수도 있습니다.

register("name", {required: true, minLength: 10});
register("name", {required: "해당 필드는 필수입니다.", minLength: {
  value: 3,
  message: "3글자 이상 입력해주세요."
}
});

formState

register 함수에 validation 을 넣어줬다면, 에러에 대한 정보를 어디서 찾아야 하는지 궁금하실 겁니다. 에러에 대한 정보는 formState 객체의 errors 에 들어있습니다.

에러가 존재하지 않다면 해당 객체는 빈 객체입니다. 만약 검증을 통해 에러가 발생한다면 해당 객체에 name 의 객체가 생기고, 그 객체 속에 error 의 타입과 메세지가 담겨져 있습니다.

없었는데..
생겼습니다..
<Row>
  <Label>email: </Label>
  <input
    type="text"
    {...register("email", {
      pattern: {
        value:
          /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
        message: "이메일 형식에 맞지 않습니다.",
      },
    })}
  />
  {errors?.email ? <p className="error">{errors.email?.message}</p> : null}

</Row>;

formState 에는 errors 뿐만 아니라 엄청나게 많은 유용한 정보가 담겨있습니다. submit 한 횟수를 알 수 있는 submitCount, 기본 값이 수정된 필드들이 담겨있는 dirtyFields (dirtyFields 를 사용하기 위해선 defaultValues 를 제공해야 합니다.) 사용자에 의해 수정된 필드들이 담겨져있는 touchedFields 에러가 있는지 알 수 있는 isValid 등 이 있습니다.

useForm – FormState 바로가기


watch

비제어 컴포넌트로 폼을 구현할 때 조건에 따라서 다르게 필드를 노출해야 하는 상황이 있었습니다. 이런 상황에서 우리는 watch 함수를 활용할 수 있습니다. watch 함수는 폼에 입력된 값을 구독하여 실시간으로 체크할 수 있게 해주는 함수입니다.

const {id, name, pwd, ...watch} = watch(); //전체 필드를 리턴

const id = watch("id");

매개변수를 주지 않는다면 전체 값을 관찰할 수 있으며 매개변수 (name) 를 준다면 해당 값만 관찰할 수 있습니다.

{watch("occupation") === "professor" ? (
  <Row>
    <Label>phone: </Label>
    <ControlInputText<IForm> control={control} name="phone" />
  </Row>
) : null
}

주의해야 할 점은, 만약 관찰하려는 필드에 defaultValue 를 주지 않는다면 초기 값이 undefined 로 관리가 된다는 점입니다.


getValues

react-hook-form 에서 값을 추적할 수 있는 방법은 두 가지가 있습니다. 첫 번째는 위에서 소개한 watch 함수를 활용하는 것이고, 두 번째는 getValues 함수를 활용하는 것 입니다. 두 방법 모두 필드에 입력된 값을 받을수 있지만, 차이점이 있습니다.

watch 함수가 입력된 값을 추적하고 반환하며 해당 값에 따라서 리렌더링을 일으키는 반면에, getValues 는 값을 반환하지만 리렌더링을 발생시키지 않고 해당 값을 추적하지 않는다는 점 입니다.

const handleEvent = (event) => {
  const value = getValues("name");
  setState(value);
}

Reset

해당 폼으로 create 기능 뿐만 아니라 edit 기능까지 커버해야 합니다. 사용자가 edit 버튼을 클릭해 정보를 수정해야 한다면, 우린 비동기 데이터를 활용해 기존의 데이터를 폼에 뿌려줘야 합니다.

이런 경우엔 reset 이라는 함수를 활용해 볼 수 있습니다.

useEffect(() => {
  const data = fetch(api).then(res => res.json());
  reset({
    name: data.name,
    id: data.id,
    ...data
  })
}, []
);

현재 저희 팀에서는 서버 데이터를 위해 react-query 라는 라이브러리를 사용하고 있습니다. 저희는 react-query 에 비동기 플로우를 맡기고 있기 때문에, reset 에 관한 로직을 useQueryonSuccess 에 넣어줬습니다.

이렇게 코드를 작성한 경우에 useEffect 를 줄일수 있고, 보다 확실한 타이밍에 reset 을 해줌으로써 안정적인 플로우를 구현할 수 있습니다.

useProject(id, {
  enabled: !!id,
  refetchOnMount: "always",
  onSuccess: (projectData: IProject) => {
    reset(projectData);
  },
  [reset],
});

handleSubmit, setError, setFocus

폼에서 데이터를 입력한다면 사용자는 등록 버튼을 누릅니다. 이때 submit 이벤트가 발생하게 되는데, 우리는 서버에 데이터를 넘기기 전에 해당 데이터에 대한 검증을 끝낼 필요가 있습니다. 그러기 위해서 form 태그의 onSubmithandleSubmit 이라는 함수를 넣어주고 매개변수로 우리가 정의한 onSubmit 함수를 넣어줍니다. onSubmit 함수를 정의 할 때 매개변수로 data 라는 값을 받을 수 있는데, 해당 값은 사용자가 제출 버튼을 클릭 한 후 내려오는 사용자 입장에서 최종으로 제출하는 데이터 입니다.

cosnt { handleSubmit } = useForm();

const onSubmit = (data: IForm) => {
  // data 가 최종 데이터
};

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Row>
        <Label>name: </Label>
        <ControlInputText<IForm>
          control={control}
          name="name"
          rules={{
            required: "반드시 입력해주세요",
            maxLength: { value: 10, message: "최대 10글자 입력이 가능합니다." },
          }}
        />
        {errors.name ? <p className="error">{errors.name?.message}</p> : null}
      </Row>
      <button>Submit</button>
    </form>
  );
}

onSubmit 에서는 기본 이벤트를 막아주는 event.preventDefault() 를 할 필요가 없습니다.

onSubmit 에서 최종 데이터 검증을 하다가 어떤 에러가 체크 되었다면 setError 함수를 활용할 수 있습니다. 그리고 setFocus 를 활용해 해당 필드에 포커스도 줄 수 있습니다.

setError("name", {type: "minLength", message: "1글자 이상 입력해주세요"});
setFocus("name");

이외에 더 유용한 기능들이 많이 있습니다. 추가적인 기능들은 공식 문서를 참고해주시면 좋겠습니다.

useForm – register 바로가기


전체적인 코드

react-hook-form 을 활용해 똑같은 폼을 구현해봤습니다. 라이브러리에 의존성을 부여했기 때문에 직접 다뤄야 하는 로직은 줄고 개발 경험을 높일수 있었습니다. 또한 비제어 컴포넌트를 통해 폼을 다루기 때문에 기존에 input 태그를 다루기 위해 선언했던 state 가 없어지고 컴포넌트가 관리해야 하는 state 수도 적어졌습니다. 또한 이로 인해서 컴포넌트의 랜더링 횟수도 최소화 할 수 있게 되었습니다.

import ControlInputText from "component/ControlInputText";
import { useForm } from "react-hook-form";
import { Row, Label } from "style";

interface IForm {
  occupation: string;
  id: string;
  name: string;
  pwd: string;
  email: string;
  phone: string;
}

function ReactHookForm() {
  const {
    register,
    control,
    handleSubmit: onSubmit,
    watch,
    formState: { errors },
  } = useForm<IForm>({
    mode: "onSubmit",
    defaultValues: {
      occupation: "student",
      id: "",
      name: "",
      pwd: "",
      email: "",
      phone: "",
    },
  });

  const handleSubmit = (data: IForm) => {
    // console.log(data);
  };

  return (
    <form onSubmit={onSubmit(handleSubmit)}>
      <Row>
        <Label>Occupation: </Label>&nbsp;
        <label>student</label>
        <input type="radio" value="student" {...register("occupation")} />
        <label>professor</label>
        <input type="radio" value="professor" {...register("occupation")} />
      </Row>
      <Row>
        <Label>name: </Label>
        <ControlInputText<IForm>
          control={control}
          name="name"
          rules={{
            required: "반드시 입력해주세요",
            maxLength: { value: 10, message: "최대 10글자 입력이 가능합니다." },
          }}
        />
        {errors.name ? <p className="error">{errors.name?.message}</p> : null}
      </Row>
      <Row>
        <Label>id: </Label>
        <ControlInputText<IForm>
          control={control}
          name="id"
          rules={{
            required: "반드시 입력해주세요",
            max: { value: 10, message: "최대 10글자 입력이 가능합니다." },
            min: { value: 3, message: "3글자 이상 입력해주세요." },
          }}
        />
      </Row>
      <Row>
        <Label>pwd: </Label>
        <ControlInputText<IForm> control={control} name="pwd" />
      </Row>
      <Row>
        <Label>email: </Label>
        <input
          type="text"
          {...register("email", {
            pattern: {
              value:
                /^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
              message: "이메일 형식에 맞지 않습니다.",
            },
          })}
        />
        {errors?.email ? (
          <p className="error">{errors.email?.message}</p>
        ) : null}
      </Row>
      {watch("occupation") === "professor" ? (
        <Row>
          <Label>phone: </Label>
          <ControlInputText<IForm> control={control} name="phone" />
        </Row>
      ) : null}
      <button>Submit</button>
    </form>
  );
}

export default ReactHookForm

위 코드는 간단한 폼이기 때문에 드라마틱한 코드의 변화를 확인할 순 없었습니다. 실제로 프로젝트에 react-hook-form 을 접목 했을 때 좋았던 점을 나열해보자면..

  • 우리끼리만 아는 스펙의 최소화
    >> 기존에 제어 컴포넌트를 다루기 위해 form 을 직접 구현해서 활용하고 있었습니다. 이는 우리끼리만 아는 스펙이 생기는 것인데, 만약 문서화가 잘 되어있지 않거나 기존에 만들었던 사람이 퇴사하는 경우엔 다음 개발자가 이를 활용하는데 어려움이 있다는 단점이 있습니다. react-hook-form 이라는 라이브러리를 활용한다면 다른 개발자가 우리의 프로젝트의 유지보수를 하더라도 큰 어려움 없이 할 수 있다는 장점이 있습니다.
  • state 를 최소화 함으로써 state 로 부터 야기되는 버그 최소화
    >> 비제어 컴포넌트의 컨셉이기에 state 가 기존의 폼에 비해서 적어집니다. setState 는 기본적으로 비동기로 작동하는데, 이로 인해서 발생하는 버그가 최소화 되고, 랜더링 횟수가 최소화 됨으로 써 퍼포먼스 최적화에 큰 이점을 줍니다.
  • 직관적인 코드
    >> 직관적으로 폼을 다룸으로써 어떤 state 혹은 함수를 사용해 폼을 구현하는것이 아닌, form 자체를 직접 다룸으로써 개발을 더욱 직관적으로 편리하게 할 수 있습니다.

마무리하며

react-hook-form 에는 useForm 을 제외하고도 여러가지 훅들이 있습니다.

이번 포스팅에서는 기본적인 useForm 을 탐구해봤으며, 이번 시리즈의 다음 포스팅에는 useControllerController API 를 활용해 MUI 의 FormControl 에 접목해서 react-hook-form 을 활용하는 방법을 알아보도록 하겠습니다.

읽어봐주셔서 감사합니다.

인트로 이미지 출처: react-hook-form 공식 홈페이지

시리즈 글 살펴보기

  1. react-hook-form 과 MUI 함께 사용하기
  2. react-hook-form 다양하게 사용하기
강동희
Developer

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

Leave a Reply

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

4 replies on “react-hook-form 을 활용해 효과적으로 폼 관리하기”

  • 2023년 08월 05일 at 4:23 am

    글 너무 좋아요 정말 잘 읽었습니다.
    도움이 많이 됐어요.
    감사합니다 !!!

    • 강동희
      2023년 08월 25일 at 3:24 pm

      도움이 되었다니 정말 다행입니다!
      앞으로도 좋은 인사이트 드릴 수 있는 글 작성해보도록 노력하겠습니다 🙂

  • testp
    2023년 08월 25일 at 10:30 am

    watch 보다는 useWatch 가 조금 더 퍼포먼스 적으로 괜찮다고 해서

    getValues > useWatch > watch 순으로 고려하면 좋다고 해요!!

    https://scrapbox.io/mrsekut-p/getValues%E3%81%A8watch%E3%81%A8useWatach%E3%81%AE%E4%BD%BF%E3%81%84%E5%88%86%E3%81%91

    • 강동희
      2023년 08월 25일 at 3:24 pm

      의견 주셔서 감사합니다! useWatch 에 대한 레퍼런스도 찾아봐야겠네요!