[React] useReducer
useReducer는 언제 사용해야 할까?
- useState와 마찬가지로 state 관리를 도와준다.
- 복잡한 state에 유용하다. (여러 state가 같이 바뀌거나 서로 관련된 경우)
- 더 강력한 상태 관리가 필요할 때 useState 대신 사용할 수 있다.
const emailChangeHandler = (event) => {
setEnteredEmail(event.target.value);
setFormIsValid(
event.target.value.includes('@') && enteredPassword.trim().length > 6
);
};
여기서 setFormIsValid 함수를 보면 두 개의 다른 state(enteredEmail, enteredPassword)를 기반으로 업데이트를 하고 있다.
만약 enteredPassword의 상태 업데이트가 처리되기 전에 저 코드가 실행된다면 enteredPassword는 가장 최근에 입력한 암호가 포함되어 있지 않을 수 있다.
하지만 여기선 formIsValid의 가장 최근 스냅샷이 아닌 두 개의 다른 상태에 의존하고 있기 때문에 상태 업데이트 함수에 함수 양식을 사용할 수 없다.
(만약 다음과 같이 useEffect 안에서 하면 괜찮다. 최신 state 값으로 실행되기 때문에)
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(
enteredEmail.includes('@') && enteredPassword.trim().length > 6
);
}, 500);
return () => {
clearTimeout(identifier);
};
}, [enteredEmail, enteredPassword]);
const validateEmailHandler = () => {
setEmailIsValid(enteredEmail.includes('@'));
};
여기서도 enteredEmail을 기반으로 새로운 emailIsValid 상태를 도출하는 것은 옳지 않다. (마찬가지로 enteredEmail의 상태 업데이트가 제시간에 처리되지 않을 수 있기 때문에)
이렇게 여러 가지 관련된 state가 결합된 경우라면 useReducer를 고려할 수 있다.
useReducer의 사용
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
- state: 최신 state 스냅샷
- dispatchFn: state 스냅샷을 업데이트할 수 있게 해주는 함수, 새로운 state 값을 설정하는 대신 액션을 디스패치한다.
- reducerFn(리듀서 함수): 최신 state 스냅샷을 자동으로 가져오는 함수이다. 액션이 디스패치 될 때마다 리듀서 함수를 호출한다.
(prevState, action) => newState
- initialState: 초기 state 값
-initFn: 초기 state를 설정하기 위해 실행해야 하는 함수
위에 예에서 값과 유효성을 하나의 state로 결합하여 useReducer로 관리해보자
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: '',
isValid: undefined,
});
const emailChangeHandler = (event) => {
dispatchEmail({ type: 'USER_INPUT', val: event.target.value });
setFormIsValid(event.target.value && enteredPassword.trim().length > 6);
};
const validateEmailHandler = () => {
dispatchEmail({ type: 'INPUT_BLUR' });
};
값을 업데이트하고 싶다면 액션을 디스패치해야 한다. (dispatchEmail 호출)
디스패치 함수에 액션을 전달하면 되는데, 액션은 마음대로 정할 수 있지만 보통은 객체로 하고, type필드를 가진다.
사용자가 입력한 내용을 저장하므로 페이로드를 추가할 수 있다.(val)
이렇게 액션을 디스패치하면 리듀서 함수가 호출된다.
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('@') };
}
return { value: '', isValid: false };
};
컴포넌트 함수 내부에서 만들어진 어떤 데이터도 필요하지 않기 때문에 리듀서 함수인 emailReducer는 컴포넌트 함수 바깥에 만들 수 있다.
리듀서 함수는 action의 type을 확인하고 새로운 state 스냅샷을 반환한다.
useEffect 최적화, 이펙트 함수가 불필요하게 실행되는 것 피하기
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(emailState.isValid && passwordState.isValid);
}, 500);
return () => {
clearTimeout(identifier);
};
}, [emailState, passwordState]);
이 코드의 문제점은 emailState나 passwordState가 변경될 때마다 이펙트함수가 너무 자주 실행되는 것이다.
따라서 유효성이 변경될 때만 이펙트 함수가 실행되도록 변경할 것이다. (값이 변할 때도 유효성을 변경해주기 때문에 유효성이 true일 때 문자를 더 입력해도 이펙트 함수가 실행되지 않도록 하는 것이다.)
다음과 같이 객체 destructuring을 해서 각 state의 isValid 속성을 추출할 수 있다.
const { isValid: emailIsValid } = emailState.isValid;
const { isValid: passwordIsValid } = passwordState.isValid;
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(emailIsValid && passwordIsValid);
}, 500);
return () => {
clearTimeout(identifier);
};
}, [emailIsValid, passwordIsValid]);
이제 의존성 배열에 emailValid, passwordIsValid값을 전달하면 유효한 값일 때 문자를 더 입력하더라도 이펙트 함수가 실행되지 않아 useEffect를 더욱 최적화할 수 있다.