InputWithValidation
2023. 1. 13. 00:10ㆍ코반주반
간단한 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>
);
실제 동작은 다음과 같다.