InputWithValidation
코반주반

InputWithValidation

간단한 validation 기능을 가진 input 컴포넌트를 만들어보자.

InputWithValidation.tsx

import React, { ComponentPropsWithRef, useEffect, useRef } from 'react';

/** 유효성 검증에 사용할 메시지 타입을 aliasing */
type InvalidMessage = string;
/**
 * InputValidator는 value를 검증하고, 검증에 실패하면 메시지를 반환한다.
 * void는 return undefined, return, 아무것도 return 하지 않는 모든 경우에 대응한다.
 */
export type InputValidator = (value: string) => InvalidMessage | void;

/**
 * ComponentPropsWithRef<'input'>
 *  컴포넌트의 Props는 React에서 사용하는 input element 인터페이스를 계승한다.
 *
 * value?: string;
 *  value 타입을 string | undefined로 좁힌다.
 *
 * reportValidityEvent?: keyof HTMLElementEventMap;
 *  reportValidity를 사용해서 유효성 검증을 하기 때문에, 
 *  어떤 이벤트에서 검증할지 이벤트 이름을 받는다. ex) 'input' or 'blur'
 *
 * validators: InputValidator[];
 *  복수개의 valdator를 받을 수 있게 인터페이스를 설계한다.
 */
type InputWithValidationProps = ComponentPropsWithRef<'input'> & {
  value?: string;
  reportValidityEvent?: keyof HTMLElementEventMap;
  validators: InputValidator[];
};

const InputWithValidation = ({
  value = '',
  validators,
  reportValidityEvent,
  ...restProps
}: InputWithValidationProps) => {
  const ref = useRef<HTMLInputElement>(null);
  const invalidMassages: string[] = findInvalidMessages(validators, value);
  /**
   * setCustomValidity 메소드를 통해 reportValidity 혹은 
   *  formSumit 이벤트 발생시 보여줄 커스텀 유효성 검증 메시지를 등록할 수 있다.
   *  빈 문자열을 넣어 지울 수 있다.
   */
  ref.current?.setCustomValidity(getVisibleInvalidMessage(invalidMassages) ?? '');

  useEffect(() => {
    if (!reportValidityEvent) {
      return;
    }

    function reportValidity(event: Event): void {
      if (event.currentTarget instanceof HTMLInputElement) {
        event.currentTarget.reportValidity();
      }
    }

    /** reportValidityEvent에 대해 reportValidity 콜백을 등록한다. */
    ref.current?.addEventListener(reportValidityEvent, reportValidity);

    return () => {
      ref.current?.removeEventListener(reportValidityEvent, reportValidity);
    };
  }, [reportValidityEvent]);

  return <input ref={ref} value={value} {...restProps} />;
};

export default InputWithValidation;

/**
 * validator에서 반환되는 invalidMessage를 reduce로 모아서 반환한다.
 */
function findInvalidMessages(validators: InputValidator[], value: string): string[] {
  return validators.reduce<string[]>((invalidMessages, validators) => {
    const invalidMessage = validators(value);
    if (invalidMessage) {
      invalidMessages.push(invalidMessage);
    }
    return invalidMessages;
  }, []);
}

/**
 * 복수의 메시지를 개행처리.
 *  메시지가 없다면 null을 반환한다.
 */
function getVisibleInvalidMessage(invalidMassages: string[]): string | null {
  return invalidMassages.length > 0 ? invalidMassages.join('\n') : null;
}

 

validator 등록은 다음과 같이 할 수 있다.

validation.ts

const INPUT_VALIDATOR: Record<string, InputValidator> = {
  MIN_LENGTH_8: (value) => {
    if (value.length < 8) {
      return '- 최소 8자 이상 입력해주세요.';
    }
  },
  INCLUDE_NUMBERS: (value) => {
    if (/\d/.test(value)) {
      return;
    }
    return '- 숫자를 최소 1개 이상 포함해야 합니다.';
  },
};
const PASSWORD_VALIDATE_CONDITIONS: InputValidator[] = [
  INPUT_VALIDATOR.MIN_LENGTH_8,
  INPUT_VALIDATOR.INCLUDE_NUMBERS,
];

Form.tsx

return (
  <label>
    <span>비밀번호</span>
    <InputWithValidation
      type="password"
      required
      value={password}
      onChange={handleChangePassword}
      validators={PASSWORD_VALIDATE_CONDITIONS}
      reportValidityEvent={'input'}
    />
  </label>
);

 

실제 동작은 다음과 같다.

단일 검증 실패

 

복수의 검증 실패