What is useReducer()?
앞서서 useState()
와 useEffect()
에 대해서 다뤄봤었다.
이번에는 다음 React hook인 useReducer()
에 대해서 이야기해보자.
간단히 말해, state의 관리를 도와주는 것으로 useState()
와 비슷하다.
오히려 더 많은 기능들을 가지고 있다. 특히, 더 복잡한 state에 유용하다.
예를 들어, 여러 state들이 함께 속해 있는 경우에 같이 바뀐다던가 서로가 관련되어 있다면 관리하는 측면에서 사용이나 관리가 어려워지거나 오류가 발생하기 쉬워진다.
효율이 나빠지거나 버그가 생길 수 있는 코드가 되기 쉽상이다.
물론 그런 상황에 다다르는 것을 원하는 개발자는 아무도 없을 것이다.
React project를 진행하면서 더 강력한 state 관리가 필요한 경우, useReducer()
가 useState()
를 대신해줄 수 있다.
하지만 그렇다고 해서 항상 useReducer()
를 사용해야한다는 것은 아니다.
더 강력하다고 해서 항상 모든 상황에서 더 좋다고는 말할 수 없기 때문이다.
useReducer()
는 useState()
보다 사용하기 조금 더 복잡하기 때문에 더 많은 설정이 필요하다.
따라서, 대부분의 경우에는 useState()
를 사용하는 편이 더 유리하다.
Why use it?
그러나 useReducer()
가 작동하도록 추가적인 작업을 하는 것이 더 가치있는 경우가 있다.
이전 코드에서 한번 살펴보자.
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
여기에서는 5가지의 state를 사용했었다 :
- 사용자로부터 받아온 email과 password를 관리하는 state
- 받은 email과 password과 전체 form의 유효성을
boolean
타입으로 관리하는 state
이 과정에서 중복적으로 동작하는 부분을 찾아볼 수 있는데, email과 password의 유효성을 확인함으로써 전체 form의 유효성을 설정한다는 것이다.
이 코드에서 문제가 있었는데 아래 코드를 한번 보길 바란다.
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
해당 handler에서 사용하는 enteredEmail
과 emailIsValid
state는 전혀 다른 state다.
물론, 두가지 모두 사용자가 입력한 내용에 따라 바뀐다는 관련이 있지만 엄밀히 말하자면 두가지는 다른 state다.
때문이 이런식으로 enteredEmail
을 가지고 emailIsValid
를 판단하는 것은 해서는 안되는 일이다.
대부분의 경우에 잘 동작하겠지만 항상 그렇다고 자부할 수 없다.
왜냐하면 enteredEmail
에 대한 state 업데이트가 제 시간에 처리되지 않을 수도 있기 때문이다.
이런 경우 emailIsValid
는 이전의, 오래된 enteredEmail
을 기반으로 업데이트하게 되는 것이다.
따라서, 여기서는 이전 state에 의존하게끔 state를 업데이트해야한다.
하지만 그것은 불가능하다. state 업데이트 함수를 이전 state에 의존하게끔 만들기 위해서는 같은 state에 한해서만 가능하기 때문이다.
이런 경우에 useReducer()
를 사용하는 것이 항상 좋다.
물론, 다른 state를 기반으로 하는 state를 업데이트하고자 한다면, 하나의 객체 형태로 state를 병합하는 것도 좋은 방법이다.
해당 case의 경우, useReducer()
없이도 충분히 구현할 수 있다.
하지만 state가 더 복잡해지고 커진다면, 또한 여러 가지 관련된 state들이 서로 결합된 경우라면 useReducer()
도 고려할만 하다.
Structure of useReducer()
useReducer 불러오기
우선 사용하기 위해서는 useReducer()
를 불러와야한다.
import React, { useReducer } from "react";
useReducer 선언
이렇게 불러온 useReducer()
는 다음과 같이 선언할 수 있다.
useState()
처럼 항상 두 개의 값이 있는 배열을 반환하기에 배열 destructuring을 사용할 수 있다.
이것은 state 관리 메커니즘이기 때문에 useState()
와 비슷한 구조를 갖는다.
return value : [state 변수, state를 업데이트할 수 있게 해주는 dispatch 함수]
하지만 state를 업데이트하는 함수는 다르게 작동한다. 새로운 state 값을 설정하는 대신 dispatch을 수행한다.
여기서 dispatch란, reducer(state를 변화시키는 로직이 있는 함수)에게 action을 발생시키라고 시키는 것이라고 한다.(아직 잘 모르겠다)
그 action의 동작은 useReducer()
의 첫 번째 parameter인 reducer 함수(reducerFn)로, 최신 state snapshot과 dispatch된 action을 자동으로 가져온다.
React는 새로운 action이 dispatch될 때마다 이 reducerFn을 호출하기 때문에 React로부터 호출된 이 함수는 항상 React가 관리하는 최신의 state snapshot을 가져오게 된다.
useState()
에서의 업데이트 함수와 약간 비슷하나 추가적으로 action이 있기에 확장된 버전이라고 할 수 있다.
또한, useReducer()
의 두 번째, 세 번째 parameter는 state와 함수의 초기값으로 지정할 수 있다.
이렇게 봐서는 도무지 이해가 되질 않는다.
아래의 예제를 통해 살펴보자.
Practice
import React, { useState, useEffect, useReducer } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const emailReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.val, isValid: action.val.includes("@") };
}
if (action.type === "INPUT_BLUR") {
return state ; // { value: state.value, isValid: state.value.includes("@") };
}
return { value: "", isValid: false };
};
const passwordReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.val, isValid: action.val.trim().length > 6 };
}
if (action.type === "INPUT_BLUR") {
return state ; // { value: state.value, isValid: state.value.trim().length > 6 };
}
return { value: "", isValid: false };
};
const Login = (props) => {
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: undefined, //or null
});
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
value: "",
isValid: undefined, //or null
});
const [formIsValid, setFormIsValid] = useState(false);
const { isValid : emailIsValid } = emailState ;
const { isValid : passwordIsValid } = passwordState ;
useEffect(() => {
const identifier = setTimeout(() => {
console.log("Checking form validity!");
setFormIsValid(emailIsValid && passwordIsValid);
}, 500);
return () => {
console.log("CLEANUP");
clearTimeout(identifier);
};
}, [emailIsValid, passwordIsValid]);
const emailChangeHandler = (event) => {
dispatchEmail({ type: "USER_INPUT", val: event.target.value });
setFormIsValid(
event.target.value.includes("@") && passwordState.isValid
);
};
const passwordChangeHandler = (event) => {
dispatchPassword({ type: "USER_INPUT", val: event.target.value });
setFormIsValid(
emailState.isValid && event.target.value.trim().length > 6
);
};
const validateEmailHandler = () => {
dispatchEmail({ type: "INPUT_BLUR" });
};
const validatePasswordHandler = () => {
dispatchEmail({ type: "INPUT_BLUR" });
};
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(emailState.value, passwordState.value);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
formIsValid
: 초기값은false
로 선언된 state이다. 값이true
라면 사용자 input 값이 유효하다는 것을 의미하며, form의 로그인 버튼이 활성화된다. 반대로 값이false
라면 유효하지 않음을 의미하며, 로그인 버튼이 비활성화된다.setFormIsValid
로 값을 업데이트한다.emailChangeHandler
&passwordChangeHandler
: 사용자로 email(또는 password)을 입력받아 값이 변하면[onChange
] 수행되는 handler. type과 val를 지정해서dispatchEmail
(혹은dispatchPassword
)로 dispatch를 보내고 있다. 이 때의 type은 'USER_INPUT'이며, val은 input으로 입력된 값이 할당된다. 이후setFormIsValid
로 조건에 맞는지 업데이트한다.validateEmailHandler
&validatePasswordHandler
: 사용자로 email(또는 password)을 입력받는 입력창에서 벗어나면[onBlur
] 수행되는 handler. type값을 지정해서(val은 사용되지 않기 때문에 할당해주지 않고 있다.)dispatchEmail
(혹은dispatchPassword
)로 dispatch를 보내고 있다. 이 때의 type은 'INPUT_BLUR'이다.emailState
&passwordState
: 초기값은{value: "", isValid: undefined /* (or null) */}
의 객체형태로 가진다. 여기서isValid
의 값을 지정하지 않은 까닭은 만약false
로 되어 있으면, input 값을 입력하지 않아도 유효하지않음을 출력하기 때문이다.useReducer()
로 email(또는 password)을 관리한다.emailReducer
&passwordReducer
: email(또는 password)의 reducer 함수로 기본적으로 최신의 state와 action을 가진다. 앞에서 dispatch한 action 값을 바탕으로 현재의 상태를 판단한다. 'USER_INPUT'은 사용자가 입력창으로 값을 입력하고 있음을 의미하며, 'INPUT_BLUR'는 입력창의focus
가 해제된 상태를 의미한다.return
값은 선언한 state와 동일한 형태의 객체로 반환해줘야한다. 'USER_INPUT'으로 입력이 되고 있을 때라면,action.val
의 값이 사용자 입력값을 의미하며, 매순간 유효한지 체크해서isValid
속성에 넣어준다.focus
가 해제됬다면 parameter로 들어온 state를 그대로return
한다.emailIsValid
&passwordIsValid
:emailState
(또는passwordState
)의isValid
속성의 값을 저장한 const 변수로,useEffect()
안에 들어가는 의존성에는 내부 함수에 들어가는 변수가 사용되어야하기 때문에 새로 할당해주었다. 객체 destructuring을 사용하면 위와 같이 객체의 값을 뽑아올 수 있다.- dispatch시 할당했던 action의 type값은 어떤 값이든 상관없으나, 관례적으로 대문자로만 이루어진 문자열을 사용한다.
전반적으로 잘 이해가 되질 않을 정도로 복잡하다는 생각이 들었다.
실제 얼마나 쓰일지는 모르겠지만 나중에라도 다시 보고 이해할 수 있게 잘 정리해야겠다는 생각이 든다.