들어가며..

iSyncBrain 3.0 프로젝트에서는 react-hook-form이라는 라이브러리를 활용하고 있습니다. 해당 라이브러리를 활용하면 유효성 검사를 편리하게 구현할 수 있습니다. 저는 주로 form 데이터를 submit 할 때 유효성을 검증하는 로직을 사용해왔습니다. 라이브러리를 처음 다루는 데다가 실시간으로 사용자가 입력한 데이터를 검증하는 요구사항을 구현하는 데 어려움이 있었습니다. 제가 구현해야 하는 요구사항은 다음과 같았습니다.

하나의 POST 통신을 요청하는 multipart/form에 네 가지의 텍스트 박스가 있고, 네 가지 박스에 모두 값이 입력된 상태가 아니면 submit을 실행하는 버튼을 비활성화합니다.

처음 이 요구사항을 확인했을 때는, 텍스트 박스에 입력되는 값들을 onChange를 통해 state로 받아와야겠다고 생각했습니다. 해당 기능에는 material UI와 같은 ui 라이브러리에서 데이터를 입력하는 것이 포함되어 있어 input 구성 요소가 다르기 때문에 hook Form의 Controller를 활용해야만 합니다. 라이브러리에 대한 이해도가 낮아 Controller를 통해 input 태그를 관리하면 onChange 속성을 사용할 수 없다고 생각했습니다.

라이브러리 없이 구현한 방법

먼저, onChange를 통해 네 가지 input에 입력되는 값을 useState를 활용하였습니다. 아래 예시와 같이 네 가지 텍스트 박스 값을 모두 같은 방식으로 관리했습니다.

<input
 onChange={(e) => {
  setTitleKr(e.target.value);
 }}
/>

그러고 나서, 유효성 검사를 통과했는지를 따지는 boolean 타입의 submitDisabled를 선언해주었습니다. useEffect를 통해 네 가지 state를 실시간으로 검증하여 하나라도 빈 값일 때는 false, 모든 텍스트 값이 존재할 때는 true로 변경되도록 했습니다.

const [submitDisabled, setSubmitDisabled] = React.useState < boolean > true;

useEffect(() => {
 setSubmitDisabled(titleKr === "" || contentKr === "" || titleEn === "" || contentEn === "");
}, [titleKr, contentKr, titleEn, contentEn]);

마지막으로, submit을 실행할 버튼에는 disabled 속성을 넣어 버튼이 조건에 따라 활성화/비활성화 되도록 했습니다.

<Button onClick={onSubmit} disabled={submitDisabled} type="submit">
 {"확인"}
</Button>

의도한 대로 다음과 같이 성공적으로 동작했습니다. input_form_test

아쉬운 점

요구사항대로 구현하였지만, 해당 기능은 POST 통신을 보낼 때 네 가지 텍스트뿐만 아니라 Date나 File과 같은 데이터들을 포함하고 있어서 유효성 검증을 위해 핸들러가 많아지는 것이 가독성을 해치고, 특히나 아래 코드와 같이 submit을 실행하는 함수가 복잡해 보였습니다. 또 서두에서 밝혔듯이 iSyncbrain 3.0에서 form data를 다루는 다른 기능들은 react-hook-form을 사용하고 있기 때문에 해당 라이브러리가 모든 기능에 동일하게 적용되기를 원했습니다.

const onSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
 if (Number(beginHour) > 23 || Number(endHour) > 23) {
  return onFailureModalOpen(t("ICorrectTimeRequired"));
 }
 if (Number(beginMin) > 59 || Number(endMin) > 59) {
  return onFailureModalOpen(t("ICorrectTimeRequired"));
 }

 const contentData: EmergencyNotifyRequest = {
  isNotProceed: false,
  begin: null,
  end: null,
  contents: JSON.stringify([
   {
    languageCode: "EN",
    title: titleEn,
    content: contentEn,
    deleteImage: deleteFileEn,
   },
   {
    languageCode: "KO",
    title: titleKr,
    content: contentKr,
    deleteImage: deleteFileKr,
   },
  ]),
 };

 if (krFileSelected) {
  contentData.KO_attachments = krFileSelected;
 }
 if (enFileSelected) {
  contentData.EN_attachments = enFileSelected;
 }
 if (beginHour && beginMin && beginDate) {
  contentData.begin = `${dateToDashString(beginDate)} ${handleDuplicate(beginHour, beginMin)}`;
 }
 if (endHour && endMin && endDate) {
  contentData.end = `${dateToDashString(endDate)} ${handleDuplicate(endHour, endMin)}`;
 }
};


라이브러리를 적용한 방법

라이브러리 사용법을 찾아보니, react-hook-form에 실시간 유효성 검사를 할 수 있는 방법이 있었습니다. 서두에서 밝혔듯이 해당 기능에서 UI 라이브러리를 사용하고 있는 입력 폼이 있기 때문에 hook-form의 Controller를 활용해야 했습니다. 예를 들자면 날짜 데이터를 받아오기 위해서 캘린더를 표시하는 입력 폼을 다음과 같이 사용하고 있습니다.

<Controller
 name="beginDate"
 control={control}
 render={({ field }) => (
  <CalenderSinglePicker
   dateValue={beginDate}
   onChangeValue={(e) => {
    setBeginDate(e ?? new Date());
   }}
  />
 )}
/>

라이브러리를 통해 요구사항을 구현하기 위해 네 가지 boolean 값을 선언해주고 onChange로 해당 값들이 변경 사항이 있을 때마다 입력 여부를 체크해주도록 하였습니다.

  const [titleKrCheck, setTitleKrCheck] = React.useState<boolean>(false)
  const [contentKrCheck, setContentKrCheck] = React.useState<boolean>(false)
  const [titleEnCheck, setTitleEnCheck] = React.useState<boolean>(false)
  const [contentEnCheck, setContentEnCheck] = React.useState<boolean>(false)

<Controller
 name="titleKr"
 control={control}
 rules={ { required: true } }
 render={({ field: { onChange } }) => (
  <input
   onChange={(e) => {
     setTitleKrCheck(!(e.target.value === ""));
   }}
   type="text"
  />
 )}
/>

다음으로 위에서 구현했던 방식과 비슷하게 useEffect를 활용하여 모든 값이 입력된 상태여야 submitDisabled 값이 true가 되도록 처리해주었습니다. 또한 첫 번째 방식과 동일하게 버튼에는 disabled 속성을 추가해주었습니다.

useEffect(() => {
 setSubmitDisabled(!(titleKrCheck && contentKrCheck && titleEnCheck && contentEnCheck));
}, [titleKrCheck, contentKrCheck, contentEnCheck, titleEnCheck]);
<Button onClick={onSubmit} disabled={submitDisabled} type="submit">
 {"확인"}
</Button>

마치며..

동일한 요구사항을 다른 방식으로 구현해 보았습니다. 한 프로젝트에서 라이브러리를 사용할 때는 모든 기능에 동일하게 적용될 수 있는지가 중요합니다. 유지보수하는 데 유리해지고 코드에 일관성이 생기기 때문입니다. 특히나 react-hook-form은 Controller를 활용하면 UI 라이브러리로 구현된 input에도 적용할 수 있고, form data의 유효성 검사에도 편리하고, 텍스트를 검증하여 에러 메시지를 간편하게 띄우는 등의 기능들을 제공하고 있으니 프로젝트에 도입할지 여부를 고민하고 있다면 추천해 드립니다.