React input 숫자 - React input susja

input에 숫자만 입력되게 하고싶을 경우 사용가능한 방법

Show
    const onChange(e: React.ChangeEvent<HTMLInputElement>) {
      const { value } = e.target
      // value의 값이 숫자가 아닐경우 빈문자열로 replace 해버림.
      const onlyNumber = value.replace(/[^0-9]/g, '')
      setInputs(onlyNumber)
    }

    업무를 하면서 카드결제시 카드번호 작성 인풋이나
    생년월일등 숫자만 들어가야할 input에 위 방법으로 해결한적이
    있어 공유 합니다.

    더 좋은 방법이나 위 방법이 잘못된점이 있다면 지적해주세요!

    Photo by Alexandre Debiève on Unsplash

    form을 만들다보면 여러가지 input을 쓰게된다. 그 중에서 number input은 많은 곳에 사용되지만 불행히도 html5에서 제공되는 type=”number”가 우리의 입맛에 맞는 경우는 잘 없다. 그래서 numberInput을 react로 만들어보기로 했다.
    (마감시간에 맞춰 글쓸 내용이 떠오르지 않아 급조한 것은 안….비밀;;)

    우선 number input하면 필요한 내용을 아래와 같이 정리해 보았다.

    value의 범위(max, min),
    화살표로 value의 up, down기능,
    updown시 가감되는 차이(step),
    focus 시에 텍스트를 모드 선택할건지 여부(selectOnFocus),
    받아들일 소수점 자리수(decimal)
    style을 위한 classs(className)
    placeholder, id, name, readonly, disabled등의 dom attribute

    위를 props로 정리하면 아래와 같다.

    interface NumberInputProps {decimal: number; // 소수점 몇자리까지 받을 것인지 필요하다
    value: number; // 외부에서의 값 변경을 위해 value를 받는다.
    min?: number; // 최소값을 설정할지 여부에 따라 optional이다.
    max?: number; // 최대값을 설정할지 여부에 따라 optional이다.
    selectOnFocus?: boolean; // focus 되었을때 편한 수정을 위해 값을 select해준다.
    step?: number; // 화살표로 값 변경시 가감값이다.
    setValue: Function; // 부모에게 값을 전달해줄 emitter이다.
    className?: string; // style을 위해 필요하다.
    placeholder?: string; // Dom attribute
    readonly?:boolean;
    id?: string // Dom attribute(label 이용시 접근성)
    disabled?: boolean;
    identifier?: any // name으로 해도 되나 실제 name에 이상한 값이 설정되는 것을 방지하기 위해 identifier로 변경
    }

    우리가 render할 DOM은 input밖에 없으므로 render는 간단하게 처리가 된다.

    현재 우리가 설계한 내용에서 필요한 이벤트는 focus(selectOnFocus)와 change(value 변경처리), keydown(up, down value) 그리고 마지막으로 input의 값을 이쁘게 변경해줄 blur 이벤트면될것이다.

    render() {  return (
    <input
    className={this.props.className}
    type="text"
    id={this.props.id}
    placeholder={this.props.placeholder}
    readOnly={readonly}
    disabled={disabled}
    onBlur={this.onBlur}
    onChange={this.onChange}
    onFocus={this.onFocus}
    onKeyDown={this.onKeyDown}
    />
    );
    }

    다음으로 연결할 이벤트를 만든다.

    onChange

    react에서 제공하는 onChange는 실제 dom의 onchange(실제 blur시에만 발생한다)와는 다르다. 이는 사실 물리적인 입력을 인지하는input event
    (https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event)에 더 가깝다.
    또한 인자로 넘어온 이벤트 객체 역시 실제 DOM api event가 아니라 react에서 제공되는 synthetic event이다.
    synthetic event에 대해서는 공식문서에 잘 나와있은니 이를 참조하자. https://reactjs.org/docs/events.html

    우선 input의 값 변경 처리에 대한 결정이 필요하다.

    1. this.state 에 값을 저장하고 그것을 input의 value에 연결한다.(controlled-component https://reactjs.org/docs/forms.html#controlled-components)
    2. input의 dom에 바로 access한다.

    react의 경우 모든 dom의 변경은 state를 통해 이루어지는 것이 좋으나 실제 input을 만들어 보면 그것이 좋은 방법인지는 잘 모르겠다.
    그 이유는 차후에 설명하는 것으로 하고 우선은 공식문서에 언급된 controlled component(1)로 제작을 해보려 한다.
    부모 component에서 number로 받는 것이 편리하므로 state를 실제 input에 적힌 숫자와 이를 보내고 변경을 감지하기 위한 number 두가지를 만든다.

    interface State {  strValue: string;
    numValue: number;
    }

    다음으로 실제 onChange에서 처리할 로직이다.

    잘못 입력된 글자(숫자를 제외한 값)들을 제거한다. 실제 number input의 경우 .(소수점)와 e(5-e7을 같은 지수값을 위해)를 입력가능하지만 일반적인 앱에서 저 값을 입력할 일은 없으니 삭제 범위는 [^\d\-.](숫자와 minus 기호, 소수점을 제외하고 소수점이 따라오지 않는 시작점의 0도 삭제한다)로 정한다.

    // digit 별로 필요없는 글자를 지운다.
    cleanRegExp = this.props.decimal ?
    /[^.\d]|^0+(?!(\.|\b))/g : /[^.\d]|^0+(?!\b)/g;
    // 숫자 형식에 맞춘다.
    fixRegExp = this.props.decimal?
    new RegExp(`^[+\\-]?\\d*(\\.\\d{0,${decimal}})?`):
    new RegExp("^[+\\-]?\\d+");
    onChange(e: SyntheticEvent<HTMLInputElement>) { const target = e.currentTarget;
    const selectionStart = target.selectionStart;
    const value = target.value;
    const sign = value[0] === "-" ? "-" : "";
    const trimmed = sign + value.replace(this.cleanRegExp, "");
    // 숫자로 변경이 되지 않지만 허가된 케이스
    if (['', '.', '-'].indexOf(trimmed) > -1) {
    this.applyValues(trimmed, 0);
    return;
    }
    const m = trimmed.match(this.fixRegExp);
    let strValue = m ? m[0] : "0";

    // 해당 숫자가 max와 min을 따르는지 확인하고
    const numValue = this.maskingRange(Number(strValue));

    // 범위를 넘겼을 경우 input의 글자도 바꾼다.
    if (numValue !== Number(strValue)) {
    //바로 .toString으로 변경하지 않은 것은 지수 문제 때문이다.
    strValue = this.safeString(numValue);
    }
    this.applyValues(strValue, numValue);}maskingRange(num: number) { if (isNumber(this.props.max)) {
    num = Math.min(num, this.props.max!);
    }
    if (isNumber(this.props.min)) {
    num = Math.max(num, this.props.min!);
    }
    return num;}applyValues(strValue: string, numValue: number) {
    const newState = {} as Pick<State, keyof State>;
    const shouldChangeStr = this.state.strValue !== strValue;
    const shouldChangeNum = this.state.numValue !== numValue;
    if (shouldChangeStr) {
    newState.strValue = strValue;
    }
    if (shouldChangeNum) {
    newState.numValue = numValue;
    this.props.setValue(numValue, {
    identifier: this.props.identifier
    });
    }
    if (shouldChangeStr || shouldChangeNum) {
    this.setState(newState);
    }
    }

    input에 입력된 ‘0.00’ 과 ‘0’이 숫자로는 같은 0을 나타내기 때문에 numValue와 strValue 두개를 따로 저장한다. emit되는 값이 글자라면 위처럼 numValue를 굳이 저장할 필요는 없다.

    onFocus

    onFocus의 역할은 두 가지이다.
    하나는 글자를 편하게 지울 수 있도록 selection을 처리해주는 것과 실제 ui에서 필요한 경우를 위한 props 의 onFocus를 처리해주는 것이다. onFocus는 form의 touched를 확인할시 필요하기도 하다.

    onFocus(e: SyntheticEvent<HTMLInputElement>) { 
    this.touched = true;
    const value = e.currentTarget.value; if (this.props.selectOnFocus) {
    // selectOnFocus 처리
    e.currentTarget.setSelectionRange(0, value.length);
    }
    if (this.props.onFocus) {
    // event를 넘겨주지만 이를 믿을 수 없기 때문에 options로 필요한 값을 넘긴다.
    this.props.onFocus(e, options);
    }
    }

    onBlur

    onBlur에서는 마지막으로 기존 숫자 기본형 포맷대로 정리를 해주는 역학을 한다. 이전처럼 0을 0.00으로 놔둘시 다른 처리를 할때 귀찮을 수 있으므로 이곳에서 format을 통일시킨다. (아직 options에 대한 정의는 이루어지지 않았다.)

    onBlur(e: SyntheticEvent<HTMLInputElement>) {  const value = e.currentTarget.value;  if (["", ".", "-"].indexOf(value) > -1) {
    const valueToUpdate = this.props.allowEmpty ? "" : "0";
    this.applyValues(valueToUpdate, 0);
    } else {
    this.applyValues(this.safeString(this.state.numValue));
    }
    if (this.props.onBlur) {
    this.props.onBlur(e, options);
    }
    }

    onKeyDown

    마지막으로 onKeyDown이다. onKeyDown 같은 경우 dirty체크를 하기 위해 사용되고 readonly 이거나 disabled 일때 입력을 제어할 수 있어야 한다.

    onKeyDown(e: KeyboardEvent<HTMLInputElement>) {

    if (this.props.disabled || this.props.readonly) {
    return e.preventDefault();
    }

    // 입력이 활성화 되지 않았을때의 dirty는 업데이트 하지 않는다.
    this.dirty = true;
    if (this.props.step) {
    const value = e.currentTarget.value;
    if (e.key.toLowerCase() === 'arrowup') {
    // setValue: 플러스된 값을 세팅한다
    e.preventDefault();
    }
    if (e.key.toLowerCase() === 'arrowdown') {
    // setValue: 마이너스된 값을 세팅한다
    e.preventDefault();
    }
    }
    if (this.props.onKeyDown) {
    this.props.onKeyDown(e, { identifier: this.props.identifier });
    }
    }

    이제 추가 기능으로 masking을 넣을 것이다.

    masking

    masking은 특정 format으로 변경해주는 작업이다. 이를테면 큰 숫자를 입력하는 거래소에서는 자리 수 확인을 위해 ‘,’를 넣는 작업이 필요하다. 이것은 변경이 다 된 strvalue를 setState하기 전에 변경해주면 된다. 또한 외부에서 값을 변경하거나 키보드 업앤다운으로 값이 변경될때 같은 로직을 타야하므로 onChange 내부의 로직을 두개로 분리해서 같이 공유하도록 한다.

    setValueByRule(value: string) {  const sign = value[0] === "-" ? "-" : "";
    ...
    this.applyValues(strValue, numValue);
    }applyValues(strValue: string, numValue: number) {
    const newState = {} as Pick<State, keyof State>;
    const maskedStr = this.maskedStr(strValue);
    const shouldChangeStr = this.state.strValue !== maskedStr;
    const shouldChangeNum = this.state.numValue !== numValue;
    // ... strValue 대신 maskedStr로 처리한다.
    }
    // keydown에 setting에 추가한다.

    이제 우리는 여기 있는 코드를 리팩토링 할것이다.

    그를 위해 다음편 에서는 React의 life cycle과 해당 코드의 분리등을 다시 설명하려 한다. 아직 설명되지 않은 callback option 부분이나 safeString의 이유, 그리고 controlled component의 한계는 역시 함께 설명하려 한다.

    위의 코드로 처리된 number input의 예제는 아래에서 확인할 수 있다.