Home

Blog

useReducer를 어떻게, 그리고 언제 사용할까?

2023.08.18
10

이 글은 주로 React 공식문서(react.dev)의 내용을 번역하여 정리한 내용을 바탕으로 하고 있습니다.

React를 사용하면서 항상 습관적으로 useState를 주로 사용해왔고, 리액트의 훅을 상황에 맞게 잘 사용하고 싶다는 생각과 함께 useReducer를 다시 공부하게 됐다. useReducer의 기본적인 문법과 어떻게 사용하는지, 그리고 언제 사용해야하는지에 대해 알아보자.

useReducer는 리액트의 내장 Hook 중 하나로, reducer를 사용하여 컴포넌트의 상태 로직을 관리하는 데 사용된다. 주로 useState보다 복잡한 상태 로직을 처리해야할 때 사용한다. (이에 대한 내용은 뒤에서 더 자세히)

const [state, dispatch] = useReducer(reducer, initialArg, init?)

사용할 때는 컴포넌트 최상단에서 호출하여 사용한다.

import { useReducer } from 'react';

function reducer(state, action) {
  // ...
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });
  // ...
  • reducer : 상태가 어떻게 업데이트할 지 명시한 리듀서 함수. 순수 함수여야하며, 인자로 상태와 액션을 받아야 하고, 다음 상태값을 반환해야 한다. 상태와 액션은 어떤 타입도 가능하다.
  • initialArg : 초기값. 어떤 타입의 값도 가능하다. 초기 상태를 계산하는 방법은 다음 init 인자에 따라 달라진다.
  • (선택사항) init : 초기값을 반환하는 초기화 함수. 초기화 함수가 전달되지 않은 경우, 초기 값은 initialArg 값으로 설정된다. 그렇지 않은 경우, 초기값은 init(initialArg)를 호출한 결과로 설정된다.

useReducer 는 두 값을 가지는 배열을 반환한다.

  1. 현재 상태 : 첫 번째 렌더 중 init(initialArg) 또는 initialArg로 설정된다.
  2. dispatch 함수 : 상태를 다른 값으로 업데이트하고 재렌더를 발생시키는 디스패치 함수

useReducer에 의해 반환되는 dispatch 함수는 상태를 다른 값으로 업데이트하고 재렌더를 발생시킨다. dispatch 함수의 유일한 인자로 액션을 전달한다.

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

function handleClick() {
  dispatch({ type: 'incremented_age' });
  // ...

React는 현재 상태 및 디스패치에 전달한 액션과 함께 제공한 reducer 함수를 호출한 결과로 다음 상태의 값을 결정한다.

  • action : 사용자에 의해 수행되는 액션. 어떤 값도 가능하다. 컨벤션에 의하면, 한 액션은 보통 그것을 구별할 수 있는 type 속성을 가지고 있는 object이고, 선택적으로 다른 속성을 추가적인 정보와 함께 사용할 수 있다.

반환하는 값이 없는 dispatch 함수를 반환한다.

리액트는 초기값을 한 번 저장하고 다음 렌더부터는 초기값을 무시한다.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, createInitialState(username));
  // ...

createInitialState(사용자 이름)의 결과가 초기 렌더에서만 사용되지만, 모든 렌더에서 해당 함수를 호출하고 있다. 큰 배열을 만들거나 비용이 많이 드는 계산을 수행하는 경우 낭비적일 수 있다.

이러한 경우, useReducer의 세 번째 인자로 Initializer 함수를 전달한다.

function createInitialState(username) {
  // ...
}

function TodoList({ username }) {
  const [state, dispatch] = useReducer(reducer, username, createInitialState);
  // ...

이렇게 하면 초기값을 계산하면서 createInitialState함수의 인자로 username이 사용되게 된다.

이렇게 함수를 직접 호출하여 초기값을 전달하는 것이 아니라, 초기값 함수와 그 함수의 인자로 사용될 값을 전달하므로써 초기화 이후 초기 상태가 다시 생성되지 않는다.

만약 초기 상태를 계산하는데 필요한 정보가 없다면 두 번째 인자로 username 대신 null을 전달하면 된다.

얼핏보면 리듀서가 useState보다 항상 나은 선택일 것처럼 보이지만, 꼭 그렇지만은 않다. 성능적인 문제보다는 상태 관리의 복잡성에 초점을 맞추어 선택하면 된다.

공식문서에서 설명하고 있는 두 Hook의 주요 차이점이다.

일반적인 경우에는 useReducer는 reducer 함수와 dispatch 액션을 함께 작성해야하기 때문에 useState가 작성해야 하는 코드의 양이 훨씬 적다. 하지만, 여러 이벤트 핸들러가 비슷한 방식으로 상태를 변경하고 있다면, 상태 변경 로직을 useReducer의 reducer 내부로 옮김으로써 코드를 줄이는 데 도움이 된다.

useState는 상태 변경이 매우 간단할 때 읽기 쉽다. 하지만 상태 변경 로직이 복잡해지면 가독성이 떨어진다. 이러한 경우 useReducer를 사용하면 상태 변경 로직을 reducer내부로 옮겨 이벤트 핸들러와 상태 변경 로직을 완전히 분리할 수 있다.

useState의 경우 상태 변경 도중 버그가 발생하면 어디서, 왜 잘못됐는지 알 수 어렵다. useReducer를 사용하면 콘솔 로그를 확인하거나 에러를 발생시켜 디버깅할 수 있다.

reducer는 컴포넌트의 의존하지 않는 순수 함수이기 때문에 개별적으로 테스트할 수 있다.

useState와 useReducer를 사용하는 것은 선호의 문제로, 사람마다 선호하는 것이 다르다.

그래서 언제 useReducer를 사용하는 것이 적절한가?

사실 이번에 useReducer를 다시 공부하면서 좀 더 명확하고 대단한(?) 논리적인 이유를 기대했지만, 내가 기대한 것만큼 useReducer를 사용하는 엄청난 이유는 없었다. (예를 들면, 성능적으로 엄청난 차이가 있다거나 상태 변경하는 내부 로직에 큰 차이점이 있다거나…)

useState를 사용할 때, 상태가 객체 혹은 배열인 경우 상태를 변경할 때 항상 상태를 새롭게 복사한 값을 전달하여 상태가 변경되었음을 Object.is 비교 로직에 의해 감지될 수 있도록 해야했다. useReducer도 마찬가지로 객체 혹은 배열 타입의 상태의 경우 내부 값을 직접 변경(mutate)하면 안되지만, reducer 함수의 일반적인 패턴이 새 상태를 생성하여 반환하는 것이므로 복잡한 상태 구조를 가지고 있을 때 useReducer가 useState보다 유용하다.

수많은 이벤트 핸들러에 여러 상태 변경이 분산되면 가독성도 좋지 않을 뿐더러, 상태 변경 로직을 관리하는 것이 어려워진다. 이러한 경우 useReducer를 사용하면 컴포넌트 외부에 있는 reducer로 모든 상태 변경 로직을 옮겨 한 곳에 통합시킬 수 있다.

참고

https://react.dev/learn/extracting-state-logic-into-a-reducer#comparing-usestate-and-usereducer

Eunjee Lee • © 2023 • https://eun-jee.com