[React] useReducer hook

SuamKang·2023년 7월 9일
0

React

목록 보기
18/34
post-thumbnail

리액트에서 상태를 관리하기 위해 제공되는 hook은 보통 가장 일반적으로 useState 훅을 사용하여 관리하게 된다.

여기서 그럼 useReducer 훅은 useState 훅과 어떤점이 비슷하고 차이가 있는지 또 어떤 상황에서 각각 맞춰 사용하는편이 좋을것인가?



useState vs useReducer


useState hook과 useReducer hook은 리액트에서 상태관리를 위해 사용되는 두가지 훅이다.


공통점

  1. 리액트 함수 컴포넌트에서 상태를 관리하기 위해 사용된다.
  2. 컴포넌트의 상태를 업데이트하고 리렌더링하는 기능을 제공한다.

차이점

  1. useState는 단순한 상태 값만을 관리하는 반면에, useReducer는 좀 더 복잡한 상태 관리를 할 수 있도록 도움을 준다.
    useReducer는 상태 값 뿐만 아니라 액션과 리듀서 함수를 같이 사용하여 상태를 업데이트 해준다.

  2. useState는 단순히 새로운 상태 값을 설정하는 방식(갱신함수적용)으로 동작하지만, useReducer는 현재 상태(state)와 액션(action)을 받아 새로운 상태를 반환하는 리듀서(reducer) 함수를 통해 업데이트 된다.

  3. useState는 개별적인 상태 값을 관리하며, 여러 useState 훅을 사용할 수 있다. 반면에 useReducer는 여러 상태값을 하나의 리듀서 함수로 관리 할수 있기때문에 관련된 상태 값들을 그룹화하고 관리하기 유용하다.

활용


useState

단순한 상태 값의 변경이 필요한 경우 사용한다.
해당 컴포넌트의 관리되는 상태갯수가 적고 간단한 업데이트 로직을 가지고 있는 경우에 적합하다.
예) 토글버튼의 상태관리, 입력 필드값 관리 등

useReducer

복잡한 상태관리나 상태 간의 의존성이 있는 경우 사용한다.
상태 값이 여러개이고, 상태를 업데이트하는 로직이 복잡하거나 예측하기 어려운 경우에 적합하다.
예) 상태가 여러개의 속성으로 구성되어있고 복잡한 액션과 리듀서 로직이 필요한경우, 상태들을 그룹화하여 관리하고 싶을 경우


결론적으로, useState는 간단한 상태 관리에 사용되며 useReducer는 좀 더 복잡한 상태 관리에 사용된다. 어떤 훅을 선택할지는 상태 관리의 복잡성유지보수성을 고려하여 결정해야 한다.



코드예시


그럼 기존의 useState로 상태를 관리했던부분을 어떻게 useRedcuer로 대체했는지 코드로 살펴보자.

Before

import { useState, useEffect, useReducer } from "react";

import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";


const Login = (props) => {
  const [enteredEmail, setEnteredEmail] = useState("");
  const [emailIsValid, setEmailIsValid] = useState();

  const [enteredPassword, setEnteredPassword] = useState("");
  const [passwordIsValid, setPasswordIsValid] = useState();

  const [formIsValid, setFormIsValid] = useState(false);

  useEffect(() => {
    // 사용자 입력 디바운싱(그룹화)하여 매번 입력이 진행될때 검사를 하는게 아닌 일정 시간이 지나서 사용자입력이 끝났다 생각된다면 그때 한번 이 로직을 실행하여 유효성검사를 하게 하는것이다.

    // 타이머를 저장한다
    const identifier = setTimeout(() => {
      console.log("useEffect안 사이드이펙트 로직 실행");
      setFormIsValid(
        enteredEmail.includes("@") && enteredPassword.trim().length > 6
      );
    }, 1000);

    // 새로 다시 타이머가 시작하기 전에 이전 타이머를 지워준다.(클린업 함수) -> 단, 첫 번째 사이드이펙트함수가 실행되기 전엔 실행 X
    return () => {
      console.log("이전 로직 초기화");
      clearTimeout(identifier);
    };

    // 따라서 결국 입력받아 사이드이펙트함수(타이머) 실행되고(마운트) 클린업함수로 이전 타이머 시작전 지워주게(언마운트) 되면 마지막 타이머만 완료될것이다.
  }, [enteredEmail, enteredPassword]);

  // 이메일 입력 함수
  const emailChangeHandler = (event) => {
    setEnteredEmail(event.target.value);

    setFormIsValid(
      event.target.value.includes("@") && enteredPassword.trim().length > 6
    );
  };

  // 비번 입력 함수
  const passwordChangeHandler = (event) => {
    setEnteredPassword(event.target.value);

    setFormIsValid(
      enteredEmail.includes("@") && event.target.value.trim().length > 6
    );
  };

  // 이메일 스타일 유효성 함수
  const validateEmailHandler = () => {
    setEmailIsValid(enteredEmail.includes("@"));
  };

  // 비번 스타일 유효성 함수
  const validatePasswordHandler = () => {
    setPasswordIsValid(enteredPassword.trim().length > 6);
  };

  // 새로운 폼 전달 함수
  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(enteredEmail, enteredPassword);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            emailIsValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={enteredEmail}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            passwordIsValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={enteredPassword}
            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;

위처럼 useState로 관리하게 될때 아무래도 이 컴포넌트가 사용자의 로그인 정보를 받는 부분은 하나의 행위로 볼 수 있기때문에 연관되는 상태들 끼리 그룹화 시키고 싶다.
또한, 입력을 받으면 입력 하나하나 렌더링이 이루어 지게 되는데

여기서 로그인 입력정보의 유효성 여부만 파악하여 상태를 전달하기 위해 입력값이 아닌 유효성 자체에대한 업데이트만 감지하여 의존성을 부여하게 된다면 입력이벤트가 발생해도 불필요한 리랜더링이 일어나지 않을 수 있다는걸 파악할 수 있다.

이러한 점을 착안해 useRedcuer를 이용해서 상태를 좀 더 확장하여 관리하게 되어 위에 지적한 부분을 꾀할 수 있었다.


useReducer 작동방식

const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
  • state : 가장 최신상태 스냅샷
  • dispatchFn : 새로운 state값으로 변경될 action을 받는 함수
  • reducerFn : 새로운 action이 dispatch될때 호출하는 함수(최신의 state를 가져옴)이고 type에 맞게 업데이트된 새로운 state를 반환해준다.
  • intialState : 초기 설정 state
  • initalFn : 초기 state가 복잡할 경우 적용(ex. http 요청에대한 응답)




After

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.payload, isValid: action.payload.includes("@") };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.includes("@") };
  }

  return { value: "", isValid: false };
};

// 비번 리듀서
const passwordReducer = (state, action) => {
  if (action.type === "USER_INPUT") {
    return { value: action.payload, isValid: action.payload.trim().length > 6 };
  }
  if (action.type === "INPUT_BLUR") {
    return { value: state.value, isValid: state.value.trim().lenth > 6 };
  }

  return { value: "", isValid: false };
};

const Login = (props) => {

  const [email, emailDispatch] = useReducer(emailReducer, {
    value: "",
    isValid: null,
  });
  const [password, passwordDispatch] = useReducer(passwordReducer, {
    value: "",
    isValid: null,
  });

  const [formIsValid, setFormIsValid] = useState(false);

  // 상태들 디스트럭처링 하여 useEffect를 더욱 최적화하고 이펙트가 불필요하게 실행되는 것을 피할 수 있게된다.
  const { isValid: isValidEmail } = email;
  const { isValid: isValidPassWord } = password;

  useEffect(() => {

    const identifier = setTimeout(() => {
      console.log("useEffect안 사이드이펙트 로직 실행");
      setFormIsValid(isValidEmail && isValidPassWord);
    }, 1000);

    return () => {
      console.log("이전 로직 초기화");
      clearTimeout(identifier);
    };
  }, [isValidEmail, isValidPassWord]);

  // 이메일 입력 함수
  const emailChangeHandler = (event) => {
    emailDispatch({ type: "USER_INPUT", payload: event.target.value });
  };

  // 비번 입력 함수
  const passwordChangeHandler = (event) => {
    passwordDispatch({ type: "USER_INPUT", payload: event.target.value });
  };

  // 이메일 스타일 유효성 함수
  const validateEmailHandler = () => {
    emailDispatch({ type: "INPUT_BLUR" });
  };

  // 비번 스타일 유효성 함수
  const validatePasswordHandler = () => {
    passwordDispatch({ type: "INPUT_BLUR" });
  };

  // 새로운 폼 전달 함수
  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(email.value, password.value);
  };

  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={`${classes.control} ${
            email.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={email.value}
            onChange={emailChangeHandler}
            onBlur={validateEmailHandler}
          />
        </div>
        <div
          className={`${classes.control} ${
            password.isValid === false ? classes.invalid : ""
          }`}
        >
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={password.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;

이렇게 useRedcuer로 대체하여 상태를 관리 해보았다.
확실히 화면이 리렌더링 되는 부분에서 useEffect안 이펙트함수가 의존성이 있는 값이 변할때만 작동하게 되니 불필요한 렌더링을 최소화 할 수 있게 되었다.

profile
마라토너같은 개발자가 되어보자

0개의 댓글