My Boundary As Much As I Experienced

React) useReducer란? 본문

FrontEnd/React

React) useReducer란?

Bumang 2023. 10. 23. 12:01

useReducer란?

useReducer는 상태를 관리하는 또 다른 방식이다. useState의 경쟁자?라고 보면 된다.

 

 

 

 

그렇다면 useReducer와 useState는 무엇이 다른가?

흔히 쓰는 useState는 '필요한 state만큼 생성해서 개별 관리'하였다.

그러나 useReducer는 여러 state가 존재해도 하나의 로직으로 다 관리한다. '중앙화된 state묶음'이라고 보면 된다.

이렇듯 기능적으로는 이점이 두드러지기보단 관리 방식의 차이가 발생한다.

 

 

 

 

중앙 관리해서 좋은 점은?

 

1. state가 너무 늘어나서 헷갈리는걸 방지한다.

state가 열 몇 개씩 쌓이면 이게 어떤 state인지 헷갈리기 시작하는데 이를 방지할 수 있다.

 

2. 다른 state의 이전 값을 참조해서 최신 값을 만드는 로직에서 유리하다.

state의 이전 값을 기반으로 state를 바꿔야 하는 로직을 짜야되는 경우가 종종 존재한다.

(input의 키보드 입력을 tracking한다던가, 카운터 로직을 만들어야 한다던가...)

 

이때 useState를 사용하면 (React의 비동기 처리 스케줄 때문에) 변경 요청이 씹히는 경우가 종종 발생한다. 바로 직전의 값을 참조해야되는데 좀 더 오래된 값을 참조하는 경우가 종종 있다. 

 

이 문제의 해결 방법으로 setState((prevState)=> prevState + 1)처럼 콜백함수를 입력해서 직전값을 보장하는 방법이 있으나,

setState2(!state1)처럼 state2의 값이 state1에 의존한다면 어떻게 할 것인가? 콜백함수로 직전값을 받아오는 방법은 유효하지 않다.

 

그러나, useReducer를 사용한다면 모든 state들을 중앙관리하기 때문에 이런 문제를 해결하기 쉽다. state를 관리하는 하나의 거대한 함수에서 명료하게 교통정리(?)를 해줄 수 있다.

자세한 사항은 코드 예시로 설명하겠다.

 

 

 

 

코드 예시

기존 useState 사용 시:

useState로 form을 만들면 state가 너무 많아진다.

각각의 handler에 로직이 많아진다.

// useState 사용 시 유저 로그인 state만드는 방법

// state를 여러 개 만들어야 한다.
const [id, setId] = useState("")
const [idIsValid, setIdIsValid] = useState(true)
const [password, setPassword] = useState("") // 비번 값
const [pwIsValid, setPwIsValid] = useState(true) // 비번 유효성 유무
const [pwIsFocused, setPwIsFocused] = useState(false) // 비번 포커스 유무
const [pwIs뭐시기저시기, pwIs뭐시기저시기] = useState(false) 
// 하여튼 관리해야될 상태가 늘어날 때마다 독립된 state를 만들어서 관리해야된다.
...

const idInputHandler = () => { // 각각의 handler에 각각의 분기들을 처리해줘야 된다.
   if (...) {
     setIdIsValid(true)
   }
   if (...) {
     setIdIsValid(false)
   }
}

const passwordInputHandler = () => { // 각각의 handler에 각각의 분기들을 처리해줘야 된다.
   ...
}

return <form> // 
  <label>ID</label>
  <input onChange={idInputHandler} className={pwIsValid ? } />
  ...
</form>

 

useReducer 사용 시:

useReducer는 Reducer함수라는 중앙관리기계(?) 같은 역할에 모든 값들을 때려넣는다고 보면 된다.

그러면 이리저리 조건문으로 처리해서 상태를 업데이트 해준다.

이 블럭 아래 보면 Login 컴포넌트 바깥에 Reducer가 있는걸 볼 수 있다. 즉, reducer는 컴포넌트와 독립되어도 별 상관없다.

리액트가 매개변수로 state와 action을 제공해준다.

// useReducer 사용 시
const emailReducer = (oldState, action) => { //oldState는 가장 최신의 이전 state값, action은 방금 받은 입력값
  // console.log("in emailReducer, oldState: ", oldState, "action", action);

  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: action.val.includes("@") }; // useReducer()안의 콜백함수의 리턴값이다. useReducer에 의해 처리되어서 [state,dispatcher]안의 state에 가게 된다.
  } else if (action.type === "INPUT_BLUR") {
    return { value: oldState.value, isValid: oldState.value.includes("@") };
  }

  return { value: "", isValid: null }; // state를 반환하는 함수
}; // reducer함수는 Login component안의 다른 데이터와 상호작용할 일이 없으니 밖에 만들어도 된다.

const passwordReducer = (oldState, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.val, isValid: null };
  } else if (action.type === "INPUT_BLUR") {
    return { value: oldState.value, isValid: oldState.value.trim().length > 6 };
  }

  return { value: oldState.value, isValid: oldState.value.includes("@") };
};



// Login 컴포넌트 바깥에 reducer 로직을 둬도 된다.
const Login = (props) => {
.
.
.

 

Login 컴포넌트 내부의 값들은 이렇다.

const [emailState, dispatchEmail] = useReducer(리듀서함수, 초기값(객체인 경우가 대부분))

주의해야할 점은 따로 handler가 없다는 점이다.

 

즉 각각의 handler에서 파편적으로 수행하던 로직들을 reducer함수에 때려박고,

핸들러는 단지 dispatcher를 이용해 reducer함수로 직통으로 쏴줄 뿐이다.

useState를 썼으면 각각 다 독립된 state였을 것들을 객체 속성으로써 관리해준다.

// dispatchEmail를 통해 각각의 handler를 만들지 않고 reducer함수로 쏴준다.
const [emailState, dispatchEmail] = useReducer(emailReducer, {
  value: "",
  isValid: null,
});

// dispatchPassword를 통해 각각의 handler를 만들지 않고 reducer함수로 쏴준다.
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
  value: "",
  isValid: null, // 관리할 값들을 객체 속성화해 보낸다.
  isFocus: false,
  is뭐시기...: true,
  ...
  // useState같은 경우엔 이것들이 다 state로 독립되어 있었을 것이다.
});

const { isValid: emailIsValid } = emailState; // reducer 객체에서 구조분해할당으로 필요한 속성만 꺼내서 쓸 수  있다.
const { isValid: passwordIsValid } = passwordState;

const emailChangeHandler = (event) => {
  dispatchEmail({ type: "USER_INPUT", val: event.target.value });
}; //dispatch함수는 reducer로 값을 보낸다.
//useState처럼 핸들러에서 바로 바꾸는게 아니라 reducer로 중앙화하여 분류하는 접근 방식
//가공도 useReducer 안에서 한다.

const passwordChangeHandler = (event) => {
  dispatchPassword({ type: "USER_INPUT", val: event.target.value });
};

const validateEmailHandler = () => {
  dispatchEmail({ type: "INPUT_BLUR" });
};

const validatePasswordHandler = () => {
  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;

 

예시에선 email 관련된 state들은 모두 emailReducer로,

password관련된 state는 모두 passwordReducer로 관리했는데,

사실 loginReducer 처럼 진짜 하나로 다 묶어서 관리할수도 있다.

 

객체화되어 값이 관리되기 때문에, 다른 state의 상태에 의존하는 state들도 스케줄링에서 뒤쳐지는 경우가 없다.

'FrontEnd > React' 카테고리의 다른 글

React Testing Library 활용법  (0) 2023.12.16
React) useContext 사용법  (0) 2023.10.23
React) useRef란?  (1) 2023.10.22
React) 리액트 포털(React Portal)  (0) 2023.10.22
React) JSX가 처리되는 과정  (1) 2023.10.22