[React 완벽 가이드] Section 17 : Form

gonn-i·2024년 6월 22일
0

React 완벽 가이드

목록 보기
13/18
post-thumbnail

본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.

목차 🌳
1️⃣ 폼을 다루는 어려움?
2️⃣ Form 제출을 다루는 방법 & 유효성 검사
3️⃣ Custom Hook으로 처리하기

폼 왜 다루기 어려울까? 😵‍💫

Form 양식은 입력창의 집합이다! 근데 입력값 받아주기 뿐만 아니라, 유효성 검사도 해줘야 한다.
[Form 형식이 하는일]
1️⃣ Form 제출
-> 입력된 값은 state 관리 / 아니면 refs로 참조 / 것도 아니면 FormData 객체 사용
2️⃣ Input 유효성 검사
-> 키를 누를때마다 올바르지 않는 입력값에 대해 검증 결과 알려줌
-> 사용자 경험에 따라 언제 오류를 출력할지는 조금씩 다름

Form 제출을 다루는 방법 & 유효성 검사 🦾

Form 제출

    <form onSubmit={handleSubmit}>
      <h2>Login</h2>

      <div className="control-row">
        <div className="control no-margin">
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" />
        </div>

 		...

      <p className="form-actions">
        <button className="button button-flat">Reset</button>
        <button className="button">Login</button>
      </p>
    </form>

Form 형태에서 제출을 누르게 된다면, Form 양식 요소에서 요청을 발생시키고 -> 그 요청이 서버로 전송되는게 일반적이다. 그치만 Form 제출 시, 브라우저는 기본적으로 페이지를 새로 고침하게 된다.

싱글 페이지 어플리케이션인 React에겐 전체 페이지를 로드하는 것은 날벼락 ! ⚡️⚡️

그렇기 때문에 필요한 부분만을 업데이트하는 방식을 고수하기 때문에, handleSubmit 안에
e.preventDefault() 을 수행해준다


Form input 수집 및 관리

아무튼 Form의 input을 받아내는 방법은 ~

1️⃣ State 관리를 통한 input value 관리

import { useState } from 'react';
export default function Login() {
  const [enteredValue, setEnteredValue] = useState({
    email: '',
    passward: '',
  });

  function handleSubmit(e) {
    e.preventDefault();
    console.log(enteredValue);
  }

  function handleInputChange(identifier, e) {
    setEnteredValue((prev) => ({
      ...prev,
      [identifier]: e.target.value,
    }));
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>

      <div className="control-row">
        <div className="control no-margin">
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            name="email"
            onChange={(e) => handleInputChange('email', e)}
            value={enteredValue.email}
          />
        </div>

        // ... 생략
    </form>
  );
}

2️⃣ Ref v를 통한 input value 관리

import { useRef } from 'react';
export default function Login() {
  const emailRef = useRef();
  const passwordRef = useRef();

  function handleSubmit(e) {
    e.preventDefault();
    const enteredEmail = emailRef.current.value;
    const enteredPassword = passwordRef.current.value;

    console.log(enteredEmail, enteredPassword);
  }

  return (
    <form onSubmit={handleSubmit}>
      <h2>Login</h2>

      <div className="control-row">
        <div className="control no-margin">
          <label htmlFor="email">Email</label>
          <input id="email" type="email" name="email" ref={emailRef} />
        </div>

      // ... 생략
  );
}

두 방법 비교 🥊

사실 확실한 정답은 없지만, 따지고 봤을때 입력값의 갯수가 작고 간단할때는 Ref가 더 간결

ref가 간결한 사유) onChange를 통해 입력에 대한 상태 변화를 제어할 필요가 없음
-> 그치만 단점, DOM을 다룰때, 참조로 재설정 하는 등의 경우엔 좀 신중해야함

반대로 입력폼이 복잡할때) 그땐 useRef 로 연결할 값이 늘어나기 때문에 state 관리가 더 좋지 않을까

3️⃣ FormData 객체 활용하기

FormData 객체란,
HTML 폼 형태의 데이터를 쉽게 관리하고 전송할 수 있도록 해주는 API

  • key-value로 데이터를 저장 )
    폼 요소의 name과 value로 key-value를 쌍으로 저장
  • <input type="file"> 를 통해 파일 전송도 쉽게 처리 가능

FormData 사용 예시,

 function handleSubmit(e) {
    e.preventDefault();

    const fd = new FormData(e.target); // form 요소를 전달
    const data = Object.fromEntries(fd.entries()); // ⬇️ 하단에 설명 추가 
    const acquisitionChannel = fd.getAll('acquisition'); // name이 'acquisition'인 모든 값을 배열로 반환하여 저장	
    data.acquisition = acquisitionChannel; // acquisition 속성 추가 후, 그 값으로 acquisitionChannel 배열을 설정ㄴ
    console.log(data);
  }

entries() 는 fd 에 들어있는 모든 key-value 쌍을 포함하는 배열을 반환. [name, value]
Object.fromEntries() 는 위의 값을 사용해, key-value 쌍으로 구성된 객체로 반환 {name: value}

암튼 쉽게! 복잡한 형태의 Form을 다룰 수 있다!

Form input 초기화

1️⃣ form 요소에 reset 호출
e.target.reset();

2️⃣ state를 초기값으로 업데이트하기
setEnteredValue({ email: '', passward: ''});

유효성 검사하기 🔍

유효성 검사는 그 시점에 따라 접근법이 달라지는데,
!제출시! 값들의 유효성을 검사할 경우에는 크게 방법에 구애받지 않지만,
만일 매번 !키 입력에 따라! 유효성 검사를 할 경우에는 => state를 이용해야 한다.

!키 입력에 따라! 유효성 검사를 할 경우

근데 타이핑을 치는 순간순간에 유효성을 검사할 경우, (ex. 이메일 형식 @ 들어가는지)
오류 문구가 일찍 보여지는 경향이 있다.

그렇기 때문에, onBlur 이벤트리스너를 통해서, 포커스를 잃은 경우 검증하되, 값을 입력하기 시작하면 오류 메시지가 사라지게 하는 것이 Best!!!!

function Login() {
  const [enteredValue, setEnteredValue] = useState({
    email: '',
    passward: '',
  });

  // 유효성 검증할 시점
  const [didEdit, setDifEdit] = useState({
    email: false,
    passward: false,
  });

  // 이메일 형식에 @가 있는지 
  const emailIsInvaild = didEdit.email && !enteredValue.email.includes('@');

	// 입력시 (포커스 상태)일때, 검증 X
  function handleInputChange(identifier, e) {
    setEnteredValue((prev) => ({
      ...prev,
      [identifier]: e.target.value,
    }));
    setDifEdit((prev) => ({
      ...prev,
      [identifier]: false,
    }));
  }

  // 포커스 아웃시, 검증 ! 
  function handleEmailBlur(identifier) {
    setDifEdit((prev) => ({
      ...prev,
      [identifier]: true,
    }));
  }

  // 이하 함수 생략.. 
  return (
    <form onSubmit={handleSubmit}>

      <div className="control-row">
        <div className="control no-margin">
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            name="email"
            onChange={(e) => handleInputChange('email', e)}
            onBlur={() => handleEmailBlur('email')}
            value={enteredValue.email}
          />
          <div className="control-error">{emailIsInvaild && <p>plz enter invalid email</p>}</div>
        </div>
	
        // ... 생략
        
    </form>
  );
}

!제출시! 값들의 유효성을 검사할 경우

1️⃣ 방법1 : 제출시 유효한지를 판별한 상태 하나 더 선언

  const [emailIsInvalid, setEmailIsInvalid] = useState(false);
  // .. 일부 생략

  function handleSubmit(e) {
    e.preventDefault();
    const enteredEmail = emailRef.current.value;

    console.log(enteredEmail);
    const emailIsValid = enteredEmail.includes('@');

    if (!emailIsValid) {
      setEmailIsInvalid(true);
      return;
    }

    setEmailIsInvalid(false);
  }

2️⃣ 방법2 : 내장된 Prop을 사용

required : input value가 공란인지 확인하고, input type에 따라 유효성 판별됨

	<div className="control">
		<label htmlFor="email">Email</label>
		<input id="email" type="email" name="email" required />
    </div>

minLength={num} : 최소 입력글자수를 정하여, 유효하지 않을 경우 제출을 막음

	 <div className="control">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" name="password" required minLength={8} />
	</div>
  • 물론, 빌드인 기능과 이외의 직접 만든 유효성 검사를 함께 써도 무방 !

Custom 으로 처리하기

1️⃣ 중복되는 label + input + error 코드는 Custom Component로!

Input.jsx

export default function Input({ label, id, error, ...props }) {
  return (
    <div className="control no-margin">
      <label htmlFor={id}>{label}</label>
      <input id={id} {...props} />
      <div className="control-error">{error && <p>{error}</p>}</div>
    </div>
  );
}

custom Component 사용

<Input
  label="Email"
  id="email"
  type="email"
  name="email"
  onChange={handleEmailChange}
  onBlur={handleEmailBlur}
  value={emailValue}
  error={emailHasError && 'Please enter a valid email!'}
  />

2️⃣ 유효성 검사 함수도 따로 모아두고 재사용하기!

📁 utils -> validations.js

export function isEmail(value) {
  return value.includes('@');
}

export function isNotEmpty(value) {
  return value.trim() !== '';
}

export function hasMinLength(value, minLength) {
  return value.length >= minLength;
}

export function isEqualsToOtherValue(value, otherValue) {
  return value === otherValue;
}

-> 이러면 boolean 으로 값이 반환!

3️⃣ input 에 대한 입력상태 관리 + 유효성 검증은 Custom Hook으로!

📁 Hooks -> useInput.js

useInput.js

import { useState } from 'react';

export function useInput(defaultValue, validationFn) {
  const [enteredValue, setEnteredValue] = useState(defaultValue);
  const [didEdit, setDidEdit] = useState(false);

  const valueIsValid = validationFn(enteredValue);

  function handleInputChange(e) {
    setEnteredValue(e.target.value);
    setDidEdit(false);
  }

  function handleInputBlur() {
    setDidEdit(true);
  }

  return { value: enteredValue, handleInputBlur, handleInputChange, hasError: didEdit && !valueIsValid };
}

custom hook 사용

  const {
    value: emailValue,
    handleInputBlur: handleEmailBlur,
    handleInputChange: handleEmailChange,
    hasError: emailHasError,
  } = useInput('', (value) => isEmail(value) && isNotEmpty(value));

0개의 댓글