완벽 가이드 - form

primav·2024년 12월 9일

React

목록 보기
27/35
post-thumbnail

📌 htmlFor

리액트에서 classclassName으로 쓰듯이, label에서 for 대신 htmlFor를 사용한다.

 <label htmlFor="email">Email</label>
 <input id="email" type="email" name="email" />

📌 Form

Form 이 하는 역할

  • 폼 제출 (사용자 입력 관리)

    • 양방향 바인딩을 통한 상태 관리 (state)
    • refs(참조) 를 사용한 값 추출
    • 브라우저가 제공하는 함수를 이용하여 데이터 추출 (formData 객체)
  • 유효성 검사

    • 키를 입력할 때마다 검사 → 너무 빨리 경고
    • 포커스 해제 상황에서 검사 → 너무 오래 경고
    • 폼 제출 시 검사 → 너무 느리게 경고

Form 양식 안에 버튼을 넣으면?

양식을 제출하는 용도로 사용되므로, HTTP요청이 발생하고 바로 웹사이트의 서버로 전송된다.
따라서 웹 페이지가 새로고침 된다!!

👀 이를 막기 위해서는 어떤 방법들이 있을까?

  1. type="button"
  2. type="submit" & onSubmit 이벤트 핸들러

🔑 1. type = "button"

저장하고 새로고침을 눌렀을 때 페이지가 새로고침되지 않는다.
즉, 양식을 제출하지 않는다.

🔑 2. type = "submit" & onSubmit 이벤트 핸들러

const handleSubmit = (event) => {
    event.preventDefault(); // 새로고침 방지
    console.log('폼 제출 방지!');
  };
  
<form onSubmit={handleSubmit}>
	<input type="text" placeholder="입력하세요" />
    <button type="submit">제출</button>
</form>

📌 사용자 입력 관리 (폼 제출)

1. state 사용

개별 스냅샷

이 방법을 사용하면 직관적이고 쉽게 코드를 짤 수 있지만, 관리해야 하는 데이터가 늘어난다면 상태(state) 도 늘어나며 그에 따른 함수도 늘어나서 코드가 너무 길어질 수 있다.

import { useState } from "react";

export default function Login() {
  const [email, setEmail] = useState(""); // 각 데이터의 상태 관리
  const [password, setPassword] = useState(""); // 각 데이터의 상태 관리


  function handleEmailChange(e) { // 상태 관리하는 함수 만들기
    setEmail(e.target.value);
  }

  function handlePasswordChange(e) { // 상태 관리하는 함수 만들기
    setPassword(e.target.value);
  }

  return (
    <input
         id="email"
         type="email"
         name="email"
         onChange={handleEmailChange}
         value={email}
      />
      <input
         id="password"
         type="password"
         name="password"
         onChange={handlePasswordChange} // 키보드 입력마다 상태 변경
         value={password} // 저장된 값 불러와서 데이터 동기화 (입력 필드 & state(상태))
       />
  );
}

공통 스냅샷 - 객체 하나

import { useState } from "react";

export default function Login() {

  const [values, setValues] = useState({
    email: "",
    password: "",
  });

  function handleInputChange(identifier, e) { // 하나의 객체에 모든 상태 관리
    setValues((prev) => ({
      ...prev, // 이전 값 복붙 
      [identifier]: e.target.value, // 식별자에 따라 값 추가
    }));
  }

  return (
   
      <input
         id="email"
         type="email"
         name="email"
         onChange={(e) => handleInputChange("email", e)}
        value={values.email}
       />

      <input
         id="password"
         type="password"
         name="password"
         onChange={(e) => handleInputChange("password", e)} // 익명 함수로 감싸서 보내주기 ( 매개변수 때문에!! )
         value={values.password} // 값 찾아서 빼내기
       />
  );
}

2. Refs 사용

참조를 설정하면 DOM 요소 간에 연결 관계가 생성된다.
이렇게 참조를 사용하여 사용자의 입력을 관리하면 코드가 더 간결하다.

단점은, 각각 참조를 연결해야 해서 번거로울 수 있다.

import { useRef } from "react";

export default function Login() {
  const email = useRef(); // 참조값 생성
  const password = useRef();

  function handleSubmit(e) {
    e.preventDefault();
    
    const savedEmail = email.current.value; // 참조 값에 접근하려면 .current.value로 접근
    const savedPassword = password.current.value;

    console.log(savedEmail, savedPassword);
  }

  return (
    <form onSubmit={handleSubmit}>
       <input id="email" type="email" name="email" ref={email} /> // 참조 연결
       <input id="password" type="password" name="password" ref={password} />
    </form>
  );
}

💡 state vs ref 비교

FeatureState 사용 (제어 컴포넌트)Refs 사용 (비제어 컴포넌트)
사용성상태를 통해 입력 값을 실시간 관리DOM 요소에 직접 접근해 값을 추출
실시간 검증가능 (입력 변경마다 즉시 검증 가능)어려움 (값을 제출 시점에만 확인 가능)
리렌더링 상태 변경 시리렌더링 발생리렌더링 발생하지 않음
사용 방법입력 요소에 대한 변화 제어 함수 & onChange참조 → 참조 속성과 연결 → 값 추출
  • 복잡한 폼 관리가 필요한 경우

    • 상태를 사용하는 제어 컴포넌트 방식 (state)을 추천한다.
    • 특히, 실시간 검증이나 동적 업데이트가 필요할 때 유용하다.
  • 간단한 폼 관리가 필요한 경우:

    • useRef를 사용하는 비제어 컴포넌트 방식을 추천한다.
    • 단, 리액트의 철학(단방향 데이터 흐름)에 어긋날 수 있으니 주의해야한다.

❗️ React에서는 제어 컴포넌트 방식이 일반적으로 권장되며, 특정 상황에서 비제어 컴포넌트를 선택적으로 사용하는 것이 좋다.

👀 그렇다면 더 복잡한 양식을 제출하려면 어떤 방법을 사용해야 할까?

3. FormData 이용

리액트가 기본으로 제공하는 기능이며, 양식에 입력된 각기 다른 값을 쉽게 얻을 수 있도록 도와주는 객체이다.

장점

  • 다양한 입력 필드 데이터 자동 수집
    input, select, textarea, checkbox

  • 다양한 메서드 제공

    • get(name) - 단일 값을 가져옴
    • getAll(name) - 같은 이름을 가진 모든 값을 배열로 반환
    • entries() - 모든 값을 key-value 형태로 반환

주의 사항

  • 값을 추출하려는 모든 입력 값 (input 필드) 에 이름 속성(name)이 있어야 함!
  • 실시간 상태 관리 불가능 (필요시 state 사용하기)
export default function SignUp() {
  function handleSubmit(e) {
    // 여기서 e는 form!!
    e.preventDefault();

    const formObject = new FormData(e.target); // e.target은 form안의 전부
    
    const savedEmail = formObject.get("email");
    const savedPassword = formObject.get("password");
  }

  return (
    <form onSubmit={handleSubmit}>

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

      <div className="control-row">
        <div className="control">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" name="password" />
        </div>
    </form>
  );
}

만약 모든 입력값을 한번에 얻기를 원한다면?

getAll('name')

  function handleSubmit(e) {
    // 여기서 e는 form!!
    e.preventDefault();

    const formObject = new FormData(e.target); 
    const acquisitionChannel = formObject.getAll("acquisition"); // 통일한 name 이름으로
  }

return (
    <form onSubmit={handleSubmit}>

      <div className="control">
        <label htmlFor="email">Email</label>
        <input id="email" type="email" name="acquisition" /> // name 이름 통일!
      </div>

      <div className="control-row">
        <div className="control">
          <label htmlFor="password">Password</label>
          <input id="password" type="password" name="acquisition" /> // name 이름 통일!
        </div>
    </form>
  );

FormData.entries()

FormData 객체에 포함된 모든 키-쌍 값을 Iterator(반복 가능한 객체) 형태로 반환한다. (JS 객체로 변환)
JSON 객체 형태이므로 API에 전달할 때 효과적이다!

const formObject = new FormData(e.target); // e.target은 <form> 요소
const entries = formObject.entries(); 

for (const [key, value] of entries) {
  console.log(key, value);
}
  • 값 키-값 쌍은 [key, value] 형태의 배열
  • FormData.entries()로 얻은 데이터를 for...of 문이나 spread 문법으로 탐색할 수 있다.

📌 Form 초기화

reset 버튼을 활용한 기본 초기화

<button type="reset">Sign Up</button>

버튼 타입

  • submit - 폼 제출 & 렌더링 (기본값)
  • button - 아무 것도 일어나지 않음
  • reset - 폼 초기화

값 직접 초기화

  • 상태 값 (value) 일 때

	setValues({
    	email: '',
        password: '',
    })
  • 참조 값(ref) 일 때

    이 방식은 조심스럽다. 왜냐하면 DOM의 업데이트는 리액트가 맡아 하도록 두는 것이 일반적이기 때문이다.
    DOM에 직접 연결해서 삭제하는 것이므로, 위험하다.
	email.current.value = ''

DOM reset() 메서드 사용

이것 또한 위의 ref와 방식이 같다. 참조를 사용하였을 때 위의 단점을 해결하기 위해 사용하는 것으로, 리액트의 DOM 관리 흐름을 벗어날 수 있다.

event.target.reset();

💡 결론

리액트에서는 상태(state) 관리를 통한 초기화가 가장 권장되는 방식이다. 단순한 HTML 폼이라면 reset 버튼으로도 충분히 처리할 수 있다. DOM 조작은 가능한 피하는 것이 좋다.

📌 유효성 검사

1. 키를 입력할 때마다 검사 (state)

이 방식에는 문제점이 있다.

  1. 이메일 필드를 지워 인풋이 빈칸이 되었을 때도 오류메시지가 보이지 않는다.
  2. 오류메시지는 보이지 않다가, 사용자가 타이핑을 시작하자마자 보인다.

➡️ 오류 메시지가 너무 이르게 보여지고 있다. (타이핑과 동시에)

const emailsInValid = savedValues.email !== '' && !savedValues.email.includes('@');

<div> {emailIsInvalid && <p>error</p>}</div>

2. 포커스 해제 상황에서 검사

아래와 같이 포커스가 해제 되었을 때 (onBlur) 상태를 변경하여 에러 메시지를 띄울 수 있다.
이 때 문제점은, 유효한 값을 얻기전까지는 아무리 입력을 해도 오랫동안 에러 메시지가 뜬다는 것이다.

ex) ublsdkfkdkfmdksf (@ 없으므로 계속 에러 메시지가 뜸)

✔️ Before: 유효한 값 얻기전까지 계속 에러 메시지

const [didEdit, setDidEdit] = useState({
	email: false,
  	password: false
})

function handleInputBlur(identifier) {
  setDidEdit(prev => ({
  	...prev,
    [identifier]: true
  }));
}

<input onBlur={() =>handleInputBlur('email')}/>

✔️ After: 포커스 해제되면 에러메시지 & 타이핑 치는 동시에 유효성 검사

  function handleInputChange(identifier, e) {
    setValues((prev) => ({
      ...prev,
      [identifier]: e.target.value,
    }));
    
   	setDidEdit(prev => ({ // 입력 변화할 때마다 실행
  		...prev,
   	 [identifier]: true
  	}));
  }

3. Form 제출 시 검사

사용자가 입력 중인 상태에서 오류 메시지를 표시하지 않고, 제출 시 한 번에 검사를 처리하는 방법이다.
사용자가 타이핑을 할 때마다 검사를 하지 않아도 되기 때문에 입력 중인 데이턱 ㅏ아니라 제출된 데이터에만 집중할 수 있다.

이 방식으로 하면 폼이 제출 될 때만 검사를 진행하고 에러메시지를 띄우게 된다.

❗️ 이미 키 입력 기반의 방식이 있어도 제출 기반의 유효성 검사를 추가하는 것이 좋다!!

✔️ ref 사용한 코드

const emailsIsValid = savedValues.email.includes('@');

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

setEmailIsInvalid(false);

<div> {emailIsInvalid && <p>error</p>}</div>

4. 내장된 검증 Props 로 검사 (더 쉬운 방식)

<input type="email" /> // 알아서 이메일 유효성 검사를 해줌 (@ 포함 검사)
<checkout required> // 필수 입력
<input type="password" required minLength={8} /> // 8자 이상

✨ 참고 자료 : HTML Form Validation 속성

5. 커스텀 검사

특정 조건을 사용자 정의하여 검증해야 할 때 사용할 수 있다.

ex) 비밀번호와 비밀번호 확인 값 비교

if (data.password !== data['confirm-password']) {
 	setPasswordsAreNotEqual(true);
	return 	
}


<input 
     type="password" 
     id="password" 
     name="password" 
     required 
     minLength={8} 
/>

<input 
     type="password" 
     id="confirm-password" 
     name="confirm-password" 
     required 
     minLength={8} 
/>

6. 유효성 검사 로직 아웃소싱

다른 파일에 미리 유효성 검사 로직을 저장해두고 함수 형태로 꺼내서 쓴다.


📁 validation.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;
}


📁 Login.jsx

import { isEmail, isNotEmpty, hasMinLength } from "../util/validation.js";

const emailIsValid =
  didEdit.email && isEmail(values.email) && !isNotEmpty(values.email);

7. 커스텀 훅

위에서 사용한 handleInputChange, handleBlur 등의 함수는 해당 상태를 업데이트 하기 위한 함수이다. 이 함수들은 여러 폼에서 상태를 관리하고 유효성 검사를 처리하는 데 반복적으로 사용된다.

따라서 우리는 이러한 코드의 중복을 줄이고, 재사용 가능한 상태 관리 로직을 제공하기 위해 커스텀 훅을 사용할 수 있다.
(이때 우리가 원하는 코드의 상태를 관리해야하기 때문에 일반적인 함수는 안되고 커스텀 훅을 사용해야 한다.)

라이브러리 사용

아래와 같은 폼 라이브러리를 사용하면 더 쉽게 구현 가능하다.

0개의 댓글