회사에서 금액을 입력하는 필드를 구현해야 할 일이 생겼다.
해당 프로젝트에서는 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 시 유효성 검증이 작동하지 않는 모습]
위 움짤을 보면, 입력 값이 비어있을 때 경고 메세지가 보여지지 않는다. 치명적이진 않지만, 유저 경험에는 큰 영향을 미칠 수 있는 요인이다. 왜 이런 현상이 발생하는 것일까?
현재 동작 방식으로는 제일 바깥쪽에 <MUI.TextField/>
가 있고, 그 안에 <NumberFormatInput/>
, 그 안에 <NumericFormat/>
이 있다.
아마 <NumericFormat/>
내부에는 <input/>
이 있을 것이다. (실제로 개발자 도구로 살펴보면, <input/>
요소가 마크업 되어 있다.)
내가 작성한 코드측에서 확인할 수 있는 제일 깊은 부분인 <NumberFormatInput/>
컴포넌트의 <NumericFormat/>
의 onValueChange
에서 로그를 찍어봤다.
onValueChange={(values) => {
console.debug("[NumberFormatInput] onValueChange", onChange);
onChange({
target: {
value: values.value,
},
});
}}
[콘솔창에 찍힌 onChange]
이 onChange
함수는 MUI.InputBase
의 handleChange
함수다.
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
는 어디로 갔을까? 내가 직접 로그를 찍어 확인할 수가 없어 확실하진 않지만 다음과 같이 동작할 것으로 짐작한다.
[실행 흐름도(뇌피셜)]
<input/>
의 onchange
이벤트 발생NumericFormat
로부터 주입받은 onValueChange
호출onValueChange
내부에서 BaseInput
로부터 주입받은 onChange
호출TextField
의 InputProps
로 주입받은 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]