재사용 가능한 Input 컴포넌트 만들기(2) - 기능 로직 관점

정균·2023년 7월 30일
0

우아한테크코스

목록 보기
8/15
post-thumbnail

페이먼츠 미션 개요

이번 페이먼츠 미션을 통해 사용자의 카드 정보를 저장하고 보여주는 애플리케이션을 제작했다. 페이먼츠 애플리케이션에는 카드 저장, 카드 별칭 등 여러 요구 사항이 있었다. 미션을 진행하며 가장 고민을 많이 하고 중점을 둔 요구 사항은 새로운 카드 등록 기능이었다. 이번 포스팅에서는 카드 등록 기능을 구현하며 겪었던 어려움과 이를 해결하는 과정들을 담았다.

페이먼츠 미션 레포지토리
페이먼츠 배포 페이지


기능 로직 관점에서 Input의 재사용 요소는?

각 인풋들은 사용자로부터 입력을 받고, 올바르지 않은 입력은 사용자에게 에러메시지를 보여줘야한다. 여기서 ‘올바르지 않은 입력’을 검증하는 과정이 모든 요소에 공통적으로 존재한다. 이 과정을 어떻게 분리할 수 있을까?

제어 컴포넌트와 비제어 컴포넌트

사용자의 입력을 상태로 가지고 있고 리액트에 의해 값이 제어되는 것은 제어 컴포넌트라고 한다. 반대로 상태로 제어하지 않는 입력 컴포넌트는 비제어 컴포넌트라고 한다.

페이먼츠 미션에서 만들어야 하는 Input은 실시간으로 입력 검증을 필요하므로 제어 컴포넌트로 구현해야 한다. 제어 컴포넌트는 위에서 설명했던 것 처럼 각 입력 값들을 상태로 가지고 있어야 하기 때문에 각 Input 마다 value와 이 value를 변경해주는 onChange 함수가 따로 존재한다. 이 value와 onChange에 대한 커스텀 훅을 분리해보자.

useInput 처음부터 만들어 나가기

각 input 컴포넌트는 제어 컴포넌트로 관리 해야 하므로 value 상태와 이 value 상태를 변경 할 onChange라는 함수가 필요하다. 해당 부분을 다음과 같이 커스텀 훅으로 분리했다.

const useInput = () => {
	const [value, setValue] = useState("");
	
	const onChange = (input: string) => {
		setValue(string);
	}

	return { value, onChange };
}

간단한 useInput 커스텀 훅이 완성됐다. 하지만 이 정도 기능의 커스텀 훅으로는 원하는 요구 사항을 충족하지 못한다. 입력 값에 대한 검증이 각 Input 마다 꼭 필요하다. 이 검증 로직을 추가 해보자.

검증 로직 추가

type Validator = ( value:string ) => boolean

const useInput = (validator: Validator) => {
	const [value, setValue] = useState("");
	
	const onChange = (input: string) => {
		if(!validator(input)) return;

		setValue(string);
	}

	return { value, onChange };
}

onChange 함수에 검증 로직을 추가해줬다. 이로 인해 검증이 통과하면 value가 바뀔 것이고, 검증이 통과하지 않는다면 value가 바뀌지 않을 것이다.

검증 로직이 들어갔으니 충분할까? 아직 부족하다. 단순히 입력만 막으면 사용자는 왜 입력이 안되는지 알 수 없을 것이고, 이는 나쁜 사용자 경험으로 이어질 것이다. 사용자에게 어떻게 입력이 실패했는지 메시지로 피드백을 줘야한다.

에러 메시지 추가

에러 메시지를 위해 인자로 받는 validator 함수의 시그니쳐를 조금 바꿔보자

type ValidateResult = {
  hasError: boolean;
  message?: string;
  isAllowInput?: boolean;
}

type Validator = (value: string) => ValidateResult;

기존 validator에서는 단순 value 검증 여부만 반환했지만, 바뀐 validator는 여러 값을 객체로 묶어 반환하도록 했다. hasError 변수에는 검증 여부를,message 변수에는 검증이 실패했을 때의 에러 메시지를 가진다.

추가적으로 isAllowInput이라는 변수도 넣어줬는데, 입력 값을 value에 적용할지 안할지 여부를 가진다. isAllowInput이 true면 입력한 값이 반영이 되고, false면 입력한 값이 value에 반영이 안되어 아예 입력을 방지한다.

위 Validator 타입을 적용해 에러 메시지 기능을 추가한 useInput 코드는 다음과 같다.

type ValidateResult = {
  hasError: boolean;
  message?: string;
  isAllowInput?: boolean;
}

type Validator = (value: string) => ValidateResult;

const useInput = (validator: Validator) => {
  const [inputValue, setInputValue] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const onChange = (inputValue: string) => {
    const { hasError, message, isAllowInput } = validator(inputValue);

    if (hasError) {
      setErrorMessage(message || "");

      if (isAllowInput) setInputValue(inputValue);
      return;
    }

    setInputValue(inputValue);
    setErrorMessage("");
  };

  return { inputValue, errorMessage, onChange };
};

onBlur 추가

useInput의 onChange 함수를 통해 값을 입력 할 때마다 검증을 하도록 구현했지만 다음과 같은 피드백이 들어왔다.

만료일은 01~12 사이의 숫자가 아니라면 에러를 띄워주기 때문에 모두 입력을 완료 하기 전에는 계속 에러가 보인다. 이 부분이 좋지 않은 경험이라고 피드백을 주셨고, 나 또한 이에 공감했다. 만료일 Input에서 형식 검증 에러를 보여주는 시점이 값을 입력했을 때(onChange 시점)가 아닌, 값을 모두 입력하고 input의 focus가 풀릴 때(onBlur 시점)으로 바뀌어야겠다고 생각했고 useInput에 onBlur 함수를 추가로 적용하기로 결정했다.

const onBlur = (inputValue: string) => {
  const { hasError, message } = onBlurValidator(inputValue);

  if (hasError) {
    setErrorMessage(message || "");
    return;
  }
};

Validator 타입을 사용하는 onBlurValidator라는 인자를 추가로 받고, onChange와 다르게 입력 로직은 추가해주지 않아도 된다. onBlur시 검증 후 에러 메시지 로직만 추가해줬다.

useInput의 최종 형태

const useInput = (onChangeValidator?: InputValidator, onBlurValidator?: InputValidator) => {
  const [inputValue, setInputValue] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const onChange = (inputValue: string) => {
    const { hasError, message, isAllowInput } = onChangeValidator?.(inputValue) ?? { hasError: false };

    if (hasError) {
      setErrorMessage(message || "");

      if (isAllowInput) setInputValue(inputValue);
      return;
    }

    setInputValue(inputValue);
    setErrorMessage("");
  };

  const onBlur = (inputValue: string) => {
    const { hasError, message } = onBlurValidator?.(inputValue) ?? { hasError: false };

    if (hasError) {
      setErrorMessage(message || "");
      return;
    }
  };

  return { inputValue, errorMessage, onChange, onBlur };
};

입력값 상태 inputValue와 입력값 상태를 바꿀 수 있는 onChange 함수가 있고, 사용자가 정의한 입력 검증에 맞지 않을 경우 사용자에게 보여줄errorMessage 상태를 정의해줬다. 또한, onBlur 시점에도 검증을 할 수 있도록 onBlur 함수도 정의해줬다.

profile
💻✏️🌱

0개의 댓글