
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값은 어떤 값이든 상관없으나, 관례적으로 대문자로만 이루어진 문자열을 사용한다.
전반적으로 잘 이해가 되질 않을 정도로 복잡하다는 생각이 들었다.
실제 얼마나 쓰일지는 모르겠지만 나중에라도 다시 보고 이해할 수 있게 잘 정리해야겠다는 생각이 든다.