[React] useReducer Hook

Shin Hyun Woo·2022년 12월 16일
0
post-thumbnail

useReducer는 단일 상태를 관리하는 useState보다 더 복잡한 state를 관리할 때 유용하게 사용할 수 있는 React Hook이다.

useState와 useReducer의 비교

간단한 카운트 앱으로 useStateuseReducer의 차이를 알아보자.

useState의 카운트 앱

import { useState } from "react";

const App = () => {
  const [age, setAge] = useState(42);
  
  // 이전의 state 값을 기반하여 증가/감소
  const plusAge = () => {
    setState(prevState => prevState + 1)
  };

  const minusAge = () => {
	setState(prevState => prevState - 1)
  };

  return (
    <>
      <h2>{age}</h2>
      <button onClick={minusAge}>한살 빼기</button>
      <button onClick={plusAge}>한살 더하기</button>
    </>
  );
};

export default App;

이렇게 useState를 이용하여 카운트 앱을 만들었다.
여기서 useState의 두번째 set 함수에 인자로 콜백 함수를 사용하여 현재 state의 값(화면에 출력된 값)을 기반하여 1씩 증가/감소하게 구현하였다.

useReducer의 카운트 앱

그럼 이제 useReducer를 이용한 카운트 앱을 보자.

import { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case "PLUS_AGE":
      return { ...state, age: state.age + 1 };
    case "MINUS_AGE":
      return { ...state, age: state.age - 1 };
    default:
      return state;
  }
};

const App = () => {
  const [age, dispatch] = useReducer(reducer, { age: 42 });
  
  const plusAge = () => {
    dispatch({ type: "PLUS_AGE" });
  };

  const minusAge = () => {
    dispatch({ type: "MINUS_AGE" });
  };

  return (
    <>
      <h2>{age.age}</h2>
      <button onClick={minusAge}>한살 빼기</button>
      <button onClick={plusAge}>한살 더하기</button>
    </>
  );
};

export default App;

모두 아래 결과처럼 동일한 기능을 한다.

근데 useState의 카운트 앱보다 훨씬 복잡해보인다.

하지만 useReducer는 이런 간단한 앱보단 다수의 복합적인 state를 다루는 앱(예를 들면, 회원가입 폼 등)에 더 효과적으로 사용될 수 있다.
나는 useReducer를 개인적으로 사용해보고 정리를 하기 위해 간단한 카운트 앱을 했지만 복잡한 상태 관리가 필요하다면 직접 사용해보길 바란다.

useReducer 단계별로 살펴보기

useReducer 호출

우선 하나씩 보도록 하자.
Hook을 사용하기 위해 import가 필요하다.

import { useReducer } from "react";

import 후 사용하기 위해 아래처럼 작성한다.

const [state, dispatchFunc] = useReducer(ReducerFunc, initialState);

여기서 하나씩 살펴보면,
useReduceruseState처럼 반환값이 배열인데 첫번째 요소가 state, 두번째 요소가 dispatchFunc이다.

이것은 위의 useReducer의 카운트 앱에 사용된 예시이다.

const [age, dispatch] = useReducer(reducer, { age: 42 });

state

여기서 state는 말 그대로 useState와 동일한 역할의 현재의 state 변수이다.
useReducer의 두번째 인자인 initialState가 최초로 할당된다.

dispatchFunc

dispatchFunc는 '상호작용에 따라 변경할 수 있는 디스패치 함수'이다.
쉽게 말하자면 화면에 있는 state를 업데이트하기 위한 함수이다.

중요한 점은 dispatchFunc가 호출이 되면 state를 다른 값으로 업데이트하고 사용된 컴포넌트의 렌더링을 발생시킨다.

useReducer(ReducerFunc, initialState)

ReducerFunc

useReducer의 첫번째 인자 ReducerFunc는 "dispatchFunc가 전달한 action 객체
type 프로퍼티에 따라 state를 어떻게 업데이트할지 결정하는 함수이다.

type 프로퍼티가 아닌 다른 것으로도 업데이트 작업을 결정할 수 있다.
단, 보편적으로 어떻게 업데이트를 할지 식별하기 위해 type 프로퍼티를 포함한 action 객체를 사용한다.

initialState

useReducer의 두번째 인자 initialState는 반환하는 배열의 첫번째 요소인 state의 초기값을 설정하는 인자이다.
물론 useState처럼 해당 컴포넌트가 처음 렌더링되었을 때 최초로 state에 할당되며 이후의 렌더링에는 무시되는 값이다.

dispatchFunc 호출

카운트 앱의 dispatchFunc를 보도록 하자.

// age의 증가를 위한 함수
const plusAge = () => {
    dispatch({ type: "PLUS_AGE" });
};

// age의 감소를 위한 함수
const minusAge = () => {
    dispatch({ type: "MINUS_AGE" });
};

dispatchFunc를 보면 뭔가 당황스러울 것이다.
왜냐하면 dispatchFuncstate를 업데이트하는 함수인데 왜 useState처럼 증가/감소하는 로직이 없기 때문이다. 또한 반환값도 없다.

useReduceruseState와 달리 복잡한 state를 관리하기 때문에 인자로 ReducerFunc에 전달되는 action 객체의 type 프로퍼티에 따라 state 업데이트 동작(action)을 결정하게 된다.

쉽게 말하자면 dispatchFunc는 그저 state를 어떻게 업데이트할 것인지 결정하기 위해 조건ReducerFunc에 전달한다고 생각하면 된다.

그래서 위처럼 { type: "PLUS_AGE" }{ type: "PLUS_AGE" }action 객체를 ReducerFunc에 전달한다.

여기서 type의 값이 모두 대문자인 것을 볼 수 있는데 이것은 관례라고 하니 만약 다른 컨벤션을 원한다면 변경해도 된다.

React Docs Beta 공식 문서에는 소문자로 표기되어 있다.

ReducerFunc 실행(state 업데이트)

// ReducerFunc
const reducer = (state, action) => {
  switch (action.type) {
    case "PLUS_AGE":
      return { ...state, age: state.age + 1 };
    case "MINUS_AGE":
      return { ...state, age: state.age - 1 };
    default:
      return state;
  }
};

const App = () => {
  // ...
  
  const plusAge = () => {
    dispatch({ type: "PLUS_AGE" });
  };
  
  const minusAge = () => {
    dispatch({ type: "MINUS_AGE" });
  };

  // ...
}

ReducerFunc는 대체적으로 컴포넌트 외부에 위치한다. 왜냐하면 컴포넌트가 재렌더링이 되면서 새로 생성될 필요가 없기 때문이다.

이제 dispatchFunc 호출로 인해 type 프로퍼티가 있는 action 객체를 전달 받았다고 가정해보자.

예를 들어 plusAge 함수가 버튼 클릭으로 호출이 되어 dispatchFunc가 호출되어 action 객체가 ReducerFunc에 전달되었다.

그럼 ReducerFunc의 내부의 switch문에 의해 해당되는 action.type에 따라 state의 업데이트 방식이 결정된다.

여기서 궁금한 점이 있다면 ReducerFunc의 매개변수 stateaction일 것이다.

  • state화면에 있는 기존의 값을 가리키며
  • actiondispatchFunc로 부터 전달받은 action 객체이다.

이것을 로그로 출력해보면 아래와 같다.

// plusAge 함수를 클릭 이벤트로 호출했을 때

const reducer = (state, action) => {
  console.log(state, action) // { age : 42 }, { type : "PLUS_AGE" }
  switch (action.type) {
    case "PLUS_AGE":
      return { ...state, age: state.age + 1 };
    case "MINUS_AGE":
      return { ...state, age: state.age - 1 };
    default:
      return state;
  }
};

처음 클릭 이벤트로 인해 호출되었기 때문에 첫번째 매개변수 state에는 useReducer 호출 단계에서 설정했던 initialState가 출력되었다.

											    👇
const [age, dispatch] = useReducer(reducer, { age: 42 });

그리고 두번째 매개변수 actiondispatchFunc로부터 전달받은 action 객체가 출력되었다.

const plusAge = () => {
    dispatch({ type: "PLUS_AGE" });
  			          👆
};

여기선 action 객체의 type"PLUS_AGE"이기 때문에 switch문의 첫번째 case의 반환값이 새로운 state 값으로 업데이트된다.

스프레드 연산자(...)를 사용해서 기존의 state을 전개한 이유는? 🤔

이 카운트 앱에는 굳이 필요가 없어보인다. 왜냐하면 기존에 initialState로 설정한 값의 프로퍼티는 age만 존재하기 때문에 굳이 스프레드 연산자를 사용할 필요없이 { age : state.age + 1 }만 해주면 된다.

다만, 만약 age 말고도 다른 프로퍼티가 존재할 때 그냥 { age : state.age + 1 }만 작성한다면 "age와 다른 프로퍼티를 포함한 객체""age 프로퍼티만 가진 객체"로 완전히 교체해버리는 문제가 생긴다.

그래서 이를 방지하기 위해 기존의 state를 전개하고 이후에 업데이트할 값을 작성해서 기존의 state에 오버라이드 해주는 것이다.

기존의 state의 age 값에 1을 더하는 것을 알 수 있다.

useReducer 주의할 점

  • useReducer 호출은 반드시 사용할 컴포넌트의 최상위 레벨에서 해야한다. 절대 루프나 조건문에서 호출해서는 안된다.
  • 객체(state, action)의 불변성을 유지해야한다.
// React의 불변성을 위해 아래처럼 직접 업데이트해서는 안된다.
case "PLUS_AGE":
  return state.age++;
case "MINUS_AGE":
  return state.age--;
profile
untiring_dev - Notion 페이지는 정리 중!

2개의 댓글

comment-user-thumbnail
2023년 1월 24일

취업하셨는지 궁금합니다.

1개의 답글