react-hook-form Peer 대체하기

JoonPark·2023년 12월 18일
0
post-thumbnail

서론

이전부터 입력폼이 많으면 react-hook-form을 사용하는 것이 개발 경험과 성능에서 효율적이라 판단하여 꾸준히 사용해왔으나, 말 그대로 useForm의 폼 컨트롤과 유효성 검사, 에러처리만 이용했다.
그러나 이번에 input과 Label을 연결한 컴포넌트로 UX를 개선하며 생각대로 react-hook-form 라이브러리를 잘 다루지 못하게 되자, 한번 깊게 다룰 줄 알아야겠다는 다짐을 했다.
정말 그때는 공식 문서도 계속 보면서 다시 코딩하고, 안되서 팀장님께 부탁드려서 같이 의논도 하고, 그래도 안되서 StackOverflow에 질문까지 올렸었다.... 답변은 안달렸지만.

본론

컴포넌트 기능 요구 사항

  • 부모 컴포넌트에서 react-hook-form을 사용하여 폼 컨트롤과 유효성 검사, 에러처리를 용이하게 사용토록 한다.
  • 해당 컴포넌트는 기본적으로 Placeholder로서 기능하며, 다음 조건이 충족될 경우에는 input태그 위에서 legend와 같이 기능한다.

기존 컴포넌트 코드

  • 기존 컴포넌트는 TailwindCSS 에서 지원하는 Peer라는 클래스 기능으로 작성되었다.
  • 인풋 컴포넌트에 peer 클래스를 넣고 스타일을 변경하고 싶은 형제 컴포넌트에 'peer-조건:스타일' 형식으로 작성한다.
  • 인풋 컴포넌트가 focus 되거나, valid한 값이 들어올 경우에 peer 조건대로 스타일이 달라진다.
// InputLabel.tsx 컴포넌트 코드

const InputLabel = forwardRef<HTMLInputElement, InputLabelProps>(
  ({ className, type, label, ...props }, ref) => {
    return (
      <div className="relative pt-2">
        <input
          type={type}
          className={cn(
            "peer",
            className,
          )}
          ref={ref}
          {...props}
        />
        {label && (
          <label
            htmlFor={props.id}
            className={cn("peer-focus:scale-75 peer-valid:scale-75")}>
            {label}
          </label>
        )}
      </div>
    );
  },
);

export default InputLabel;
// Login.tsx 컴포넌트 사용시
<InputLabel
    id="password"
    type="password"
    label="Password"
    required
    value={password}
    onChange={(e: any) => setPassword(e.target.value)}
>

기존 컴포넌트의 문제점

  1. Peer 클래스 특성상 "어떤 값이든 들어오면 Peer가 동작한다" 라는 조건은 존재하지 않는다. 그래서 이메일 값이 valid임에도 불구하고, type을 email로 넣으면 일반적인 텍스트가 들어갔을때 label 텍스트가 input 텍스트를 가려버리는 불상사가 발생한다.
  2. Peer 클래스를 사용하면 react-hook-form과 호환이 어렵다. 위와 같은 이유로 아무 값(valid type:text)이 들어오면 Peer가 동작하기 때문에 react-hook-form의 {...register} 또한 그 "아무 값" 에 포함된다. 따라서 실제 입력값이 없더라도 Peer가 동작하여 해당 컴포넌트는 placeholder 기능을 잃어버린다.

그래서 등장한 watch 함수

  • 과감하게 Peer 클래스 사용을 취소하고, watch함수를 사용해서 해당 input을 입력값을 listen하게 만든다.
  • 어떤 값이든 들어오거나, focus가 입력될 경우 Peer 클래스로 적용하려 했던 클래스를 동적으로 적용시켜준다.

watch를 적용하기 위해 고쳐야할 부분이 여전히 많다....

그냥 해당 컴포넌트에 쉽게 watch만 넣어서 동적으로 적용할 수 있을거라 생각한건 오산이었다.
1. watch로 input값을 체크하려면 부모 컴포넌트에서 상태가 저장되어 prop으로 직접 내려주어야 한다. 그래서 watch의 명시적인 prop 전달이 강제된다.
2. 마찬가지로 register함수 또한 부모 컴포넌트에서 상태가 저장되어 prop으로 직접 내려주어야 한다. 기존의 {...register} 형식 대신 register={register}의 형태로 내려야 watch의 조건을 체크할 수 있음을 확인했다.

고치면서 발견한 시행착오

  1. 위 내용 중 두번째 내용, props 중 watch가 참고하는 중요한 요소인 'id' 값을 'name' 이라는 이름으로 prop 내렸기 때문에 오해한 부분이 있다. 그러니까, {...register} 형식으로 컴포넌트 상태가 전달되면, 자동으로 'name'이라는 이름의 상태전달은 중복적용으로서 강제 삭제 시켜버린다(!!)
  2. 그래서 'name' 대신 'id'라는 이름으로 이를 대체할 경우 {...register} 형식으로 기존처럼 상태값을 잘 내려줄 수 있게 되었다.

변경된 컴포넌트 코드

  • watch를 명시적 prop방식으로 내려주었다.
  • Peer-focus를 대신하기 위해서 useState를 사용해서 isFocused가 참인지 확인한다.
  • Peer-valid를 대신하기 위해서 fieldValue?.length > 0 코드로 값이 입력되었는지 아닌지 확인한다.
  • 둘 중 하나를 만족하면 즉시 Peer로 구현하려 했던 스타일을 적용한다.
// InputLabel.tsx 컴포넌트 코드

const InputLabel = forwardRef<HTMLInputElement, InputRHF>(
  ({ className, type, label, id, watch, ...props }, ref) => {
    const fieldValue = watch(id);

    const [isFocused, setIsFocused] = useState(false);
    const isPeer = fieldValue?.length > 0 || isFocused;

    return (
      <div className="relative pt-2">
        <input
          type={type}
          id={id}
          ref={ref}
          {...register}
          className={cn(
            "...",
            className,
          )}
          {...props}
          onFocus={() => setIsFocused(true)}
          onBlur={() => setIsFocused(false)}
        />
        {label && (
          <label
            htmlFor={id}
            className={cn("peer-focus:scale-75 peer-valid:scale-75")}>
            {label}
          </label>
        )}
      </div>
    );
  },
);

export default InputLabel;
// Login.tsx 컴포넌트 사용시
<InputLabel
    id="password"
    type="password"
    label="Password"
    value={password}
    watch={watch}
    {...register('password', {
        required: '비밀번호를 입력해주세요!',
        minLength: {
            value: 4,
            message: '최소 6글자를 입력해주세요',
        },
    })}
>
profile
FE Developer

0개의 댓글