[React Reducer] 더 복잡한 state 관리하기

summereuna🐥·2023년 4월 21일
0

React JS

목록 보기
45/69

📝 Reducer란?


리액트 내장 훅인 useReducer()useState()와 마찬가지로 state 관리를 도와준다. 하지만 더 강력한 state 관리를 도와주는 훅인 useReducer()useState()에 비해 더 많은 기능을 가지고 있기 때문에 더 복잡한 state(more complex state)를 다룰 때 유용하다.

복잡한 state의 예

  • 많은 state를 가지는 경우(have multiple states),
    • state들이 함께 속해 있는 경우(belong together)
    • 여러 state가 함께 바뀌거나 서로 관련된 경우(change together or are related)

복잡한 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의 스냅샷, enteredEmailenteredPassword에 의존하고 있기 때문이다. 이 두가지 state는 formIsValid의 최근 state 스냅샷이 아니다.

  • 이런식으로 작성하면 안된다. 왜냐하면 리액트가 state 업데이트를 스케줄링하는 방식 때문에 비밀번호 state(enteredPassword)가 업데이트 처리되기도 전에, 즉, 최근 스냅샷으로 업데이트 되기도 전에 아래 setFormIsValid() 코드가 먼저 실행되어 버릴 수도 있다. 그러면 오류가 생길 수 있다.

setFormIsValid(
    event.target.value.includes("@") && enteredPassword.trim().length > 6
  );
  • 이전 state 스냅샷에 의존하여 state를 업데이트하는 함수폼을 괜히 사용하는게 아니다. 이는 가장 최신의 스냅샷을 사용할 수 있는 가장 안정적인 방식이다. 🥲

  • 하지만 여기서는 사용할 수가 없어.. 다른 두 state에 의존하고 있기 때문에..
    이런 경우 useState() 대신 useReducer()를 사용하면 된다.

📝 useState() useReducer()를 사용하면 좋은 경우

  • 위의 상황처럼 다른 state 값에 의존하여 state를 업데이트 해야 하는 경우

    • setFormIsValid()는 다른 두 state 값인 enteredEmail, enteredPassword에 의존하여 state를 업데이트하고 있는데, 이는 함수 폼 사용하여 state 업데이트해야 한다는 규칙을 어기는 일이다.
      하지만 이렇게 하지 않으면 방법이 없음! 이럴 땐 useReducer()를 사용하면 된다.
  • 또는 함께 속하는 state들이 있는 경우,

    • 입력된 값enteredEmail이 있고, 그 값의 유효성을 검사하는 emailIsValid이 있음

다른 state를 기반으로 state를 업데이트하는 경우, 하나의 state로 병합하는 것도 방법이다.
예를 들어 enteredEmail, isEmailValid 두 state를 하나로 만들어서 관리할 수도 있다.

{
  email: 
    enteredEmail,
    isEmailValid,
}

이런식으로 하나의 객체로 만들어 계속 useState를 사용할 수도 있다.

하지만 state가 더 복잡해지고 커지면 여러 가지 관련된 state가 결합된 경우라면 useReducer도 고려해 보자~


✅ useReducer()

const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);

📝 반환하는 두 값

useState()처럼 useReducer()도 항상 두 개의 값이 있는 배열을 반환한다
따라서 배열 디스트럭처링을 사용하여 값을 추출하여 별도의 상수에 저장할 수 있다.

  1. state
    컴포넌트 재랜더링/재평가 싸이클에 사용되는 최신 state 스냅샷

  2. dispatchFn
    새로운 액션을 디스패치하는 함수로, 그 액션은 useReducer()의 첫 번째 인수인 리듀서 함수가 소비한다.

📝 세 가지 인자

  1. reducerFn(리듀서 함수)
const reducer = (prevState, action) => {
  ...
};

새 액션이 디스패치될 때 리듀서 함수가 호출되는데, 리듀서 함수는 리액트가 관리하는 최신 state 스냅샷(새로 업데이트된 state)을 자동으로 반환하는 함수이다.

  • A function that is triggered automatically once an action is dispatched (via dispatchFn()) - it receives tha latest state snapshot and should retrun the new, updated state.
  • useState 훅의 함수 폼과 비슷한데, 액션이 있기 때문에 좀 더 확장된 버전이라고 할 수 있다.
  1. initialState(초기 state)

  2. initFn(초기 함수)
    초기 state가 복잡한 경우 초기 state를 설정하기 위해 실행해야 하는 함수를 설정할 수 있다.

  • 예) http 리퀘스트 결과 등..

✅ useReducer() 사용하기

  • 입력값, 유효성 결합하기 위해 사용

    • enteredEmail, isEmailValid 결합
    • enteredPassword, isPasswordValid 결합
  • 전체 form state 관리하는데 사용
    전부 들어 있는 큰 form state 하나로 관리, 또는 작은 state 여러개로 관리

여기서는 입력값, 유효성을 결합하여 관리해보자.

1. useReducer 임포트하기

import React, { useReducer } from "react";

2. 컴포넌트 외부에서 리듀서 함수 작성

리듀서 함수는 컴포넌트 함수 밖에서 명명함수로 작성한다.
컴포넌트 함수 내부에서 만들어진 어떤 데이터도 리듀서 함수 내부에서 필요하지 않기 때문에, 리듀서 함수는 컴포넌트 함수 밖에서 만들어서 사용할 수 있다.

리듀서 함수는 새로운 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) => {
  //...

3. useReducer 사용하여 state 생성

  • 입력된 이메일과 이메일 유효성을 판단하는 값을 가진 state 생성
  • 입력된 비밀번호와 비밀번호 유효성을 판단하는 값을 가진 state 생성
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,
  });

4. 디스패치 함수 사용하여 사용자가 이메일/비밀번호를 입력 시 액션을 리듀서 함수에 보내기

dispatchFn({ type: "대문자", payload })

디스패치되는 액션은 본인 마음대로 정하면 된다. 문자열 식별자일 수도 있고 숫자일 수도 있다. 하지만 보통 객체{ }를 주로 쓴다.

📝 액션어떤 식별자를 가지고 있는 어떤 필드를 가진 객체{ filed: identifier }이다.

  • dispatchFn({ type: "대문자", payload })
    • 필드는 대부분 명명된 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 ( ...

5. Login 컴포넌트 리턴되는 JSX에 useReducer로 생성한 state값 넣기

//...
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;

➕ useEffect()로 코드 최적화 하기

  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()를 사용하자.

  • 다른 state의 값에 의존하고 있는 setFormIsValid()를 사이드 이펙트 안에서 호출하는 것은 좋은 방법이다. state 스냅샷을 참조하면서도, state가 변경될 때마다 이 이펙트는 최신 state 값으로 확실히 다시 실행된다.
  • 따라서 useEffect 안에서 다른 state를 기준으로 state를 업데이트하는 것이 낫다.
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);
  };

    //...

➕ 불필요하게 실행되는 useEffect 더 최적화하기

콘솔을 보면 유효성이 이미 검증되고 나서도 비번에 문자를 추가할 경우 이펙트가 실행되고 있다.

디펜던시에 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에 종속성으로 추가하기

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 vs useReducer


useReducer는 언제 사용하는 것이 좋을까?

useState를 사용하면 번거로운 경우 useReducer를 사용한다.

  • 너무 많은 일을 처리해야 하는 경우
  • 관련 state 스냅샷들이 서로 독립적이고 같이 업데이트가 잘 안되는 경우

useState는 주요 state 관리 도구로 보통 useState로 관리를 시작한다.
대부분의 경우에는 useState만 있으면 된다.
useReducer를 쓰는 것이 좋아보이는 경우에도 특히 useEffect와 함께 useState를 사용하여 처리할 수 있다.
하지만 useReducer를 사용하면 더 우아하고 간단하다.
절대적으로 항상 useReducer를 사용해야 하는 것은 아니다.
예) 두 개의 서로 다른 값을 전환하기만 하는 단순한 state의 경우 useReducer는 너무 과하다.

useState

  • 주요 state 관리 도구
  • 개별적인(independent) state/data 다루기에 적합
  • state 업데이트 쉽고, 몇 안되는 업데이트에 적합, state가 변경되는 경우가 다양하지 않은 경우, 특히 state로서 객체 같은게 없는 경우 사용하기 적합

useReducer

  • 더 강력한 state 관리 도구, 복잡한 state 업데이트 로직 포함하는 리듀서 함수 사용 가능
  • 연관(related)된 state/data 다루는 경우 고려(예. form input state)
  • 복잡한 state 업데이트가 많은 경우, state 하나를 변경하는 여러 다른 액션이 있는 경우 적합

일단 이렇게 알고 넘어갑시다요~

profile
Always have hope🍀 & constant passion🔥

0개의 댓글