리액트 내장 훅인 useReducer()
는 useState()
와 마찬가지로 state 관리를 도와준다. 하지만 더 강력한 state 관리를 도와주는 훅인 useReducer()
는 useState()
에 비해 더 많은 기능을 가지고 있기 때문에 더 복잡한 state(more complex state)를 다룰 때 유용하다.
복잡한 state를 가지는 경우, useState()
나 거기에서 얻은 state의 사용 및 관리가 어려워지거나, 오류가 발생하기 쉽다. 이런 경우 useState()
대신 useReducer()
를 사용하면 더 강력한 state 관리를 할 수 있다.
하지만 더 강력하다고 해서 항상 더 좋은 관리를 하는 것은 아니다. useReducer()
는 사용하기가 보다 복잡하다. 대부분의 경우 useState()
를 사용하는 것이 좋고, 추가로 작업해야 할 만한 경우에만 useReducer()
를 사용하면 된다.
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
//폼의 유효성을 검사하여 업데이트 하는 setFormIsValid()
//enteredEmail, enteredPassword 두 state에 의존하고 있다.
setFormIsValid(
event.target.value.includes("@") && enteredPassword.trim().length > 6
);
}
폼의 유효성을 검사하여 업데이트 하는 setFormIsValid()
에서는 이전 state값을 기반으로 state를 업데이트하는 함수 폼을 사용할 수 없다.
업데이트할 state가 이전 state 스냅샷에 의존하는 경우에만 함수 폼을 사용하여 업데이트 할 수 있는데, 여기서는 현재 2개의 다른 state의 스냅샷, enteredEmail
와 enteredPassword
에 의존하고 있기 때문이다. 이 두가지 state는 formIsValid
의 최근 state 스냅샷이 아니다.
이런식으로 작성하면 안된다. 왜냐하면 리액트가 state 업데이트를 스케줄링하는 방식 때문에 비밀번호 state(enteredPassword
)가 업데이트 처리되기도 전에, 즉, 최근 스냅샷으로 업데이트 되기도 전에 아래 setFormIsValid()
코드가 먼저 실행되어 버릴 수도 있다. 그러면 오류가 생길 수 있다.
setFormIsValid(
event.target.value.includes("@") && enteredPassword.trim().length > 6
);
이전 state 스냅샷에 의존하여 state를 업데이트하는 함수폼을 괜히 사용하는게 아니다. 이는 가장 최신의 스냅샷을 사용할 수 있는 가장 안정적인 방식이다. 🥲
하지만 여기서는 사용할 수가 없어.. 다른 두 state에 의존하고 있기 때문에..
이런 경우 useState()
대신 useReducer()
를 사용하면 된다.
위의 상황처럼 다른 state 값에 의존하여 state를 업데이트 해야 하는 경우
enteredEmail
, enteredPassword
에 의존하여 state를 업데이트하고 있는데, 이는 함수 폼 사용하여 state 업데이트해야 한다는 규칙을 어기는 일이다.또는 함께 속하는 state들이 있는 경우,
enteredEmail
이 있고, 그 값의 유효성을 검사하는 emailIsValid
이 있음다른 state를 기반으로 state를 업데이트하는 경우, 하나의 state로 병합하는 것도 방법이다.
예를 들어 enteredEmail, isEmailValid 두 state를 하나로 만들어서 관리할 수도 있다.
{
email:
enteredEmail,
isEmailValid,
}
이런식으로 하나의 객체로 만들어 계속 useState를 사용할 수도 있다.
하지만 state가 더 복잡해지고 커지면 여러 가지 관련된 state가 결합된 경우라면 useReducer도 고려해 보자~
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
useState()
처럼 useReducer()
도 항상 두 개의 값이 있는 배열을 반환한다
따라서 배열 디스트럭처링을 사용하여 값을 추출하여 별도의 상수에 저장할 수 있다.
state
컴포넌트 재랜더링/재평가 싸이클에 사용되는 최신 state 스냅샷
dispatchFn
새로운 액션을 디스패치하는 함수로, 그 액션은 useReducer()의 첫 번째 인수인 리듀서 함수가 소비한다.
const reducer = (prevState, action) => {
...
};
새 액션이 디스패치될 때 리듀서 함수가 호출되는데, 리듀서 함수는 리액트가 관리하는 최신 state 스냅샷(새로 업데이트된 state)을 자동으로 반환하는 함수이다.
initialState(초기 state)
initFn(초기 함수)
초기 state가 복잡한 경우 초기 state를 설정하기 위해 실행해야 하는 함수를 설정할 수 있다.
입력값, 유효성 결합하기 위해 사용
enteredEmail
, isEmailValid
결합enteredPassword
, isPasswordValid
결합전체 form state 관리하는데 사용
전부 들어 있는 큰 form state 하나로 관리, 또는 작은 state 여러개로 관리
여기서는 입력값, 유효성을 결합하여 관리해보자.
import React, { useReducer } from "react";
리듀서 함수는 컴포넌트 함수 밖에서 명명함수로 작성한다.
컴포넌트 함수 내부에서 만들어진 어떤 데이터도 리듀서 함수 내부에서 필요하지 않기 때문에, 리듀서 함수는 컴포넌트 함수 밖에서 만들어서 사용할 수 있다.
리듀서 함수는 새로운 state 반환하는 함수이다.
여기서는 state{ value: 입력된 이메일 값, isValid: 이메일이 유효 한지에 대한 불리언 값 }
를 객체로 반환하고 있다.
리듀서 함수는 디스패치된 액션의 type에 따라 반환 값을 다르게 할 수 있다.
//이메일 리듀서 함수
const emailReducer = (state, action) => {
//액션의 타입이 USER_INPUT인 경우 => 새롭게 생성된 액션의 페이로드 값 참고
if (action.type === "USER_INPUT") {
return {
value: action.val,
//액션의 페이로드에 @가 포함되어 있는지 => 있으면 true 반환하니까 isValid: true가 된다.
isValid: action.val.includes("@") };
}
//액션의 타입이 INPUT_BLUR인 경우 => 이전 값인 state 참고
if (action.type === "INPUT_BLUR") {
return {
//value 값은 비어있는 값이 아닌 이전에 가졌던 state 값으로 설정해야한다.
//안그러면 사용자가 무언가 입력 후에도 인풋이 블러 처리 될 수 있기 때문이다.
value: state.value,
isValid: state.value.includes("@") };
}
//위 조건에 해당하지 않는 모든 경우에는 기본으로 설정해둔 아래 state 반환
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 { value: state.value, isValid: state.value.trim().length > 6 };
}
return { value: "", isValid: false };
};
//Login 컴포넌트
const Login = (props) => {
//...
const Login = (props) => {
const [formIsValid, setFormIsValid] = useState(false);
// emailReducer 리듀서 포인팅
// emailState 스냅샷에 대해 초기값 설정가능:{ value: "", isValid: false, }
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
//🌟 초기값을 false로 주면 아에 처음부터 이메일 인풋이 invalid되지만
//undefined이나 null로 주면 처음 앱이 렌더링되었을 때는 이메일 인풋이 정상 작동한다.
isValid: null,
});
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
dispatchFn({ type: "대문자", payload })
디스패치되는 액션은 본인 마음대로 정하면 된다. 문자열 식별자일 수도 있고 숫자일 수도 있다. 하지만 보통 객체{ }
를 주로 쓴다.
📝 액션은 어떤 식별자를 가지고 있는 어떤 필드를 가진 객체{ filed: identifier }
이다.
dispatchFn({ type: "대문자", payload })
event.target.value
를 넣으면 된다.dispatchEmail({ type: "USER_INPUT", val: event.target.value });
즉, 액션은 type 필드가 있는 객체이고, type은 어떤 일이 일어나는 지(유저 입력/인풋 블러처리 등)에 대한 설명이다. 또한 여기서는 사용자가 입력한 값을 페이로드로 추가했다.
//...
const emailChangeHandler = (event) => {
//setEnteredEmail(event.target.value);는 더 이상 필요 없음
//🔥유저 입력 시, 새로운 이메일 state 업데이트 하기 위해 액션 디스패치
dispatchEmail({ type: "USER_INPUT", val: event.target.value });
//passwordState.isValid로 유효성 검사
setFormIsValid(event.target.value.includes("@") && passwordState.isValid);
};
const passwordChangeHandler = (event) => {
// setEnteredPassword(event.target.value); 더 이상 필요 없음
//🌟유저 입력 시, 새로운 패스워드 state 업데이트 하기 위해 액션 디스패치
dispatchPassword({
type: "USER_INPUT",
val: event.target.value,
});
// setFormIsValid(
// enteredEmail.includes("@") && event.target.value.trim().length > 6
// ); 더 이상 사용 안함
// emailState 스냅샷 사용하여 검증 값 보내기
setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
};
const validateEmailHandler = () => {
// setEmailIsValid(emailState.isValid); 는 더 이상 필요 없음
//🔥유효성에 따른 인풋 블러 처리 위해 이메일 유효성을 업데이트 하기 위한 액션 디스패치
dispatchEmail({
//액션 구조는 동일하게 작성하는게 좋다.
//유효성검사가 false일 때 인풋이 블러처리되므로 이름을 INPUT_BLUR 라고 지음
type: "INPUT_BLUR",
//여기서는 굳이 val를 설정하지 않아도 되는데 왜냐하면 인풋이 포커스를 잃었다는 사실이 중요하기 때문에
//추가하는 데이터가 없기 때문에 굳이 안 넣어도 된다.
});
};
const validatePasswordHandler = () => {
// setPasswordIsValid(enteredPassword.trim().length > 6); 더 이상 필요 없음
//🌟유효성에 따른 인풋 블러 처리 위해, 패스워드 유효성을 업데이트 하기 위한 액션 디스패치
dispatchPassword({ type: "INPUT_BLUR" });
};
//🔥🌟기존 useState의 state값들 지우고 useReducer의 state.value 값을 prop으로 보내기
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(emailState.value, passwordState.value);
};
return ( ...
//...
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;
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 { value: state.value, isValid: state.value.includes("@") };
}
//아닌 경우 아래 state 반환
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 { value: state.value, isValid: state.value.trim().length > 6 }<;
}
//아닌 경우 아래 state 반환
return { value: "", isValid: false };
};
const Login = (props) => {
// const [enteredEmail, setEnteredEmail] = useState("");
// const [emailIsValid, setEmailIsValid] = useState();
// const [enteredPassword, setEnteredPassword] = useState("");
// const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
useEffect(() => {
console.log("EFFECT RUNNING");
return () => {
console.log("EFFECT CLEANUP");
};
}, []);
// useEffect(() => {
// const identifier = setTimeout(() => {
// console.log('Checking form validity!');
// setFormIsValid(
// enteredEmail.includes('@') && enteredPassword.trim().length > 6
// );
// }, 500);
// return () => {
// console.log('CLEANUP');
// clearTimeout(identifier);
// };
// }, [enteredEmail, enteredPassword]);
const emailChangeHandler = (event) => {
// setEnteredEmail(event.target.value);
dispatchEmail({
type: "USER_INPUT",
val: event.target.value,
});
setFormIsValid(event.target.value.includes("@") && passwordState.isValid);
};
const passwordChangeHandler = (event) => {
// setEnteredPassword(event.target.value);
dispatchPassword({
type: "USER_INPUT",
val: event.target.value,
});
setFormIsValid(emailState.isValid && event.target.value.trim().length > 6);
};
const validateEmailHandler = () => {
//setEmailIsValid(enteredEmail.includes("@"));
dispatchEmail({
type: "INPUT_BLUR",
});
};
const validatePasswordHandler = () => {
//setPasswordIsValid(enteredPassword.trim().length > 6);
dispatchPassword({
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;
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);
};
//...
현재 setFormIsValid()
는 formIsValid state가 아닌 emailState
, passwordState
, 즉 다른 state들의 값에 따라서 유효성 검증을 하고 있다.
코드를 최적화 하기 위해 useEffect()
를 사용하자.
useEffect(() => {
const identifier = setTimeout(() => {
console.log("Checking form validity!");
//✅
setFormIsValid(emailState.isValid && passwordState.isValid);
}, 500);
return () => {
console.log("CLEANUP");
clearTimeout(identifier);
};
//✅
}, [emailState, passwordState]);
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);
};
//...
콘솔을 보면 유효성이 이미 검증되고 나서도 비번에 문자를 추가할 경우 이펙트가 실행되고 있다.
디펜던시에 emailState, passwordState 대신 아에 isValid 값을 주면 이 문제는 해결된다.
emailState, passwordSate에 객체 디스트럭처링으로 emailState.isValid
, passwordSate.isValid
속성을 구조 분해하여 useEffect의 디펜던시에 추가해보자.
const { 속성 이름: 별칭 할당 } = 객체
//✅
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]);
한 번 검증된 유효성은 바뀌지 않기 때문에, 즉 emailIsValid, passwordIsValid의 값은 바뀌지 않기 때문에 콘솔에 더 이상 이펙트가 찍히지 않는 것을 확인할 수 있다.
useEffect()
에 객체의 특정 속성을 종속성으로 추가하기 위해 객체를 구조 분해(destructuring)한다.
const { someProperty } = someObject;
useEffect(() => {
// code that only uses someProperty ...
}, [someProperty]);
이는 매우 일반적인 패턴 및 접근 방식이다.
여기서의 핵심은 destructuring을 사용한다는 것이 아니라, 전체 객체 대신 특정 속성을 종속성으로 전달한다는 것이다.
useEffect(() => {
// code that only uses someProperty ...
}, [someObject.someProperty]);
useEffect(() => {
// code that only uses someProperty ...
}, [someObject]);
이렇게 하면 effect 함수는 전체 객체인 someObject가 변경될 때마다 재실행되기 때문에 불필요한 이펙트가 실행될 수 있다.
객체의 단일 속성이 변경되면 전체 객체가 변경되므로, 더 작은 단위인 단일 속성을 디펜던시로 보내 주는 것이 좋다.
useState를 사용하면 번거로운 경우 useReducer를 사용한다.
useState는 주요 state 관리 도구로 보통 useState로 관리를 시작한다.
대부분의 경우에는 useState만 있으면 된다.
useReducer를 쓰는 것이 좋아보이는 경우에도 특히 useEffect와 함께 useState를 사용하여 처리할 수 있다.
하지만 useReducer를 사용하면 더 우아하고 간단하다.
절대적으로 항상 useReducer를 사용해야 하는 것은 아니다.
예) 두 개의 서로 다른 값을 전환하기만 하는 단순한 state의 경우 useReducer는 너무 과하다.
일단 이렇게 알고 넘어갑시다요~