리액트에서 상태를 관리하기 위해 제공되는 hook은 보통 가장 일반적으로 useState 훅을 사용하여 관리하게 된다.
여기서 그럼 useReducer 훅은 useState 훅과 어떤점이 비슷하고 차이가 있는지 또 어떤 상황에서 각각 맞춰 사용하는편이 좋을것인가?
useState hook과 useReducer hook은 리액트에서 상태관리를 위해 사용되는 두가지 훅이다.
useState는 단순한 상태 값만을 관리하는 반면에, useReducer는 좀 더 복잡한 상태 관리를 할 수 있도록 도움을 준다.
useReducer는 상태 값 뿐만 아니라 액션과 리듀서 함수를 같이 사용하여 상태를 업데이트 해준다.
useState는 단순히 새로운 상태 값을 설정하는 방식(갱신함수적용)으로 동작하지만, useReducer는 현재 상태(state)와 액션(action)을 받아 새로운 상태를 반환하는 리듀서(reducer) 함수를 통해 업데이트 된다.
useState는 개별적인 상태 값을 관리하며, 여러 useState 훅을 사용할 수 있다. 반면에 useReducer는 여러 상태값을 하나의 리듀서 함수로 관리 할수 있기때문에 관련된 상태 값들을 그룹화하고 관리하기 유용하다.
단순한 상태 값의 변경이 필요한 경우 사용한다.
해당 컴포넌트의 관리되는 상태갯수가 적고 간단한 업데이트 로직을 가지고 있는 경우에 적합하다.
예) 토글버튼의 상태관리, 입력 필드값 관리 등
복잡한 상태관리나 상태 간의 의존성이 있는 경우 사용한다.
상태 값이 여러개이고, 상태를 업데이트하는 로직이 복잡하거나 예측하기 어려운 경우에 적합하다.
예) 상태가 여러개의 속성으로 구성되어있고 복잡한 액션과 리듀서 로직이 필요한경우, 상태들을 그룹화하여 관리하고 싶을 경우
결론적으로, useState는 간단한 상태 관리에 사용되며 useReducer는 좀 더 복잡한 상태 관리에 사용된다. 어떤 훅을 선택할지는 상태 관리의 복잡성과 유지보수성을 고려하여 결정해야 한다.
그럼 기존의 useState로 상태를 관리했던부분을 어떻게 useRedcuer로 대체했는지 코드로 살펴보자.
import { useState, useEffect, useReducer } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const Login = (props) => {
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
// 사용자 입력 디바운싱(그룹화)하여 매번 입력이 진행될때 검사를 하는게 아닌 일정 시간이 지나서 사용자입력이 끝났다 생각된다면 그때 한번 이 로직을 실행하여 유효성검사를 하게 하는것이다.
// 타이머를 저장한다
const identifier = setTimeout(() => {
console.log("useEffect안 사이드이펙트 로직 실행");
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, 1000);
// 새로 다시 타이머가 시작하기 전에 이전 타이머를 지워준다.(클린업 함수) -> 단, 첫 번째 사이드이펙트함수가 실행되기 전엔 실행 X
return () => {
console.log("이전 로직 초기화");
clearTimeout(identifier);
};
// 따라서 결국 입력받아 사이드이펙트함수(타이머) 실행되고(마운트) 클린업함수로 이전 타이머 시작전 지워주게(언마운트) 되면 마지막 타이머만 완료될것이다.
}, [enteredEmail, enteredPassword]);
// 이메일 입력 함수
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
setFormIsValid(
event.target.value.includes("@") && enteredPassword.trim().length > 6
);
};
// 비번 입력 함수
const passwordChangeHandler = (event) => {
setEnteredPassword(event.target.value);
setFormIsValid(
enteredEmail.includes("@") && event.target.value.trim().length > 6
);
};
// 이메일 스타일 유효성 함수
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
// 비번 스타일 유효성 함수
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
// 새로운 폼 전달 함수
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(enteredEmail, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
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;
위처럼 useState로 관리하게 될때 아무래도 이 컴포넌트가 사용자의 로그인 정보를 받는 부분은 하나의 행위로 볼 수 있기때문에 연관되는 상태들 끼리 그룹화 시키고 싶다.
또한, 입력을 받으면 입력 하나하나 렌더링이 이루어 지게 되는데
여기서 로그인 입력정보의 유효성 여부만 파악하여 상태를 전달하기 위해 입력값이 아닌 유효성 자체에대한 업데이트만 감지하여 의존성을 부여하게 된다면 입력이벤트가 발생해도 불필요한 리랜더링이 일어나지 않을 수 있다는걸 파악할 수 있다.
이러한 점을 착안해 useRedcuer를 이용해서 상태를 좀 더 확장하여 관리하게 되어 위에 지적한 부분을 꾀할 수 있었다.
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
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.payload, isValid: action.payload.includes("@") };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: state.value.includes("@") };
}
return { value: "", isValid: false };
};
// 비번 리듀서
const passwordReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.trim().length > 6 };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: state.value.trim().lenth > 6 };
}
return { value: "", isValid: false };
};
const Login = (props) => {
const [email, emailDispatch] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const [password, passwordDispatch] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
const [formIsValid, setFormIsValid] = useState(false);
// 상태들 디스트럭처링 하여 useEffect를 더욱 최적화하고 이펙트가 불필요하게 실행되는 것을 피할 수 있게된다.
const { isValid: isValidEmail } = email;
const { isValid: isValidPassWord } = password;
useEffect(() => {
const identifier = setTimeout(() => {
console.log("useEffect안 사이드이펙트 로직 실행");
setFormIsValid(isValidEmail && isValidPassWord);
}, 1000);
return () => {
console.log("이전 로직 초기화");
clearTimeout(identifier);
};
}, [isValidEmail, isValidPassWord]);
// 이메일 입력 함수
const emailChangeHandler = (event) => {
emailDispatch({ type: "USER_INPUT", payload: event.target.value });
};
// 비번 입력 함수
const passwordChangeHandler = (event) => {
passwordDispatch({ type: "USER_INPUT", payload: event.target.value });
};
// 이메일 스타일 유효성 함수
const validateEmailHandler = () => {
emailDispatch({ type: "INPUT_BLUR" });
};
// 비번 스타일 유효성 함수
const validatePasswordHandler = () => {
passwordDispatch({ type: "INPUT_BLUR" });
};
// 새로운 폼 전달 함수
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(email.value, password.value);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
email.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={email.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
password.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password.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;
이렇게 useRedcuer로 대체하여 상태를 관리 해보았다.
확실히 화면이 리렌더링 되는 부분에서 useEffect안 이펙트함수가 의존성이 있는 값이 변할때만 작동하게 되니 불필요한 렌더링을 최소화 할 수 있게 되었다.