우리는 useState를 알고있고, 활용할 수 있다.
또한, 이 훅을 통해 상태관리를 문제없이 진행하고 있었다.
하지만, 작업을 하다보면 state들이 많이 존재하거나, 서로 엉켜있어 상당히 골치아픈 경우가 종종 발생한다.
이런 경우에도 useState를 활용해서 당연히 해결할 수 있다.
그래도 마음속으로는 좀 더 편한 기능이 있었으면 좋겠다 라고 바라는게 정상일것이다.
이러한 고민을 해결해주는 리액트 훅이 useReducer 훅이다.
이 글에서 알아볼 것
- useReducer
- useEffect와 useReducer 같이 사용할 때 발생하는 사소한 문제점
1. useReducer
앞에서 간략하게 소개했지만 다시 자세히 알아보자
- useReducer란?
- state를 관리하는 리액트 훅이다.
- 정확히 말하면, 복잡한 state를 관리할때 유용한 리액트 훅이다.
- 복잡한 state란 무엇일까?
- 여러 state들이 함께 속해있는 경우 (같은 것을 관리하지만, 관리하는 측면이 다를경우)
- 여러 state가 같이 바뀌거나, 서로 관련되어있는 경우
- 글로만 봐서는 "useState로 할만한데?" 생각될수도있지만,, 추후 서술할 문제에 거의 무조건 직면하게 된다.
- 일단 복잡한 state라는게 뭔지 코드로 살펴보자
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
- 로그인을 관리하는 state들이다...입력값을 위한 state와 유효성 검사를 위한 state가 존재한다.
- 이걸 useState만 사용해서 간단한 로직을 작성해보자
const [enteredEmail, setEnteredEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState();
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
setFormIsValid(event.target.value.includes("@") && enteredPassword.trim().length > 6);
};
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes("@"));
};
다 작성하기에는 좀 그렇고, email 관련만 작성해봤다.
뭐가 문제인지 파악이 되는가?
코드 한줄을 뜯어와서 살펴보자
setFormIsValid(event.target.value.includes("@") && enteredPassword.trim().length > 6);
- 이 코드는 동작하는데는 문제가 없지만, 좋지 못한 코드다.
- 왜냐하면, 이 코드는 두개의 다른 state를 기반으로 state를 업데이트하는 함수다.
- 우리는 이런 비슷한 상황을 겪은적이 있다..바로 이전 state의 활용이다.
- 이때는 함수를 이용해 이전 state와 최신 state를 기반으로 state를 업데이트하였다.
- 이 코드에도 똑같이 함수를 이용하고 싶지만, 불가능하다
- 왜냐하면 그 방법은 state 업데이트가 동일한 state의 이전 state 스냅샷에 의존하는 경우에만 가능하기 때문이다.
- 하지만 이 코드는 전혀 다른 state의 스냅샷에 의존하고있다
- 결국 저런식으로 코드를 작성할수밖에없는데, 이러면 문제가 발생한다.
- 이메일 유효성 검사에서 문제가 발생하면 setFormIsValid 함수를 통해 state가 업데이트 될것이다..하지만 이때 enteredPassword state는 최신 상태의 스냅샷이 아닐수있다.
- 왜냐하면, 보통 로그인을 할 때 email을 입력한 후 비밀번호를 입력하는데, 만약 email을 입력할 때 우리가 작성해놓은 유효성 검사에 걸렸다고 가정해보자
- 그러면 setFormIsValid 함수가 트리거되것이고, state가 업데이트된다.
- 하지만 아직 비밀번호에 대한 어떠한 작업도 하지 않았다...
- 즉, enteredPassword state는 최신 상태의 스냅샷이 아닐수있다.
- 이메일 유효성 검사에서 문제가 발생하면 setFormIsValid 함수를 통해 state가 업데이트 될것이다..하지만 이때 enteredPassword state는 최신 상태의 스냅샷이 아닐수있다.
- 이처럼 다른 state에 의해 state가 바뀌는 상황등은 어떻게 해결해야할까?
- useReducer 훅을 사용하자!
- useReducer 사용법
- const [state, dispatchFn] = useReducer(reducerFn, initState, initFn)
- state → 최신 state 스냅샷
- dispatchFn → state 스냅샷을 업데이트해주는 함수
- useState의 업데이트함수와 헷갈릴 수 있는데,
- useState의 업데이트함수는 새로운 state 값을 설정하였지만, useReducer의 업데이트함수는 액션을 디스패치한다
- reducerFn → 최신 state 스냅샷을 자동으로 가져오고, 디스패치된 액션을 가져오는 함수
- 액션이 디스패치될때마다 reducerFn이 호출된다
- 새로운 업데이트된 state를 반환한다
- initState, initFn → 초기 state나 초기 함수를 설정할수있다
- 구조를 살펴봤는데,, 액션은 뭐고 디스패치는 뭔지 어려울수있다..
- const [state, dispatchFn] = useReducer(reducerFn, initState, initFn)
위에 useState로만 작성된 문제의 코드를 useReducer로 바꿔보면서 이해해보자
- useReducer의 정의하자
- const [emailState, dispatchEmail] = useReducer(emailReducer, { value: "", isValid: undefined });
- 문제코드의 useState가 다루는 state중에 enteredEmail, emailIsValid를 각각 value, isValid로 생각하자
- reducerFn을 정의하자
const emailReducer = (state, action) => {
return { value: "", isValid: false };
};
- reducerFn는 보통 컴포넌트 함수 밖에 정의한다
- 왜냐하면 이 함수는 컴포넌트 함수 내부에서 만들어진 어떤 데이터도 필요하지 않기때문이다
- (state, action)에서 state는 리액트가 제공하는 최신 state 스냅샷이고, action은 dispatchFn에서 정의된 액션이다
3. 이제 dispatchFn을 이용하여 emailChangeHandler 부터 고쳐보자
const emailChangeHandler = (event) => {
dispatchEmail({ type: "USER_INPUT", payload: event.target.value });
setFormIsValid(event.target.value.includes("@") && enteredPassword.trim().length > 6);
};
- 여기서 dispatchEmail에 인자로 들어가는 것이 액션인데,
- 이 액션이 무엇인지 정하는 것은 내 마음이다..보통 객체다.
- 액션 객체에는 보통 식별자인 type과 지정한 값을 전달하는 payload가 들어간다
4. 이제 다시 reducerFn으로 돌아가 마무리하자
const emailReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.includes("@") };
}
return { value: "", isValid: false };
};
- if문을 이용해 action.type을 확인해주고, enteredEmail 역할을 하는 value에는 action.payload를 (= event.target.value), emailIsValid 역핳을 하는 isValid에는 조건을 넣어주자
이러면 이전에 useState를 통해서 작성한 로직과 정확히 같은 기능을 수행한다.
거기다 훨씬 좋은 코드다.
2. useEffect와 useReducer 같이 사용할 때 발생하는 사소한 문제점
const [formIsValid, setFormIsValid] = useState(false);
useEffect(() => {
const timerHandler = setTimeout(() => {
console.log("사이드 이펙트 함수");
setFormIsValid(emailState.isValid && passwordState.isValid);
}, 500);
return () => {
console.log("클린업 함수");
clearTimeout(timerHandler);
};
}, [emailState, passwordState]);
- useReducer로 바꾼후에 다시 코드를 살펴보니, 앞서 살펴본 문제점이 보인다.
- form 유효성 검사 state가 다른 state에 의존하고있는점이다.
- 하지만, 괜찮다…useEffect는 의존성이 변하면 무조건 다시 실행되므로 앞서 살펴본 문제점이 발생하지 않는다
- 하지만, 다른 문제점이 존재한다
- 이 이펙트가 너무 자주 실행된다는 점이다
- 우리가 실행하려는 사이드이펙트함수의 목적은 단순히 form의 유효성 검사인데, 의존성때문에 모든 변경에 트리거된다
- 어떻게 해결할까?…자바스크립트의 객체 구조 분해를 활용하자!
const { isValid: emailIsValid } = emailState;
const { isValid: passwordIsValid } = passwordState;
useEffect(() => {
const timerHandler = setTimeout(() => {
console.log("사이드 이펙트 함수");
setFormIsValid(emailIsValid && passwordIsValid);
}, 500);
return () => {
console.log("클린업 함수");
clearTimeout(timerHandler);
};
}, [emailIsValid, passwordIsValid]);
이제 우리가 살펴본 리액트 훅은 3개다
useState
useEffect
useReducer
앞의 두개의 개념도 어려웠지만,
이번 개념은 처음 듣는 용어들이 많아 더욱 어렵게 느껴졌다.
(디스패치, 액션...)
그래도 막상 이해하고나니 흐름이 보이고, 언제 사용해야할지도 명확해졌다.
다음 글에서도 또 다른 리액트 훅을 살펴볼것인데,, 그것은 이것보다 조금 더 난해할 수 있다.
하지만, 너무 중요한 기능이므로 다음 글에서 자세히 살펴보자
'OLD > React' 카테고리의 다른 글
[React] React Redux_01 (Redux 기본) (0) | 2023.03.05 |
---|---|
[React] React Context (useContext) (0) | 2023.03.03 |
[React] React useEffect 훅 (0) | 2023.03.01 |
[React] React에서 사용자 입력을 컨트롤 하는 두 가지 방법 (state vs ref) (0) | 2023.02.26 |
[React] React 여러 기능들 (Fragment, Portal) (0) | 2023.02.25 |