useState
로 입력값을 관리했다.useState
React에서 제공하는 가장 대표적인 state 관리 hook이다.
함수형 컴포넌트에서 상태(state)를 관리할 수 있게 해준다.
import {useState} from "react";
const [state, setState] = useState(initialValue);
초기값을 인자로 받아 해당 state와 state를 업데이트할 수 있는 함수를 반환한다.
state
: 현재 상태 값을 저장한다.
setState
: 상태를 업데이트하는 함수로, 호출 시 새로운 값을 전달하여 상태를 갱신한다.
useReducer
React에서 제공하는 또 다른 state 관리 hook으로, 더 복잡한 상태 로직을 처리하거나 여러 값에 대한 상태 업데이트가 필요할 때 유용하다.
import {useReducer} from "react";
const [state, dispatch] = useReducer(reducer, initialValue);
useReducer는 두 개의 파라미터를 받는다. 첫 번째는 reducer 함수이고 두 번째는 초기값이다.
이 함수는 두 가지 값을 반환하는데, 첫 번째는 state이고 두 번째는 state를 변경할 수 있는 dispatch 함수이다.
reducer
: 상태 업데이트 로직을 정의하는 함수로, 두 개의 인자를 받는다. 첫 번째는 현재 상태, 두 번째는 액션 객체이다. reducer 함수는 액션에 따라 새로운 상태를 반환한다.
dispatch
: 상태를 변경하기 위해 사용하는 함수로, 액션 객체를 인자로 받는다. dispatch는 reducer 함수가 새로운 상태를 반환하도록 트리거한다.
useForm
useForm은 react-hook-form 라이브러리에서 제공하는 폼 관리 hook으로, React에서 폼을 간편하게 처리할 수 있게 해준다.
폼 데이터의 상태 관리, 검증, 제출 등을 효율적으로 다룰 수 있어 코드의 복잡성을 줄여준다.
import { useForm } from "react-hook-form";
const { register, handleSubmit, watch, setValue, reset, formState } = useForm();
parameter로 폼 필드의 기본 상태를 설정하는 초기값을 넣을 수 있다. (필수는 아님)
useForm을 사용하려면 react-hook-form을 설치해야 한다.
npm install react-hook-form
useForm은 다음과 같이 다양한 기능을 제공한다.
register
: input에 state를 연결(등록)하는 함수이다. 폼 데이터의 상태를 추적하고 관리한다. 이를 통해 input을 useForm과 연결하여 자동으로 상태를 관리한다.
<input {...register("username")} />
handleSubmit
: 폼 제출 시 호출되는 함수이다. 제출 전에 데이터 검증을 수행하고 성공적인 제출 시 지정된 콜백 함수를 실행한다.
const onSubmit = data => {
console.log(data);
};
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username")} />
<button type="submit">Submit</button>
</form>
watch
: 폼의 입력 필드 상태를 추적하는 함수이다. 특정 입력 필드나 전체 폼 데이터를 실시간으로 감지할 수 있다. 이 기능은 폼 데이터를 실시간으로 렌더링하거나, 동적으로 변경할 때 유용하다.
const username = watch("username");
setValue
: setState와 비슷하게 폼의 상태 값을 변경하는 함수이다. 입력 필드의 값을 직접 설정할 수 있다.
setValue("username", "new value");
reset
: 폼 데이터를 초기화하거나 기본값으로 되돌리는 함수이다. 폼의 값을 초기 상태로 되돌리고 싶을 때 사용한다.
reset({ username: "default value" });
formState : {errors, isSubmitting}
: 폼의 상태 정보를 제공하는 객체로, errors와 isSubmitting 등을 포함한다. errors는 폼 필드의 검증 오류 상태를 나타내며, isSubmitting은 폼이 제출 중인지 여부를 나타낸다.
const { errors, isSubmitting } = formState;
아래 경우에 사용하면 유용하다.
다음의 요구사항을 참고해서 회원가입 기능을 구현한다.
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [nickname, setNickname] = useState("");
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
<form onSubmit={handleSignUp}>
<div>
<label>*이름</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
pattern="^[가-힣a-zA-Z]{2,}$"
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
</div>
<div>
<label>*나이</label>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
required
pattern="^[1-9][0-9]*$"
placeholder="1 이상의 양의 정수로 입력"
/>
</div>
<div>
<label>별명 (선택)</label>
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
pattern="^[a-zA-Z가-힣]*$"
placeholder="영문 또는 한글로 입력 (선택)"
/>
</div>
<div>
<label>*아이디</label>
<input
type="text"
value={id}
onChange={(e) => setId(e.target.value)}
required
pattern="^[a-zA-Z0-9]{5,20}$"
placeholder="영문과 숫자를 포함한 5~20글자로 입력"
/>
</div>
<div>
<label>*비밀번호</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
pattern="^(?=.*[a-zA-Z])(?=.*\d).{8,20}$"
placeholder="영문과 숫자를 포함해 8~20글자로 입력"
/>
</div>
<button type="submit" disabled={!isFormComplete}>
가입하기
</button>
</form>;
// state 리셋 함수
const resetForm = () => {
setName("");
setAge("");
setNickname("");
setId("");
setPassword("");
}
// 폼 제출 함수
const handleSignUp = async (e) => {
e.preventDefault(); // 기본 제출 동작을 막는다.
setIsLoading(true); // 로딩 상태를 true로 만든다.
const userInfo = { name, age, nickname, id, password };
try {
await signUpUser(userInfo); // 회원가입 api를 호출한다.
alert("회원가입 성공");
resetForm(); // state 리셋
navigate("/login"); // 로그인 페이지로 이동
} catch (err) {
console.error("회원가입 오류 :", err);
alert("회원가입 실패"); // 경고창을 띄운다.
} finally {
setIsLoading(false); // 로딩 상태를 false로 만든다.
}
};
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { signUpUser } from "../apis/signUpUser";
const SignupForm = () => {
const [name, setName] = useState("");
const [age, setAge] = useState("");
const [nickname, setNickname] = useState("");
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const isFormComplete = name && age && id && password;
const resetForm = () => {
setName("");
setAge("");
setNickname("");
setId("");
setPassword("");
}
const handleSignUp = async (e) => {
e.preventDefault();
setIsLoading(true);
const userInfo = { name, age, nickname, id, password };
try {
await signUpUser(userInfo);
alert("회원가입 성공");
resetForm();
navigate("/login");
} catch (err) {
console.error("회원가입 오류 :", err);
alert("회원가입 실패");
} finally {
setIsLoading(false);
}
};
return (
<div>
{isLoading && "로그인중입니다.."}
<h1>회원가입</h1>
<form onSubmit={handleSignUp}>
<div>
<label>*이름</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
pattern="^[가-힣a-zA-Z]{2,}$"
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
</div>
<div>
<label>*나이</label>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
required
pattern="^[1-9][0-9]*$"
placeholder="1 이상의 양의 정수로 입력"
/>
</div>
<div>
<label>별명 (선택)</label>
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
pattern="^[a-zA-Z가-힣]*$"
placeholder="영문 또는 한글로 입력 (선택)"
/>
</div>
<div>
<label>*아이디</label>
<input
type="text"
value={id}
onChange={(e) => setId(e.target.value)}
required
pattern="^[a-zA-Z0-9]{5,20}$"
placeholder="영문과 숫자를 포함한 5~20글자로 입력"
/>
</div>
<div>
<label>*비밀번호</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
pattern="^(?=.*[a-zA-Z])(?=.*\d).{8,20}$"
placeholder="영문과 숫자를 포함해 8~20글자로 입력"
/>
</div>
<button type="submit" disabled={!isFormComplete}>
가입하기
</button>
</form>
<a onClick={() => navigate("/login")}>로그인</a>
</div>
);
};
export default SignupForm;
상태를 객체로 관리해주면 하나의 useState 만으로도 여러 상태를 관리할 수 있다.
값을 업데이트하는 부분도 간결하게 나타낼 수 있다.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { signUpUser } from "../apis/signUpUser";
const SignupForm = () => {
const [formData, setFormData] = useState({
name: "",
age: "",
nickname: "",
id: "",
password: ""
});
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const isFormComplete = formData.name && formData.age && formData.id && formData.password;
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData((prevData) => ({
...prevData,
[name]: value
}));
};
const resetForm = () => {
setFormData({
name: "",
age: "",
nickname: "",
id: "",
password: ""
});
};
const handleSignUp = async (e) => {
e.preventDefault();
setIsLoading(true);
const userInfo = { ...formData };
try {
await signUpUser(userInfo);
alert("회원가입 성공");
resetForm();
navigate("/login");
} catch (err) {
console.error("회원가입 오류 :", err);
alert("회원가입 실패");
} finally {
setIsLoading(false);
}
};
return (
<div>
{isLoading && "로그인중입니다.."}
<h1>회원가입</h1>
<form onSubmit={handleSignUp}>
<div>
<label>*이름</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
pattern="^[가-힣a-zA-Z]{2,}$"
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
</div>
<div>
<label>*나이</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleInputChange}
required
pattern="^[1-9][0-9]*$"
placeholder="1 이상의 양의 정수로 입력"
/>
</div>
<div>
<label>별명 (선택)</label>
<input
type="text"
name="nickname"
value={formData.nickname}
onChange={handleInputChange}
pattern="^[a-zA-Z가-힣]*$"
placeholder="영문 또는 한글로 입력 (선택)"
/>
</div>
<div>
<label>*아이디</label>
<input
type="text"
name="id"
value={formData.id}
onChange={handleInputChange}
required
pattern="^[a-zA-Z0-9]{5,20}$"
placeholder="영문과 숫자를 포함한 5~20글자로 입력"
/>
</div>
<div>
<label>*비밀번호</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
pattern="^(?=.*[a-zA-Z])(?=.*\d).{8,20}$"
placeholder="영문과 숫자를 포함해 8~20글자로 입력"
/>
</div>
<button type="submit" disabled={!isFormComplete}>
가입하기
</button>
</form>
<a onClick={() => navigate("/login")}>로그인</a>
</div>
);
};
export default SignupForm;
const initialState = {
name: "",
age: "",
nickname: "",
id: "",
password: ""
};
const formReducer = (state, action) => {
switch (action.type) {
case "SET_FIELD":
return {
...state,
[action.field]: action.value
};
case "RESET":
return initialState;
default:
return state;
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({ type: "SET_FIELD", field: name, value });
};
const resetForm = () => {
dispatch({ type: "RESET" });
};
import { useReducer, useState } from "react";
import { useNavigate } from "react-router-dom";
import { signUpUser } from "../apis/signUpUser";
const initialState = {
name: "",
age: "",
nickname: "",
id: "",
password: ""
};
const formReducer = (state, action) => {
switch (action.type) {
case "SET_FIELD":
return {
...state,
[action.field]: action.value
};
case "RESET":
return initialState;
default:
return state;
}
};
const SignupForm = () => {
const [formData, dispatch] = useReducer(formReducer, initialState);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const isFormComplete =
formData.name && formData.age && formData.id && formData.password;
const handleInputChange = (e) => {
const { name, value } = e.target;
dispatch({ type: "SET_FIELD", field: name, value });
};
const resetForm = () => {
dispatch({ type: "RESET" });
};
const handleSignUp = async (e) => {
e.preventDefault();
setIsLoading(true);
const userInfo = { ...formData };
try {
await signUpUser(userInfo);
alert("회원가입 성공");
resetForm();
navigate("/login");
} catch (err) {
console.error("회원가입 오류 :", err);
alert("회원가입 실패");
} finally {
setIsLoading(false);
}
};
return (
<div>
{isLoading && "로그인중입니다.."}
<h1>회원가입</h1>
<form onSubmit={handleSignUp}>
<div>
<label>*이름</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
pattern="^[가-힣a-zA-Z]{2,}$"
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
</div>
<div>
<label>*나이</label>
<input
type="number"
name="age"
value={formData.age}
onChange={handleInputChange}
required
pattern="^[1-9][0-9]*$"
placeholder="1 이상의 양의 정수로 입력"
/>
</div>
<div>
<label>별명 (선택)</label>
<input
type="text"
name="nickname"
value={formData.nickname}
onChange={handleInputChange}
pattern="^[a-zA-Z가-힣]*$"
placeholder="영문 또는 한글로 입력 (선택)"
/>
</div>
<div>
<label>*아이디</label>
<input
type="text"
name="id"
value={formData.id}
onChange={handleInputChange}
required
pattern="^[a-zA-Z0-9]{5,20}$"
placeholder="영문과 숫자를 포함한 5~20글자로 입력"
/>
</div>
<div>
<label>*비밀번호</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
pattern="^(?=.*[a-zA-Z])(?=.*\d).{8,20}$"
placeholder="영문과 숫자를 포함해 8~20글자로 입력"
/>
</div>
<button type="submit" disabled={!isFormComplete}>
가입하기
</button>
</form>
<a onClick={() => navigate("/login")}>로그인</a>
</div>
);
};
export default SignupForm;
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm();
pattern에서 message 속성을 추가하면 에러 메세지를 간편하게 보여줄 수 있다.
<div>
<label>*이름</label>
<input
type="text"
{...register("name", {
required: "이름은 필수 항목입니다.",
pattern: {
value: /^[가-힣a-zA-Z]{2,}$/,
message: "2글자 이상의 영문 또는 한글로 입력",
},
})}
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>;
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { signUpUser } from "../apis/signUpUser";
const SignupForm = () => {
const {
register,
handleSubmit,
formState: { errors },
reset
} = useForm();
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const onSubmit = async (data) => {
setIsLoading(true);
try {
await signUpUser(data);
alert("회원가입 성공");
reset(); // 폼 리셋
navigate("/login");
} catch (err) {
console.error("회원가입 오류 :", err);
alert("회원가입 실패");
} finally {
setIsLoading(false);
}
};
return (
<div>
{isLoading && "로그인중입니다.."}
<h1>회원가입</h1>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>*이름</label>
<input
type="text"
{...register("name", {
required: "이름은 필수 항목입니다.",
pattern: {
value: /^[가-힣a-zA-Z]{2,}$/,
message: "2글자 이상의 영문 또는 한글로 입력"
}
})}
placeholder="2글자 이상의 영문 또는 한글로 입력"
/>
{errors.name && <p>{errors.name.message}</p>}
</div>
<div>
<label>*나이</label>
<input
type="number"
{...register("age", {
required: "나이는 필수 항목입니다.",
pattern: {
value: /^[1-9][0-9]*$/,
message: "1 이상의 양의 정수로 입력"
}
})}
placeholder="1 이상의 양의 정수로 입력"
/>
{errors.age && <p>{errors.age.message}</p>}
</div>
<div>
<label>별명 (선택)</label>
<input
type="text"
{...register("nickname", {
pattern: {
value: /^[a-zA-Z가-힣]*$/,
message: "영문 또는 한글로 입력"
}
})}
placeholder="영문 또는 한글로 입력 (선택)"
/>
{errors.nickname && <p>{errors.nickname.message}</p>}
</div>
<div>
<label>*아이디</label>
<input
type="text"
{...register("id", {
required: "아이디는 필수 항목입니다.",
pattern: {
value: /^[a-zA-Z0-9]{5,20}$/,
message: "영문과 숫자를 포함한 5~20글자로 입력"
}
})}
placeholder="영문과 숫자를 포함한 5~20글자로 입력"
/>
{errors.id && <p>{errors.id.message}</p>}
</div>
<div>
<label>*비밀번호</label>
<input
type="password"
{...register("password", {
required: "비밀번호는 필수 항목입니다.",
pattern: {
value: /^(?=.*[a-zA-Z])(?=.*\d).{8,20}$/,
message: "영문과 숫자를 포함해 8~20글자로 입력"
}
})}
placeholder="영문과 숫자를 포함해 8~20글자로 입력"
/>
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit" disabled={isLoading}>
가입하기
</button>
</form>
<a onClick={() => navigate("/login")}>로그인</a>
</div>
);
};
export default SignupForm;
참고한 블로그
https://kyung-a.tistory.com/39