useState 그리고 useReducer


맨 처음 hooks를 공부하면서 접하게 된 함수는 useState다. useState를 처음 사용하면서 든 생각은 너무나 편리하다는 것이었고, 이것으로 모든 문제가 해결될 거라 생각했다. 하지만 앱의 규모가 커지면서 여러 파일에서 공유해야 할 global state의 필요성이 생겼고, 이를 해결하기 위해 useReducer을 사용하게 되었다. 이 글에서는 useState는 무엇인지, useReducer은 무엇인지 알아보고 언제 무엇을 어떻게 쓸 것인지에 대해 알아보고자 한다.

useState

먼저 리액트 공식문서에서 말하고 있는 useState에 대해 살펴보자.

const [state, setState] = useState(initialState);
  • 상태 유지 값과 그 값을 갱신하는 함수를 반환한다
  • 최초로 렌더링을 하는 동안, 반환된 state는 첫 번째 전달된 인자인 initialState의 값과 같다
  • setState함수는 state를 갱신할 때 사용한다

아래의 카운터 예제를 보자.

function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}

initialCount값을 받아오고 이 값은 count의 최초 값과 같다. -버튼을 클릭하면 최신 count값에서 -1을 한 값으로 state가 업데이트된다. +버튼을 클릭하면 최신 count값에서 +1을 한 값으로 state가 업데이트된다.

사실 useState는 hooks에서 가장 처음 소개되는 함수인 만큼 자주 사용되고, 그만큼 사용하기 쉽다. 하지만 이러한 useState를 사용할 때에도 주의할 점이 있다. 바로 setState는 비동기로 작동한다는 점이다.

리액트 문서를 살펴보면 이런 말이 있다.

this.propsthis.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안 됩니다.

그러니 state값을 확인하고 싶을 때는 useEffect를 사용해 의존성 배열에 state 값을 넣은 후 확인하도록 하자!

useReducer

리액트 공식문서에서는 useReducer에 대해서 어떻게 말하고 있는지 알아보자.

  • useState의 대체 함수이다
  • (state, action) => newState의 형태로 reducer를 받고 dispatch 메서드와 짝의 형태로 state를 반환한다

useState에서 살펴본 카운터 예제를 useReducer를 사용해 변경한 아래의 코드를 살펴보자.

import React, { useReducer } from 'react';

const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return { ...state, count: state.count + 1 };
case 'DECREASE':
return { ...state, count: state.count - 1 };
default:
throw new Error();
}
};

const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });

const handleIncrease = () => {
dispatch({ type: 'INCREASE' });
};

const handleDecrease = () => {
dispatch({ type: 'DECREASE' });
};

return (
<div>
<h1>Counter with useReducer</h1>
<p>Count: {state.count}</p>

<div>
<button type="button" onClick={handleIncrease}>
+
</button>
<button type="button" onClick={handleDecrease}>
-
</button>
</div>
</div>
);
};

export default Counter;

우선 counterReducer이라는 이름의 리듀서를 만들어준다. stateaction 인자로 가지고 있고, 액션 타입의 케이스에 따라 state를 업데이트해 준다.

...stateES6의 Destructuring assignment 이다. 매우 자주 쓰게 될 것이므로 알아두자.

컴포넌트 안에서 useReducer을 사용해 위에 정의해둔 counterReducer 리듀서와 초기 값을 받아온다. dispatch의 첫 번째 인자는 액션의 type이고, 두 번째 인자는 payload다. 액션의 타입을 체크한 후 state object를 업데이트하고, 업데이트된 state를 리턴하고 있다.

useState vs. useReducer

useStateuseReducer에 대해 어느 정도 알아봤다. 그러면 이러한 궁금증이 들 수 있다. 둘 다 state를 업데이트하는 건데, 언제 useState를 쓰고 언제 useReducer을 쓰면 되는 거지? 내가 일을 하면서 깨달은 한 가지 사실은, 이런 질문을 미리 걱정하지 말고 이런 질문이 자연스럽게 들 때가 오기를 기다리라는 것이다! 생각보다 그 순간은 빨리 찾아올 것이니..

리액트 문서에서는 아래와 같이 말하고 있다.

다수의 하윗값을 포함하는 복잡한 정적 로직을 만드는 경우나 다음 state가 이전 state에 의존적인 경우에 보통 useState보다 useReducer를 선호합니다.

이번에는 useReducer vs useState in React - RWieruch 블로그에서 말하고 있는 useStateuseReducer의 사용에 대해 살펴보자.

이럴때 useState를 사용해라!

  • state가 원시타입인 경우
  • 간단한 state 업데이트가 필요한 경우
  • 컴포넌트 안에 비즈니스 로직이 있는 경우 (나의 경우에는 뷰 안에 비즈니스 로직이 있는 것을 좋아하지 않는다)
  • 규모가 작은 앱 (구분이 애매모호하긴 하지만)

이럴때 useReducer를 사용해라!

  • state가 객체인 경우
  • 복잡한 state 업데이트가 필요한 경우
  • 복잡한 비즈니스 로직은 reducer을 사용하는게 좋다
  • 컴포넌트 트리 (여러개의 컴포넌트) 안에서 업데이트 되어야하는 state
  • 유지 보수의 용이함을 위해

마무리

이번 글에서는 useStateuseReducer에 대해서 알아보았다. 나는 useReducerContext를 함께 사용해 스토어를 따로 만들어 사용하고 있는데 이 방법에 대해서도 나중에 다뤄보고 싶다. 나는 사실 객체지향 프로그래밍 경험은 없는 데다 함수형 프로그래밍 경험도 이번이 처음이다. 하지만 사용하면 할수록 함수형 프로그래밍의 편리함을 느끼고 있다. 가장 좋은 점은 잘 정리된 문서가 항상 내 뒤에 든든하게 있다는 것 아닐까!