React

[React] useReducer

ssohyunn 2023. 9. 14. 20:07

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를 더욱 최적화할 수 있다.