React.js 양식 및 사용자 입력 작업

강정우·2023년 1월 11일
0

react.js

목록 보기
28/45
post-thumbnail

Form에 대하여

  • 폼은 굉장히 단순해 보일 수 있지만 실은 그렇지 않다.
    하나 이상의 입력 값이 모두 유효하지 않을 수 도 있고 모두 유효할 수도 있고 또 서버로 request를 보낸 뒤에 특정 값이 사용 가능한지 검증을 해야하는 비동기 유효성 검사도 해야하기 때문에 생각보다 복잡하다.

Form에서 유효성 검증을 언제할까?

1. 제출 버튼을 눌렀을 때 => 양식을 다 작성 후 유효성 검증은 feedback이 너무 느리게 된다.
2. 다음 양식으로 넘어갔을 때 => 고치는 중에는 알 수 없게 된다.
3. typing 하나하나 마다 => 초기로드 시 다 invalid라고 뜰 것이고 이는 사용자로 하여금 좋은 경험은 아니다.

  • 그래서 위 단점들을 모두 없앤 form을 만들어보자

입력 핸들링과 리액트의 Form

ref, event

import { useRef, useState } from "react";

const SimpleInput = (props) => {
  const nameInputRef = useRef();
  const [enteredName, setEnteredName] = useState("");

  const nameInputChangeTrigger = event => {
    setEnteredName(event.target.value);
  };

  const formSubmissionTrigger = event => {
    event.preventDefault();
    console.log("state에서 가져온 값 : "+enteredName);
    const enteredValue = nameInputRef.current.value;
    console.log("ref 훅을 통해 가져온 값 : "+enteredValue);
  }

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className='form-control'>
        <label htmlFor='name'>Your Name</label>
        <input 
          ref={nameInputRef} 
          type='text' 
          id='name' 
          onChange={nameInputChangeTrigger}/>
      </div>
      <div className="form-actions">
        <button>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

  • 하지만 역시 ref보다는 state를 이용하는 편이 react scheldule에 있어서 훨신 좋아보인다.

1. 1번 사항 해결

import { useRef, useState } from "react";

const SimpleInput = (props) => {
  const nameInputRef = useRef();
  const [enteredName, setEnteredName] = useState("");
  const [enteredNameIsValid, setEnteredNameIsValid] = useState(false);
  const [enteredNametouched, setEnteredNameTouched] = useState(false);

  const nameInputChangeTrigger = event => {
    setEnteredName(event.target.value);
  };

  const formSubmissionTrigger = event => {
    event.preventDefault();
    console.log("state에서 가져온 값 : "+enteredName);

    setEnteredNameTouched(true);

    if(enteredName.trim()===""){
      setEnteredNameIsValid(false);
      return;
    }

    setEnteredNameIsValid(true);

    const enteredValue = nameInputRef.current.value;
    console.log("ref 훅을 통해 가져온 값 : "+enteredValue);

    // nameInputRef.current.value="";  => 절대 추천하지 않는 방법
    setEnteredName("");
  }
  const enteredNameIsInValid = !enteredNameIsValid && enteredNametouched;
  const nameInputClasses = enteredNameIsInValid ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Your Name</label>
        <input 
          ref={nameInputRef} 
          type='text' 
          id='name' 
          onChange={nameInputChangeTrigger}
          value={enteredName}/>
        {enteredNameIsInValid && <p className="error-text">Name must not be empty sibalryunA</p>}
      </div>
      <div className="form-actions">
        <button>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

2. 2, 3번 사항 해결

import { useRef, useState } from "react";

const SimpleInput = (props) => {
  const nameInputRef = useRef();
  const [enteredName, setEnteredName] = useState("");
  const [enteredNameIsValid, setEnteredNameIsValid] = useState(false);
  const [enteredNametouched, setEnteredNameTouched] = useState(false);

  const nameInputChangeTrigger = event => {
    setEnteredName(event.target.value);
    if(event.target.value.trim()!==""){
      setEnteredNameIsValid(true);
    }
  };

  const nameInputBlurTrigger = event => {
    setEnteredNameTouched(true);
    if(event.target.value.trim()!==""){
      setEnteredNameIsValid(true);
    }
  };

  const formSubmissionTrigger = event => {
    event.preventDefault();
    console.log("state에서 가져온 값 : "+enteredName);

    setEnteredNameTouched(true);

    if(event.target.value.trim()===""){
      setEnteredNameIsValid(false);
    }

    setEnteredNameIsValid(true);

    const enteredValue = nameInputRef.current.value;
    console.log("ref 훅을 통해 가져온 값 : "+enteredValue);

    // nameInputRef.current.value="";  => 절대 추천하지 않는 방법
    setEnteredName("");
  }
  const enteredNameIsInValid = !enteredNameIsValid && enteredNametouched;
  const nameInputClasses = enteredNameIsInValid ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Your Name</label>
        <input 
          ref={nameInputRef} 
          type='text' 
          id='name' 
          onChange={nameInputChangeTrigger}
          onBlur={nameInputBlurTrigger}
          value={enteredName}/>
        {enteredNameIsInValid && <p className="error-text">Name must not be empty sibalryunA</p>}
      </div>
      <div className="form-actions">
        <button>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

여기에서 참고할 점은 state(enteredName)을 사용하면 안되고 event.target.value를 사용해야한다는 점이다.
왜냐하면 enteredName을 여기서 업데이트 하고 있긴 하지만 배웠던 바와 같이 이러한 상태들은 리액트에서 비동기적으로 처리되므로 즉각적으로 반영되지 않아서 다음 줄이 실행될 때에 enteredName을 사용하면 최신의 상태를 반영하지 못하고 이전의 상태를 참고하게 된다 따라서 이 앞의 줄에서 state를 업데이트 하는데에 사용된 데이터와 같은 데이터인 event.target.value를 사용해야 하는 것이다.

3. 코드 정리

import {  useState } from "react";

const SimpleInput = (props) => {
  const [enteredName, setEnteredName] = useState("");
  const [enteredNametouched, setEnteredNameTouched] = useState(false);

  const enteredNameIsvalid = enteredName.trim()!=="";
  const nameInputIsInValid = !enteredNameIsvalid && enteredNametouched;

  const nameInputChangeTrigger = event => {
    setEnteredName(event.target.value);
  };

  const nameInputBlurTrigger = event => {
    setEnteredNameTouched(true);
  };

  const formSubmissionTrigger = event => {
    event.preventDefault();

    setEnteredNameTouched(true);

    if(!enteredNameIsvalid){
      return
    }

    setEnteredName("");
    setEnteredNameTouched(false);
  }
  const nameInputClasses = nameInputIsInValid ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Your Name</label>
        <input 
          type='text' 
          id='name' 
          onChange={nameInputChangeTrigger}
          onBlur={nameInputBlurTrigger}
          value={enteredName}/>
        {nameInputIsInValid && <p className="error-text">Name must not be empty sibalryunA</p>}
      </div>
      <div className="form-actions">
        <button>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;
  1. enteredNameisValid state가 필요없는 이유 : enteredName라는 상태로부터 얻어낼 수 있기 때문, 또한 새로운 값이 입력될 때 마다 이 전체 컴포넌트가 다시 실행되기 때문에enteredNameIsValid 값은 가장 최신의 enteredName과 enteredNameIsTouched 상태를 반영하게 된다. 두 상태 중 하나라도 변경된다면 이 컴포넌트가 리렌더링되기 때문이다.

  2. formSubmissionHandler함수가 컴포넌트가 리렌더링 될 때마다 다시 생성되고 이에 따라서 formSubmissionHandler는 enteredNameIsValid 의 최신 값을 가져오게 된다.

  3. setEnteredNameTouched(false);를 추가한 이유 : 제출 후 touched가 true가 되고 input이 공백이 되면서 InValid가 되는 조건을 충족버리기에 다시 touched를 false로 바꿔줌으로써 brand new한 input form으로 다시 바꿔주는 것이다.

여러 form이 있을 때

import { useState } from "react";

const SimpleInput = (props) => {
  const [enteredName, setEnteredName] = useState("");
  const [enteredNametouched, setEnteredNameTouched] = useState(false);
  const [enteredEmail, setEnteredEmail] = useState("");
  const [enteredEmailtouched, setEnteredEmailTouched] = useState(false);

  const emailRegex = new RegExp("[a-zA-Z0-9]+@[a-z]+\\.[a-z]{2,3}");

  const enteredNameIsvalid = enteredName.trim()!=="";
  const nameInputIsInValid = !enteredNameIsvalid && enteredNametouched;
  const enteredEmailIsvalid = enteredEmail.trim() !== "" && emailRegex.test(enteredEmail);
  const emailInputIsInvalid = !enteredEmailIsvalid && enteredEmailtouched;

  let formIsValid = false;
  if(enteredNameIsvalid && enteredEmailIsvalid){
    formIsValid = true;
  }

  const nameInputChangeTrigger = event => {
    setEnteredName(event.target.value);
  };
  const nameInputBlurTrigger = event => {
    setEnteredNameTouched(true);
  };

  const emailInputChangeTrigger = event => {
    setEnteredEmail(event.target.value);
  };
  const emailInputBlurTrigger = event => {
    setEnteredEmailTouched(true);
  }

  const formSubmissionTrigger = event => {
    event.preventDefault();

    setEnteredNameTouched(true);
    setEnteredEmailTouched(true);

    if(!enteredNameIsvalid || !enteredEmailIsvalid){
      return
    }

    setEnteredName("");
    setEnteredNameTouched(false);
    setEnteredEmail("");
    setEnteredEmailTouched(false);
  }

  const nameInputClasses = nameInputIsInValid ? "form-control invalid" : "form-control";
  const emailInputClasses = emailInputIsInvalid ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Your Name</label>
        <input 
          type='text' 
          id='name' 
          onChange={nameInputChangeTrigger}
          onBlur={nameInputBlurTrigger}
          value={enteredName}/>
        {nameInputIsInValid && <p className="error-text">Name must not be empty sibalryunA</p>}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor='email'>Your E-Mail</label>
        <input 
          type='email' 
          id='email' 
          onChange={emailInputChangeTrigger}
          onBlur={emailInputBlurTrigger}
          value={enteredEmail}/>
        {emailInputIsInvalid && <p className="error-text">sibalryunA Enter valid E-Mail</p>}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;
  • 따로 useEffect로 Valid를 모아서 관리해줘도 되지만 여기서는 굳이 불필요한 state를 더 만들필요는 없기에 그냥 let 변수로 처리하였다.

  • 하지만 6개월된 갓난 아기가봐도 중복되는 코드들이 많아 좋지 못 한 코드인 것을 알 수 있다.
    이를 custom hook으로 처리해보자

custom hook 처리

use-input.js

import { useState } from "react";

const useInput = (validateValueFn) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isTouched, setIsTouched] = useState(false);

  const valueIsvalid = validateValueFn(enteredValue);
  const hasError = !valueIsvalid && isTouched;

  const valueChangeTrigger = (event) => {
    setEnteredValue(event.target.value);
  };
  const inputBlurTrigger = () => {
    setIsTouched(true);
  };
  const reset = () => {
    setEnteredValue("");
    setIsTouched(false);
  };
  return {
    value: enteredValue,
    isValid: valueIsvalid,
    hasError,
    valueChangeTrigger,
    inputBlurTrigger,
    reset,
  };
};

export default useInput;
  • return객체 중 state set메서드 처럼 훅에서 정의된 함수들은 훅이 사용되는 곳에서 호출될 수 있다.
    즉, 훅을 사용하는 컴포넌트에서 호출될 수 있다.

SimpleInput.js

import useInput from "./hooks/use-input";

const SimpleInput = (props) => {
  const {
    value: enteredName,
    isValid: enteredNameIsvalid,
    hasError: nameInputHasError,
    valueChangeTrigger: nameChangedTrigger,
    inputBlurTrigger: nameBlurTrigger,
    reset: resetNameInput,
  } = useInput((value) => value.trim() !== "");

  const {
    value: enteredEmail,
    isValid: enteredEmailIsvalid,
    hasError: emailInputHasError,
    valueChangeTrigger: emailChangedTrigger,
    inputBlurTrigger: emailBlurTrigger,
    rest: resetEmailInput,
  } = useInput((value) => value.includes("@"));
  // const emailRegex = new RegExp("[a-zA-Z0-9]+@[a-z]+\\.[a-z]{2,3}");
  // (emailRegex) => emailRegex.trim() !== "" && emailRegex.test(enteredEmail)

  let formIsValid = false;
  if (enteredNameIsvalid && enteredEmailIsvalid) {
    formIsValid = true;
  }

  const formSubmissionTrigger = (event) => {
    event.preventDefault();

    if (!enteredNameIsvalid || !enteredEmailIsvalid) {
      return;
    }

    resetNameInput();
    resetEmailInput();
  };

  const nameInputClasses = nameInputHasError
    ? "form-control invalid"
    : "form-control";
  const emailInputClasses = emailInputHasError
    ? "form-control invalid"
    : "form-control";

  return (
    <form onSubmit={formSubmissionTrigger}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Your Name</label>
        <input
          type="text"
          id="name"
          onChange={nameChangedTrigger}
          onBlur={nameBlurTrigger}
          value={enteredName}
        />
        {nameInputHasError && (
          <p className="error-text">Name must not be empty sibalryunA</p>
        )}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor="email">Your E-Mail</label>
        <input
          type="email"
          id="email"
          onChange={emailChangedTrigger}
          onBlur={emailBlurTrigger}
          value={enteredEmail}
        />
        {emailInputHasError && (
          <p className="error-text">sibalryunA Enter valid E-Mail</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

참고사항

  • email을 valid할 때 나는 브라우저의 input의 type prop을 이용하여 브라우저에게 valid를 맡겼지만 딱히 valid함수에 변경사항이 없을 경우 컴포넌트 함수 밖에서 상수함수를 선언하여 가져다 써도 하나의 방법이다.

  • useForm hook을 사용해 jsx에 사용될 모든 요소들을 전부 설정해서 반환할 수 있다.

  • Formik을 사용하여 보다 손쉽게 form을 만들 수도 있다.
    다만 컴포넌트들을 많이 사용하고 리액트 예전 버전에서 사용하던 패턴들도 사용한다.
    그렇긴하지만 폼을 렌더링하고 더 복잡한 폼을 만들고 검증할 때는 매우 좋은 라이브러리이다.

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글