통화 입력 필드 UX 개선하기

Einere·2022년 12월 25일
0

일단 구현해보자

회사에서 금액을 입력하는 필드를 구현해야 할 일이 생겼다.
해당 프로젝트에서는 MUI를 사용하고 있었기에, 빠르게 UI를 구현했다.

우선, MUI 공식 홈페이지에 나와 있는 예제 코드대로 작성을 했다.

// NumberFormatInput

interface NumberFormatInputProps {
  name: string;
  onChange: (value: { target: { value: NumberFormatValues["value"] } }) => void;
}
const NumberFormatInput = React.forwardRef<unknown, NumberFormatInputProps>(
  function NumberFormatCustom(props, ref) {
    const { onChange, ...other } = props;

    return (
      <NumericFormat
        {...other}
        getInputRef={ref}
        onValueChange={(values) => {
          onChange({
            target: {
              value: values.value,
            },
          });
        }}
        thousandSeparator=","
        decimalScale={0}
        allowNegative={false}
      />
    );
  }
);

export default NumberFormatInput;

그리고 <NumberFormatInput/> 을 적용한 MUI 컴포넌트를 만들었다.

const { register } = useForm<...>({
  mode: "onChange", // 유효성 검증이 onChange 이벤트마다 동작하도록 설정
});

const currencyInputProps = {
  inputComponent: NumberFormatInput as never,
  startAdornment: <MUI.InputAdornment position="start"></MUI.InputAdornment>,
};

<MUI.TextField
  id="text-input-fee-of-narration"
  variant="standard"
  InputProps={{
    ...currencyInputProps,
    ...register(...), // react-hook-form 을 적용
  }}
  error={...}
  helperText={...}
/>

MUI에서 제공하는 <TextField/> 컴포넌트의 InputProps 속성을 이용해 <NumberFormatInput/>register 반환 객체를 같이 넣어주는 방식으로 구현했다.

그런데 문제가..

하지만 이 방법에는 약간 문제가 있는데, 바로 컴포넌트의 유효성 검증이 onChange 이벤트에 이루어지지 않는다는 점이다.

onChange 시 유효성 검증이 작동하지 않는 모습

[onChange 시 유효성 검증이 작동하지 않는 모습]

위 움짤을 보면, 입력 값이 비어있을 때 경고 메세지가 보여지지 않는다. 치명적이진 않지만, 유저 경험에는 큰 영향을 미칠 수 있는 요인이다. 왜 이런 현상이 발생하는 것일까?

나름대로 궁리해 본 결과

현재 동작 방식으로는 제일 바깥쪽에 <MUI.TextField/> 가 있고, 그 안에 <NumberFormatInput/> , 그 안에 <NumericFormat/> 이 있다.
아마 <NumericFormat/> 내부에는 <input/> 이 있을 것이다. (실제로 개발자 도구로 살펴보면, <input/> 요소가 마크업 되어 있다.)

내가 작성한 코드측에서 확인할 수 있는 제일 깊은 부분인 <NumberFormatInput/> 컴포넌트의 <NumericFormat/>onValueChange 에서 로그를 찍어봤다.

onValueChange={(values) => {
  console.debug("[NumberFormatInput] onValueChange", onChange);
  onChange({
    target: {
      value: values.value,
    },
  });
}}

콘솔창에 찍힌 onChange

[콘솔창에 찍힌 onChange]

onChange 함수는 MUI.InputBasehandleChange 함수다.

  const handleChange = (event, ...args) => {
    if (!isControlled) {
      const element = event.target || inputRef.current;

      if (element == null) {
        throw new Error(process.env.NODE_ENV !== "production" ? `MUI: Expected valid input target. Did you use a custom \`inputComponent\` and forget to forward refs? See https://mui.com/r/input-component-ref-interface for more info.` : _formatMuiErrorMessage(1));
      }

      checkDirty({
        value: element.value
      });
    }

    if (inputPropsProp.onChange) {
      inputPropsProp.onChange(event, ...args);
    } // Perform in the willUpdate


    if (onChange) {
      onChange(event, ...args);
    }
  };

이 결과를 통해, 주입받은 onChange 함수는 <MUI.InputBase/>handleChange 라는 것을 알 수 있다.
handleChange 는 온전한 event 객체를 받아야 하지만, onValueChange 에서는 { target: { value: NumberFormatValues["value"] } } 라는 단순한 객체 리터럴을 넘겨주기만 한다.

그렇다면 register 의 반환값인 onChange 는 어디로 갔을까? 내가 직접 로그를 찍어 확인할 수가 없어 확실하진 않지만 다음과 같이 동작할 것으로 짐작한다.

실행 흐름도

[실행 흐름도(뇌피셜)]

  1. 유저가 값을 변경할 때, <input/>onchange 이벤트 발생
  2. NumericFormat 로부터 주입받은 onValueChange 호출
  3. onValueChange 내부에서 BaseInput 로부터 주입받은 onChange 호출
  4. TextFieldInputProps 로 주입받은 register 반환값 인 onChange 호출

3번 과정에서 이미 불완전한 event 객체 리터럴을 받았으니, 당연히 4번에서 onChange 함수로 전달 될 객체도 불완전 할 것이다. 이 것이 유효성 검증이 제대로 동작하지 않게 하는 원인이 아닐까.. 생각중이다.

그래서 해결은..?

뭔가 찝찝하긴 하지만(이 이슈를 대체 어느 레포에 제기해야 할지.. 😵‍💫), 어찌 됬건 해결은 가능하다.

기존의 <MUI.TextFiled/> 컴포넌트를 <NumericInput/> 이라는 새 컴포넌트로 교체했다.

interface NumericInputProp<T>
  extends Pick<
    ControllerProps<T>,
    "control" | "name" | "rules" | "defaultValue"
  > {
  id: string;
  error?: FieldError;
  unit?: string;
}
export default function NumericInput<T>(props: NumericInputProp<T>) {
  const { id, control, name, rules, error, unit = "₩", defaultValue } = props;

  return (
    <Controller
      name={name}
      control={control}
      rules={rules}
      render={({ field: { onChange } }) => (
        <NumericFormat
          id={id}
          customInput={MUI.TextField}
          variant="standard"
          error={isNotEmptyObj(error)}
          helperText={error?.message}
          InputProps={{
            startAdornment: (
              <MUI.InputAdornment position="start">{unit}</MUI.InputAdornment>
            ),
          }}
          thousandSeparator={true}
          defaultValue={defaultValue as string | number | undefined}
          onValueChange={(v) => {
            onChange(v.value);
          }}
        />
      )}
    />
  );
}

register 함수 대신, <Controller/>control 을 이용해 폼 상태를 관리하게 하고, <NumericFormat/> 의 속성으로 MUI.TextFiled 를 넘겨주는 방식으로 변경했다.

위 컴포넌트를 적용하면 다음과 같이 사용자의 입력 값의 변화에 즉각적으로 경고 메세지를 보여줄 수 있다.

사용자의 액션에 즉각적으로 반응하는 UI

[사용자의 액션에 즉각적으로 반응하는 UI]

profile
지속가능한 웹 개발자를 지향합니다. 경험의 공유를 통해 타인에게 도움이 되는 것을 좋아합니다. 사용자에게 가치를 제공하는 것에 기쁨을 느낍니다.

0개의 댓글