[React] 폼, 유저 인풋 관리하기 & 커스텀 훅 만들기

summereuna🐥·2023년 5월 29일
0

React JS

목록 보기
59/69

Working with Values, Validation & State


더 나은 사용자 경험을 위해 폼 만들기

1. 폼의 복잡성: What's Complex About Forms?


개발자의 시각에서 form은 그리 단순하지 않다.
폼은 그 입력 때문에 넓고 다양한 상태를 나타낼 수 있기 때문에 굉장히 복잡할 수도 있다.

폼과 입력은 서로 다른 상태를 가정할 수 있다.

하나 이상의 인풋이 유효하지 않은 경우모든 인풋이 유효한 경우
- 폼과 입력 값이 유효하지 않다면,
특정한 입력 값에 대해 🌀에러 메시지를 출력하고
🌟문제가 되는 입력값을 강조해야 한다.

- ❌ 입력값이 제출되거나 저장되지 않도록 해야 한다.
✅ 입력된 값이 확실하게 제출되고 저장되게 해야 한다.

심지어 서버로 리퀘스트(요청)를 보낸 후, 특정 값이 사용 가능한지 확인해야 하는 비동기 유효성 검사를 이용해야 하기 때문에 상태를 알 수 없을 수도 있다.

  • 예) 이메일 주소가 유효한지 확인

또한 폼 안에 있는 모든 입력 값에는 유효성이 있다.
각각의 입력 값의 상태가 모여서 전체 폼의 상태를 결정한다.

언제 유효성 검증을 할지에 따른 차이

에러 메시지를 출력하고 유효하지 않은 입력 값을 강조하는 부분을 다루면 더 복잡해 진다.
사용자 입력에 대한 유효성 검증을 언제 하느냐에 따라 상황이 달라지기 때문이다.

폼이 제출될 때인풋 포커스 잃을 때키 입력시 마다
사용자에게 경고하기 전에 값을 입력할 수 있음사용자에게 경고하기 전에 값을 입력할 수 있음키 칠때마다 유효성 검사되므로, 올바른 값을 입력하기 전에 사용자에게 경고해버림
불필요한 경고는 피할수 있지만, 피드백이 너무 늦음이 방법은 사용자가 아무것도 입력하지 않은 폼에 유용함잘못된 입력에만 적용하면 보다 직접적인 피드백 제공 가능

이렇게 다 장단점이 있다.


2. 리액트에서 인풋과 폼 다루는 방법

2-1. 값 조작 하기: 폼 제출 처리 및 사용자 입력 값 가져오기


사용자 입력을 가져오는 방법에는 두 가지 방법이 있다.

1. 키 입력마다 값을 확인하여 이 값을 어떤 상태(state) 변수에 저장하여 가져오기

import { useState } from "react";

const SimpleInput = (props) => {
  const [userName, setUserName] = useState("");

  const userNameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    
    console.log(userName);
    setUserName("");
  };

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

export default SimpleInput;

키를 입력할 때마다 즉각적인 유효성을 검증이 필요한 경우에는 상태를 사용하는 것이 좋다.

2. ref를 이용하여 사용자가 값을 모두 입력한 후 입력 값 가져오기

import { useRef } from "react";

const SimpleInput = (props) => {
  const nameInputRef = useRef();

  const formSubmitHandler = (event) => {
    event.preventDefault();
    
    const enteredValue = nameInputRef.current.value;
    console.log(enteredValue);
    
    nameInputRef.current.value = "";
  };

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

export default SimpleInput;

폼이 제출되었을 때 한 번만 값이 필요하다면 ref를 사용하는 것이 좋다.
그런데 이 방법은 돔을 직접 조작하기 때문에 권장하는 방법은 아니다.


비교

  • 상태: //
  • ref: 그냥
import { useRef, useState } from "react";

const SimpleInput = (props) => {
  // const [userName, setUserName] = useState("");

  const nameInputRef = useRef();

  // const userNameChangeHandler = (event) => {
  //   setUserName(event.target.value);
  // };

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

    // console.log(userName);
    // setUserName("");

    const enteredValue = nameInputRef.current.value;
    console.log(enteredValue);
    nameInputRef.current.value = "";
  };

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

export default SimpleInput;

2-2. 검증하기


  • 브라우저에서 검증
    브라우저에서 하는 유효성 검증, 즉 클라이언트 사이드 유효성 검증은, 실제 웹 사이트나 웹 앱에서 브라우저에서 유효성 검증하는 방식은 사용자 경험 측면에서 빠른 반응을 할 수 있으므로 좋다.

  • 서버에서 검증
    그런데 브라우저에서 뿐만 아니라 서버에서도 입력 값들이 검증되어야 한다.
    브라우저에 있는 코드는 사용자에 의해 편집될 수 있기 때문에, 이는 사용자 경험을 위한 장치일 뿐이다.
    보안을 위해 서버에서도 검증이 필요하다.

2-2-1. input이 하나인 경우

입력 값이 공란일 때 폼을 제출할 수 없게 하는 유효성 검증

  • 입력 값이 유효한지 확인
    • 유효하다면 정상 처리
    • 유효 하지 않은데 입력창 건드린 상태면 사용자에게 에러 보여주고 블러처리
      • 에러 뜬 상태에서 다시 입력하기 시작하면 에러 즉시 없애기
import { useState } from "react";

const SimpleInput = (props) => {
  //입력된 인풋 값 저장하는 상태
  const [userName, setUserName] = useState("");
  //사용자가 인풋을 건드렸는지
  const [enteredNameTouched, setEnteredNameTouched] = useState(false);

  //새로운 값 입력될 때 마다 이 전체 컴포넌트가 재실행되기 때문에
  //가장 최신의 userName, enteredNameTouched 상태를 반영할 수 있다.

  //여기엔 userName 확인한 값 저장
  //이 조건식이 true면, 즉 입력값에 뭔가 들어가 있으면 유효한 값
  const enteredNameIsValid = userName.trim() !== "";
  //❌ 인풋 유효하지 않은 경우 = 입력창은 건드려졌지만(true), 값이 유효하지 않을 때(!false)
  const nameInputIsInvalid = enteredNameTouched && !enteredNameIsValid;

  const userNameChangeHandler = (event) => {
    setUserName(event.target.value);
    //setUserName 함수를 통해 전체 컴포넌트 리렌더링 됨
  };

  //바로바로 검증하기 위해 유효하지 않으면 블러처리
  const nameInputBlurHandler = () => {
    //인풋이 블러되는건 사용자가 인풋 건드렸기 때문에 true로
    setEnteredNameTouched(true);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    setEnteredNameTouched(true);

    //formSubmitHandler 함수는 컴포넌트가 리렌더링 될 때마다 재생성됨
    //따라서 이 함수는 enteredNameIsValid의 최신값을 가져올 수 있으니 문제 없음
    if (!enteredNameIsValid) {
      return;
    }

    console.log(userName);
    setUserName("");
    //터치도 초기화 해줘서 인풋 안건드린걸로 바꾸기
    setEnteredNameTouched(false);
  };

  //유효성 검증에 따라 스타일 변경
  const nameInputClasses = nameInputIsInvalid
    ? "form-control invalid"
    : "form-control";

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Your Name</label>
        <input
          type="text"
          id="name"
          onChange={userNameChangeHandler}
          value={userName}
          onBlur={nameInputBlurHandler}
        />
        {nameInputIsInvalid && (
          <p className="error-text">Name must not be empty.</p>
        )}
      </div>
      <div className="form-actions">
        <button>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

2-2-2. input이 여러 개인 경우: 전체 form 유효성 관리하기

전체 form이 유효하기 위해서는 모든 input이 유효해야 한다.

import { useState } from "react";

const SimpleInput = (props) => {
  const [userName, setUserName] = useState("");
  const [enteredNameTouched, setEnteredNameTouched] = useState(false);

  const enteredNameIsValid = userName.trim() !== "";
  const nameInputIsInvalid = enteredNameTouched && !enteredNameIsValid;

  //이렇게 하면 간결하게 코드 작성 가능
  let formIsValid = false;

  //전체 폼 폼 유효성 설정
  //폼의 각 인풋에 대해서 설정하면 된다.
  if (enteredNameIsValid) {
    formIsValid = true;
  }
  
  //인풋이 name과 password 두 가지라면 아래처럼
  // if (enteredNameIsValid && passwordIsValid) {
  //  formIsValid = true;
  // }

  const userNameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

  const nameInputBlurHandler = () => {
    setEnteredNameTouched(true);
  };

  const formSubmitHandler = (event) => {
    event.preventDefault();
    setEnteredNameTouched(true);

    if (!enteredNameIsValid) {
      return;
    }
    console.log(userName);
    setUserName("");
    setEnteredNameTouched(false);
  };

  //유효성 검증에 따라 스타일 변경
  const nameInputClasses = nameInputIsInvalid
    ? "form-control invalid"
    : "form-control";

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Your Name</label>
        <input
          type="text"
          id="name"
          onChange={userNameChangeHandler}
          value={userName}
          onBlur={nameInputBlurHandler}
        />
        {nameInputIsInvalid && (
          <p className="error-text">Name must not be empty.</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

실전! 두 번째 인풋 만들어서 유효성 검사하기

  • 사용자의 이메일 주소 받기
  • 입력된 값 가져와 올바른 이메일 주소인지 유효성 검증, 방법은 원하는 대로
    • (참고) 구글에 JS 이메일 검증 검색하면 많은 방법 확인할 수 있다.
    • (예시) 간단히 입력된 문자열에 @기호가 있는지 확인하기
  • 최종적으로 두 입력 값 모두 유효할 때만 제출할 수 있게 하기

입력 값을 알맞게 검증해 내는 방식에 대한 연습이다.
이를 위해 상태를 알맞게 사용하여 연습해보자.

import { useState } from "react";

const SimpleInput = (props) => {
  // name
  const [userName, setUserName] = useState("");
  const [enteredNameTouched, setEnteredNameTouched] = useState(false);

  // name validation
  const enteredNameIsValid = userName.trim() !== "";
  const nameInputIsInvalid = enteredNameTouched && !enteredNameIsValid;

  // email
  const [userEmail, setUserEmail] = useState("");
  const [enteredEmailTouched, setEnteredEmailTouched] = useState(false);

  // email validation
  const enteredEmailIsValid = userEmail.includes("@");
  const emailInputIsInvalid = enteredEmailTouched && !enteredEmailIsValid;

  // form
  let formIsValid = false;

  // form validation
  if (enteredNameIsValid && enteredEmailIsValid) {
    formIsValid = true;
  }

  // name
  const userNameChangeHandler = (event) => {
    setUserName(event.target.value);
  };

  const nameInputBlurHandler = () => {
    setEnteredNameTouched(true);
  };

  // email
  const emailChangeHandler = (event) => {
    setUserEmail(event.target.value);
  };
  const emailInputBlurHandler = () => {
    setEnteredEmailTouched(true);
  };

  // form
  const formSubmitHandler = (event) => {
    event.preventDefault();
    setEnteredNameTouched(true);
    setEnteredEmailTouched(true);

    if (!enteredNameIsValid && !enteredNameIsValid) {
      return;
    }
    console.log(userName, userEmail);
    setUserName("");
    setUserEmail("");
    setEnteredNameTouched(false);
    setEnteredEmailTouched(false);
  };

  // name: styles
  const nameInputClasses = nameInputIsInvalid
    ? "form-control invalid"
    : "form-control";

  // email: styles
  const emailInputClasses = emailInputIsInvalid
    ? "form-control invalid"
    : "form-control";

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Your Name</label>
        <input
          type="text"
          id="name"
          onChange={userNameChangeHandler}
          value={userName}
          onBlur={nameInputBlurHandler}
        />
        {nameInputIsInvalid && (
          <p className="error-text">Name must not be empty.</p>
        )}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor="email">Your E-mail</label>
        <input
          type="email"
          id="email"
          onChange={emailChangeHandler}
          value={userEmail}
          onBlur={emailInputBlurHandler}
        />
        {emailInputIsInvalid && (
          <p className="error-text">Please enter a valid E-mail.</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

3. 커스텀 훅 만들기 (Simplifications)


인풋이 여러개이고 사용하는 로직이 같다면 커스텀 훅을 사용하면 좋다.
물론 커스텀 컴포넌트를 만들어도 되지만 커스텀 훅을 만들어 보자.

📍 /hooks/use-input.js

//인풋 값, 인풋 창 건드려 졌는지 다룸
//유연해야 하기 때문에 확실한 검증 로직은 외부에서 훅으로 전달되어야 함

import { useState } from "react";

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

  // input validation
  //🌟검증 로직은 훅이 사용되는 부분에서 결정헤야 커스텀 훅 유연하게 사용 가능: 훅 인자로 받기
  const valueIsValid = validateValue(enteredValue);
  // enteredName.trim() !== "";
  // enteredEmail.includes("@"));

  // input is invalid
  const hasError = !valueIsValid && isTouched;

  // value change handler
  const valueChangeHandler = (event) => {
    setEnteredValue(event.target.value);
  };

  //input blur handler
  const inputBlurHandler = () => {
    setIsTouched(true);
  };

  const resetState = () => {
    setEnteredValue("");
    setIsTouched(false);
  };

  return {
    value: enteredValue,
    isValid: valueIsValid,
    hasError,
    valueChangeHandler,
    resetState,
    inputBlurHandler,
  };
};

export default useInput;

📍 /src/components.js

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

const SimpleInput = (props) => {
  // name input
  const {
    value: enteredName,
    isValid: enteredNameIsValid,
    hasError: nameInputHasError,
    valueChangeHandler: nameChangeHandler,
    inputBlurHandler: nameBlurHandler,
    resetState: resetNameInput,
  } = useInput((enteredName) => enteredName.trim() !== "");
  //value 이용한 인라인 함수를 훅에 전달하면, value에 대해 위 함수를 호출한 결과가 반환되므로
  //밸리데이션 할 수 있음

  // email input
  const {
    value: enteredEmail,
    isValid: enteredEmailIsValid,
    hasError: emailInputHasError,
    valueChangeHandler: emailChangeHandler,
    inputBlurHandler: emailBlurHandler,
    resetState: resetEmailInput,
  } = useInput((enteredEmail) => enteredEmail.includes("@"));

  // form
  let formIsValid = false;

  // form validation
  if (enteredNameIsValid && enteredEmailIsValid) {
    formIsValid = true;
  }

  // form
  const formSubmitHandler = (event) => {
    event.preventDefault();

    if (!enteredNameIsValid && !enteredEmailIsValid) {
      return;
    }

    console.log(enteredName, enteredEmail);
    resetNameInput();
    resetEmailInput();
  };

  // name: styles
  const nameInputClasses = nameInputHasError
    ? "form-control invalid"
    : "form-control";

  // email: styles
  const emailInputClasses = emailInputHasError
    ? "form-control invalid"
    : "form-control";

  return (
    <form onSubmit={formSubmitHandler}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Your Name</label>
        <input
          type="text"
          id="name"
          onChange={nameChangeHandler}
          value={enteredName}
          onBlur={nameBlurHandler}
        />
        {nameInputHasError && (
          <p className="error-text">Name must not be empty.</p>
        )}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor="email">Your E-mail</label>
        <input
          type="email"
          id="email"
          onChange={emailChangeHandler}
          value={enteredEmail}
          onBlur={emailBlurHandler}
        />
        {emailInputHasError && (
          <p className="error-text">Please enter a valid E-mail.</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

4. 리듀서 연습

굳이 복잡하지 않아서 하지 않아도 되지만 리듀서를 연습해 보자.

import { useReducer } from "react";

const initialInputState = { value: "", isTouched: false };

const inputStateReducer = (preState, action) => {
  if (action.type === "INPUT") {
    return { value: action.value, isTouched: preState.isTouched };
    //isTouched는 true로 설정하지 않음: 키를 입력 하는 중에 입력을 마치는 것이 아니기 때문에
    //따라서 그냥 이전 상태로 설정
  }
  if (action.type === "BLUR") {
    return { value: preState.value, isTouched: true };
    //value 값도 그냥 이전 값으로 하면 된다. 키 입력 마다 받고 있기 때문에
  }
  if (action.type === "RESET") {
    return { value: "", isTouched: false };
  }
  return { value: "", isTouched: false };
};

const useBasicInput = (valueValidate) => {
  const [inputState, dispatch] = useReducer(
    inputStateReducer,
    initialInputState
  );

  const valueChangeHandler = (event) => {
    dispatch({ type: "INPUT", value: event.target.value });
  };
  const inputBlurHandler = () => {
    dispatch({ type: "BLUR" });
  };

  const reset = () => {
    dispatch({ type: "RESET" });
  };

  const isValueValid = valueValidate(inputState.value);

  const hasError = !isValueValid && inputState.isTouched;

  return {
    enteredValue: inputState.value,
    isValueValid,
    hasError,
    valueChangeHandler,
    inputBlurHandler,
    reset,
  };
};

export default useBasicInput;
profile
Always have hope🍀 & constant passion🔥

0개의 댓글