티스토리 뷰
단계적으로 validation logic을 구축해본다.
useState() 뿐만 아니라 useReducer()도 사용해본다.
Form validation (폼 유효성 검사)
- user가 올바른 입력값을 전송하도록 여러 조건을 체크하고 잘못된 점이 있으면 알려주는 것
- 회원가입이나 기업의 입사지원서 작성시 흔히 볼 수 있음
Form은 언제 validate해야 할까?
- form이 submit 됐을 때 (
onSubmit
event) - user가 input에서 focus를 잃었을 때 (
onBlur
event) - 모든 keystroke (
onChange
event)
Empty input 처리 방법
- was touched state: 입력칸을 클릭했는지 여부
- lost focus: input field에 입력하다가 다 지우고 빈칸으로 놔둔 경우
예제와 함께 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>
- submit button은 모든 input field가 valid할때만 활성화되도록 한다.
- 해당 input field가 잘못되면 적절한 CSS와 함께 안내 문구도 아래에 함께 표시할 것
Ver.1
- input field 하나하나 전부 event handler를 추가하기로 한다.
- field 입력값과 input 클릭여부는 useState로 관리한다.
- 입력된 값에 맞게 validation을 한다. (e.g. 일반 텍스트값: empty string인지?, 이메일: '@'을 포함하고 있는지?)
- input field에 접근한 적은 있는데 입력된 값이 invalid할 경우 CSS class를 변경하여 색을 빨간색으로 바꿔준다.
- 입력값이 모두 valid할 경우에만 button을 활성화한다 -
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";
rendering 부분
<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
으로 만들어두고 재사용을 해볼 것이다.
Ver.2
- custom hook인
useInput()
을 만든다. Object destructuring
으로 각 input field별 naming을 한다.
Custom Hook이란?
- stateful logic을 재사용가능한 function으로 빼는 것
- hook name은 무조건 use로 시작해야 한다.
- 일반적인 function과 다르게 custom hook 안에선 React state나 React hook들을 사용할 수 있다.
- 아무거나 return 가능하다. (변수 한 개라던지, Object 라던지)
useEffect
나useMemo
처럼 argument를 넘기고 parameter로 받을 수 있다.- custom hook을 호출하면 호출한 component 안에 hook의 state가 tie되기 때문에 logic만 공유되고 state는 따로 관리할 수 있다.
방법
- 우선
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은 원하는 대로 가져오면 된다.
- form submit 이후 state를 reset하는 logic도 함수로 처리한다.
const reset = () => { setEnteredValue(""); setIsTouched(false); };
- 나는 className도 hook 안에서 해결하고 싶어서 가져왔다.
const inputClasses = hasError ? "form-control invalid" : "form-control";
- form submit 이후 state를 reset하는 logic도 함수로 처리한다.
- state 관리는 이제 hook 안에서만 하게 된다.
- 하지만 validation logic은 input마다 다르기 때문에, 이 부분은 form에서 넘어온 함수형 parameter를 이용한다.
const useInput = (validation) => {
...
const enteredValueIsValid = validation(enteredValue);
...
}
- 이렇게 만들어진 변수와 함수는 return하여 꺼내쓸 수 있게 하자.
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() !== "");
- 깔끔하게 hook을 부르는 것 만으로 모든 validation이 처리되었다!
Ver.3
- 이번엔
useState()
가 아닌useReducer()
로 state를 관리해본다. - state가 더 많고 복잡할때 유용한 방법이다.
useState()
vs. useReducer()
useState()
- main 상태관리 도구
- 개별 state나 data
- update가 한정되어 있고 state update가 쉬울때
useReducer()
useState()
를 너무 많이 써야할 때- 관련된 state나 data
- state update가 복잡할 때
- 뭐가 맞고 뭐가 틀리다는 없으므로 상황에 맞게 사용하면 된다.
- 지금의 validation logic에는 state가 많지 않기 때문에
useState()
로도 충분하지만 연습용으로useReducer()
로도 해보기로 한다.
useReducer()의 기본 구조
const [(state snapshot), (dispatch 함수)] = useReducer(
(reducer 함수), (initial state), (init 함수)
);
- state snapshot:
useState()
에서 첫번째 인자와 같은 의미. 쉽게 말해 현재 state 상태 (값)을 담고있다. - dispatch 함수: state update를 유도한다.
- reducer 함수
- action이 fetch되면 자동으로 trigger된다.
- 가장 최근의 state snapshot을 return한다.
- 구조는
(prevState, action) => newState
이다.
action 값에 따라 state를 반환한다.
- initial state: state의 초기 상태를 넣어준다.
- init 함수 (optional)
- lazy initialization을 하고 싶을때 넘겨주는 인자이다.
- reducer 밖으로 initial state를 처리하는 logic을 빼고 싶을때 사용한다.
- action의 응답에 따라 state를 나중에 reset하고 싶을 때 편하다.
(공식 문서 참고)
이론만 보면 이해하기 어려우니 실제 적용된 예시를 보자.
ver 2의 logic을 useReducer()
로 바꿔본다.
- useState()에서 관리되는 state는 initialState로 들어간다.
const initialInputState = { value: "", isTouched: false, };
- action의 type을 정의하기 위해 뭐가 있는지 살펴본다.
state를 변경시키는 logic을 살펴보면 된다."INPUT"
:onChangeHandler
에서 value값이event.target.value
로 변한다."BLUR"
:onBlurHandler
에서 isTouched 값이 변경된다."RESET"
: state가 초기화된다.
- 이를 바탕으로
dispatch
로 함수들을 바꾼다. - reducer 함수에서 state 값이 바뀌면 type만 정의해서 보내면 된다.
const onBlurHandler = () => {
dispatch({ type: "BLUR" });
};
const onChangeHandler = (event) => {
dispatch({
type: "INPUT",
value: event.target.value,
});
};
const reset = () => {
dispatch({ type: "RESET" });
};
- 위에서 정의한 세 type으로 Reducer 함수를 구성하면 된다.
이때 변경되지 않는 값은 기존 state값을 그대로 넘겨주어야 한다. -
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;
결과
마무리
- form validation을 하는 방법을 단계적으로 구성하는 방법을 정리해보았다.
- 관리해야 하는 input field가 많아짐에 따라 custom hook을 사용하여 hook만 호출한다면 reusable한 코드 사용으로 인해 컴포넌트를 가볍게 관리할 수 있을 것이다.
useState()
가 메인 상태관리 도구이지만 상황에 따라useReducer()
를 사용하여 state를 관리하는 방법도 있다.- 이렇게 custom hook을 사용해도 되지만 다른 form library를 사용하여도 된다. (e.g. Formik)
'Web > React' 카테고리의 다른 글
setState의 비동기적 속성, prevState, prevProps (0) | 2021.04.22 |
---|---|
React Class Method와 Arrow Function (0) | 2021.04.21 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- vue.js
- 이분탐색
- 알고리즘
- BigInteger
- 벨만포드
- 다익스트라
- 해시
- CustomHook
- web
- regex
- dfs
- 브루트포스
- 정규식
- REACT
- 문자열
- matches
- 백준
- 시뮬레이션
- swea
- java
- 그래프
- 구현
- form
- 프로그래머스
- 우선순위큐
- BFS
- dp
- 삼성역테기출
- Validation
- 백트래킹
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함