리액트에서 class를 className으로 쓰듯이, label에서 for 대신 htmlFor를 사용한다.
<label htmlFor="email">Email</label>
<input id="email" type="email" name="email" />
폼 제출 (사용자 입력 관리)
state)refs(참조) 를 사용한 값 추출formData 객체)유효성 검사
양식을 제출하는 용도로 사용되므로, HTTP요청이 발생하고 바로 웹사이트의 서버로 전송된다.
따라서 웹 페이지가 새로고침 된다!!
👀 이를 막기 위해서는 어떤 방법들이 있을까?
onSubmit 이벤트 핸들러저장하고 새로고침을 눌렀을 때 페이지가 새로고침되지 않는다.
즉, 양식을 제출하지 않는다.
const handleSubmit = (event) => {
event.preventDefault(); // 새로고침 방지
console.log('폼 제출 방지!');
};
<form onSubmit={handleSubmit}>
<input type="text" placeholder="입력하세요" />
<button type="submit">제출</button>
</form>
이 방법을 사용하면 직관적이고 쉽게 코드를 짤 수 있지만, 관리해야 하는 데이터가 늘어난다면 상태(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} // 값 찾아서 빼내기
/>
);
}
참조를 설정하면 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>
);
}
💡
statevsref비교
| Feature | State 사용 (제어 컴포넌트) | Refs 사용 (비제어 컴포넌트) |
|---|---|---|
| 사용성 | 상태를 통해 입력 값을 실시간 관리 | DOM 요소에 직접 접근해 값을 추출 |
| 실시간 검증 | 가능 (입력 변경마다 즉시 검증 가능) | 어려움 (값을 제출 시점에만 확인 가능) |
| 리렌더링 상태 변경 시 | 리렌더링 발생 | 리렌더링 발생하지 않음 |
| 사용 방법 | 입력 요소에 대한 변화 제어 함수 & onChange | 참조 → 참조 속성과 연결 → 값 추출 |
복잡한 폼 관리가 필요한 경우
state)을 추천한다.간단한 폼 관리가 필요한 경우:
useRef를 사용하는 비제어 컴포넌트 방식을 추천한다.❗️ React에서는 제어 컴포넌트 방식이 일반적으로 권장되며, 특정 상황에서 비제어 컴포넌트를 선택적으로 사용하는 것이 좋다.
👀 그렇다면 더 복잡한 양식을 제출하려면 어떤 방법을 사용해야 할까?
리액트가 기본으로 제공하는 기능이며, 양식에 입력된 각기 다른 값을 쉽게 얻을 수 있도록 도와주는 객체이다.
다양한 입력 필드 데이터 자동 수집
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>
);
}
만약 모든 입력값을 한번에 얻기를 원한다면?
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 객체에 포함된 모든 키-쌍 값을 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 문법으로 탐색할 수 있다.<button type="reset">Sign Up</button>
submit - 폼 제출 & 렌더링 (기본값)button - 아무 것도 일어나지 않음reset - 폼 초기화value) 일 때 setValues({
email: '',
password: '',
})
ref) 일 때 email.current.value = ''
이것 또한 위의 ref와 방식이 같다. 참조를 사용하였을 때 위의 단점을 해결하기 위해 사용하는 것으로, 리액트의 DOM 관리 흐름을 벗어날 수 있다.
event.target.reset();
💡 결론
리액트에서는 상태(state) 관리를 통한 초기화가 가장 권장되는 방식이다. 단순한 HTML 폼이라면 reset 버튼으로도 충분히 처리할 수 있다. DOM 조작은 가능한 피하는 것이 좋다.
state)이 방식에는 문제점이 있다.
➡️ 오류 메시지가 너무 이르게 보여지고 있다. (타이핑과 동시에)
const emailsInValid = savedValues.email !== '' && !savedValues.email.includes('@');
<div> {emailIsInvalid && <p>error</p>}</div>
아래와 같이 포커스가 해제 되었을 때 (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
}));
}
사용자가 입력 중인 상태에서 오류 메시지를 표시하지 않고, 제출 시 한 번에 검사를 처리하는 방법이다.
사용자가 타이핑을 할 때마다 검사를 하지 않아도 되기 때문에 입력 중인 데이턱 ㅏ아니라 제출된 데이터에만 집중할 수 있다.
이 방식으로 하면 폼이 제출 될 때만 검사를 진행하고 에러메시지를 띄우게 된다.
❗️ 이미 키 입력 기반의 방식이 있어도 제출 기반의 유효성 검사를 추가하는 것이 좋다!!
✔️ ref 사용한 코드
const emailsIsValid = savedValues.email.includes('@');
if(!emailIsValid) {
setEmailIsValid(true);
return;
}
setEmailIsInvalid(false);
<div> {emailIsInvalid && <p>error</p>}</div>
<input type="email" /> // 알아서 이메일 유효성 검사를 해줌 (@ 포함 검사)
<checkout required> // 필수 입력
<input type="password" required minLength={8} /> // 8자 이상
✨ 참고 자료 : HTML Form Validation 속성
특정 조건을 사용자 정의하여 검증해야 할 때 사용할 수 있다.
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}
/>
다른 파일에 미리 유효성 검사 로직을 저장해두고 함수 형태로 꺼내서 쓴다.
📁 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);
위에서 사용한 handleInputChange, handleBlur 등의 함수는 해당 상태를 업데이트 하기 위한 함수이다. 이 함수들은 여러 폼에서 상태를 관리하고 유효성 검사를 처리하는 데 반복적으로 사용된다.
따라서 우리는 이러한 코드의 중복을 줄이고, 재사용 가능한 상태 관리 로직을 제공하기 위해 커스텀 훅을 사용할 수 있다.
(이때 우리가 원하는 코드의 상태를 관리해야하기 때문에 일반적인 함수는 안되고 커스텀 훅을 사용해야 한다.)
아래와 같은 폼 라이브러리를 사용하면 더 쉽게 구현 가능하다.