본문 바로가기
React

React - useState vs useReducer and Custom Hook

by 강깅꽁 2020. 9. 10.

 

함수형 컴포넌트에서 상태를 관리하기 위해 보통 useState를 사용하게 됩니다. 

 

useState를 사용하여 만든 Counter Component입니다. 

하지만 여기서 관련 state가 추가되고 함수가 많아진다면 관리하기 까다로워질 것입니다. 

import React, { useState, useCallback } from "react";

export default function Counter() {
  const [num, setNum] = useState(0);
  const onIncrease = useCallback(() => {
    setNum((prevNum) => prevNum + 1);
  }, []);
  const onDecrease = useCallback(() => {
    setNum((prevNum) => prevNum - 1);
  }, []);
  return (
    <div>
      <p>{num}</p>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

 

그럴 경우에 useReducer를 고려해 볼 수 있습니다. 

useReducer Hook은 첫 번째 파라미터로 reducer를 받고 두 번째 파라미터로 초기 값을 받습니다.

리턴 값으로 상태 값과 dispatch 함수를 반환합니다. 

dispatch 함수에 액션 객체를 넣어주면 reducer가 실행되며 reducer는 상태값과 액션 객체를 파라미터로 받아서 새로운 상태를 return 해주게 됩니다. 

여기서 상태는 항상 불변성을 지켜주어야 합니다.(react의 상태는 항상 불변성이 유지되어야 합니다.) 

Tip. reducer와 상태 초기 값이 컴포넌트 밖에서 선언된 것을 확인할 수 있습니다. 

import React, { useReducer } from "react";

const initialState = { count: 0 };
function reducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { count: state.count + 1 };
    case "DECREASE":
      return { count: state.count - 1 };
    default:
      throw new Error("error");
  }
}

export default function Counter() {
  const [num, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <p>{num.count}</p>
      <button onClick={() => dispatch({ type: "INCREASE" })}>+1</button>
      <button onClick={() => dispatch({ type: "DECREASE" })}>-1</button>
    </div>
  );
}

 

마지막으로, 다른 컴포넌트에서도 범용적으로 사용하도록 만들고 싶다면 useState 또는 useReducer와 관련된 상태와 함수들을 Custom Hook으로 만들 수 있습니다.

이 경우에는 컴포넌트 구조가 간단해지게 됩니다. 

 

먼저 useState를 이용하여 Custom Hook을 만들면 다음과 같습니다.

 

useCounter.js의 모습입니다.

import { useState, useCallback } from "react";

export default function useCounter(initialState) {
  const [state, setState] = useState(initialState);
  const onIncrease = useCallback(() => {
    setState((prevNum) => prevNum + 1);
  }, []);
  const onDecrease = useCallback(() => {
    setState((prevNum) => prevNum - 1);
  }, []);

  return [state, onIncrease, onDecrease];
}

 Counter.js의 모습인데 훨씬 깔끔해졌습니다. 이로 인해 useCounter Hook은 다른 컴포넌트에서도 재사용 할 수 있으며 심지어 같은 컴포넌트에서도 여러 번 사용할 수 있습니다. 

그 이유는 각각의 Hook 호출은 완전히 독립된 state를 가지게 됩니다. 

import React from "react";
import useCounter from "./useCounter";

export default function Counter() {
  const [num, onIncrease, onDecrease] = useCounter(0);
  return (
    <div>
      <p>{num}</p>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

 

이제 useReducer를 활용해 Custom Hook을 만들어 보겠습니다.

 

useCounter.js를 다음과 같이 변경하였습니다. 

import {useCallback, useReducer } from "react";

function reducer(state, action) {
  switch (action.type) {
    case "INCREASE":
      return { count: state.count + 1 };
    case "DECREASE":
      return { count: state.count - 1 };
    default:
      throw new Error("error");
  }
}

export default function useCounter(initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const onIncrease = useCallback(() => {
    dispatch({ type: "INCREASE" });
  }, []);
  const onDecrease = useCallback(() => {
    dispatch({ type: "DECREASE" });
  }, []);

  return [state, onIncrease, onDecrease];
}

 

Counter.js 컴포넌트는 다음과 같습니다.

import React from "react";
import useCounter from "./useCounter";

export default function Counter() {
  const [num, onIncrease, onDecrease] = useCounter({ count: 0 });
  return (
    <div>
      <p>{num.count}</p>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
}

 

참고: ko.reactjs.org/docs/hooks-overview.html#building-your-own-hooks

 

여기까지 어떤 경우에 useState 또는 useReducer를 사용하고 또 범용적으로 사용하고 싶은 경우 Custom Hook을 만드는 것 까지 알아봤습니다.