단계적으로 validation logic을 구축해본다.
useState() 뿐만 아니라 useReducer()도 사용해본다.
user가 올바른 입력값을 전송하도록 여러 조건을 체크하고 잘못된 점이 있으면 알려주는 것
회원가입이나 기업의 입사지원서 작성시 흔히 볼 수 있음
onSubmit
event)onBlur
event)onChange
event)예제와 함께 form validation을 추가해보자🧐
<form>
<div className='control-group'>
<div className='form-control'>
<label htmlFor='name'>First Name</label>
<input type='text' id='name' />
</div>
<div className='form-control'>
<label htmlFor='name'>Last Name</label>
<input type='text' id='name' />
</div>
</div>
<div className='form-control'>
<label htmlFor='name'>E-Mail Address</label>
<input type='text' id='name' />
</div>
<div className='form-actions'>
<button>Submit</button>
</div>
</form>
disabled
attribute 이용아래는 first name만 적용한 코드이다.
const [enteredFirstName, setEnteredFirstName] = useState("");
const [nameInputIsTouched, setNameInputIsTouched] = useState(false);
const enteredFirstNameIsValid = enteredFirstName.trim() !== "";
const firstNameInputIsInvalid = nameInputIsTouched && !enteredFirstNameIsValid;
const formIsValid = enteredFirstNameIsValid;
const firstNameInputBlurHandler = (event) => {
setNameInputIsTouched(true);
};
const firstNameInputChangeHandler = (event) => {
setEnteredFirstName(event.target.value);
};
const submitFormHandler = (event) => {
event.preventDefault();
if (!enteredFirstNameIsValid) return;
console.log(enteredFirstName);
setEnteredFirstName("");
setNameInputIsTouched(false);
};
const nameInputClasses = firstNameInputIsInvalid
? "form-control invalid"
: "form-control";
<form onSubmit={submitFormHandler}>
<div className="control-group">
<div className={nameInputClasses}>
<label htmlFor="name">First Name</label>
<input
type="text"
id="name"
value={enteredFirstName}
onBlur={firstNameInputBlurHandler}
onChange={firstNameInputChangeHandler}
/>
{firstNameInputIsInvalid && (
<p className="error-text">First name must not be empty.</p>
)}
</div>
<div className="form-control">
<label htmlFor="name">Last Name</label>
<input type="text" id="name" />
</div>
</div>
<div className="form-control">
<label htmlFor="name">E-Mail Address</label>
<input type="text" id="name" />
</div>
<div className="form-actions">
<button disabled={!formIsValid}>Submit</button>
</div>
</form>
지금은 input field가 3개밖에 없어서 last name, email 코드를 first name처럼 추가해도 상관은 없겠지만...
만약에 엄청 많아지게 된다면 계속 복붙복붙하는 반복적인 작업의 연속이고 또 코드도 엄청 길어지게 될것이다.
그래서 validation하는 부분을 따로 빼서 custom hook
으로 만들어두고 재사용을 해볼 것이다.
useInput()
을 만든다.Object destructuring
으로 각 input field별 naming을 한다.useEffect
나useMemo
처럼 argument를 넘기고 parameter로 받을 수 있다.우선 src/hooks/use-input.js
경로를 가지는 파일을 camel case naming으로 생성하고 validate하는 logic을 그대로 use-input.js
로 가져온다.
이 logic은 특정 input이 아닌 모든 경우를 처리할 것이기 때문에 함수 이름을 일반적인 이름으로 바꿔준다.
const [enteredValue, setEnteredValue] = useState("");
const [isTouched, setIsTouched] = useState(false);
const enteredValueIsValid = validation(enteredValue);
const hasError = isTouched && !enteredValueIsValid;
const onBlurHandler = (event) => {
setIsTouched(true);
};
const onChangeHandler = (event) => {
setEnteredValue(event.target.value);
};
가져오고 싶은 logic은 원하는 대로 가져오면 된다.
const reset = () => { setEnteredValue(""); setIsTouched(false); };
const inputClasses = hasError ? "form-control invalid" : "form-control";
state 관리는 이제 hook 안에서만 하게 된다.
하지만 validation logic은 input마다 다르기 때문에, 이 부분은 form에서 넘어온 함수형 parameter를 이용한다.
const useInput = (validation) => {
...
const enteredValueIsValid = validation(enteredValue);
...
}
return {
value: enteredValue,
isValid: enteredValueIsValid,
hasError,
onBlurHandler,
onChangeHandler,
reset,
inputClasses,
};
hook을 form 안에서 사용할 차례이다.
Object destructuring으로 각각 input별로 변수를 정의하면 된다. 기본 형태는 아래와 같으며
const {} = useInput((value) => value.trim() !== '');
실제 적용시 이런 모습이다.
const {
value: enteredFirstName,
isValid: enteredFirstNameIsValid,
hasError: firstNameInputHasError,
onBlurHandler: firstNameBlurHandler,
onChangeHandler: firstNameChangeHandler,
reset: resetFirstNameInput,
inputClasses: firstNameInputClasses,
} = useFormInput((value) => value.trim() !== "");
이번엔 useState()
가 아닌 useReducer()
로 state를 관리해본다.
state가 더 많고 복잡할때 유용한 방법이다.
useState()
vs. useReducer()
useState()
useReducer()
useState()
를 너무 많이 써야할 때useState()
로도 충분하지만 연습용으로 useReducer()
로도 해보기로 한다.const [(state snapshot), (dispatch 함수)] = useReducer( (reducer 함수), (initial state), (init 함수) );
state snapshot: useState()
에서 첫번째 인자와 같은 의미. 쉽게 말해 현재 state 상태 (값)을 담고있다.
dispatch 함수: state update를 유도한다.
reducer 함수
(prevState, action) => newState
이다.initial state: state의 초기 상태를 넣어준다.
init 함수 (optional)
(공식 문서 참고)
이론만 보면 이해하기 어려우니 실제 적용된 예시를 보자.
ver 2의 logic을 useReducer()
로 바꿔본다.
const initialInputState = {
value: "",
isTouched: false,
};
"INPUT"
: onChangeHandler
에서 value값이 event.target.value
로 변한다."BLUR"
: onBlurHandler
에서 isTouched 값이 변경된다."RESET"
: state가 초기화된다.dispatch
로 함수들을 바꾼다. const onBlurHandler = () => {
dispatch({ type: "BLUR" });
};
const onChangeHandler = (event) => {
dispatch({
type: "INPUT",
value: event.target.value,
});
};
const reset = () => {
dispatch({ type: "RESET" });
};
const inputStateReducer = (state, action) => {
if (action.type === "INPUT")
return {
value: action.value,
isTouched: state.isTouched,
};
else if (action.type === "BLUR")
return { value: state.value, isTouched: true };
else if (action.type === "RESET") return { value: "", isTouched: false };
return inputStateReducer;
};
useState()
의 state를 사용하던 코드를 intialState.(state 이름)으로 변경해주면 된다.const enteredValueIsValid = validation(inputState.value);
const hasError = inputState.isTouched && !enteredValueIsValid;
...
return {
value: inputState.value,
...
}
import { useReducer } from "react";
const initialInputState = {
value: "",
isTouched: false,
};
const inputStateReducer = (state, action) => {
if (action.type === "INPUT")
return {
value: action.value,
isTouched: state.isTouched,
};
else if (action.type === "BLUR")
return { value: state.value, isTouched: true };
else if (action.type === "RESET") return { value: "", isTouched: false };
return inputStateReducer;
};
const useInput = (validation) => {
const [inputState, dispatch] = useReducer(
inputStateReducer,
initialInputState
);
const enteredValueIsValid = validation(inputState.value);
const hasError = inputState.isTouched && !enteredValueIsValid;
const inputClasses = hasError ? "form-control invalid" : "form-control";
const onBlurHandler = () => {
dispatch({ type: "BLUR" });
};
const onChangeHandler = (event) => {
dispatch({
type: "INPUT",
value: event.target.value,
});
};
const reset = () => {
dispatch({ type: "RESET" });
};
return {
value: inputState.value,
isValid: enteredValueIsValid,
hasError,
onBlurHandler,
onChangeHandler,
reset,
inputClasses,
};
};
export default useFormInput;
useState()
가 메인 상태관리 도구이지만 상황에 따라 useReducer()
를 사용하여 state를 관리하는 방법도 있다.