https://react.dev/learn/extracting-state-logic-into-a-reducer 를 읽고 정리한다.

리듀서 함수란

많은 이벤트 핸들러에 걸쳐 많은 상태 업데이트가 있는 컴포넌트는 다루기 힘들다.

이때 컴포넌트 외부에서 하나의 함수로 모든 상태 업데이트를 통합할 수 있다. 이를 "리듀서"라고 부른다.

상태 로직의 양이 늘어남에 따라 생기는 복잡성을 줄이고 모든 로직을 액세스 하기 쉬운 한 곳에 유지하기 위해 사용한다.

 

useState를 useReducer로 리팩토링하는 방법

1. 상태를 변경하는 함수 호출(setting state)에서 액션을 디스 패치하는 동작으로 변경해라

리듀서로 상태를 관리하는 것과 상태를 직접 설정하는 것은 다르다.

  • 상태를 설정하는 것은 React에 "할 일(what to do)"을 알려준다.
    • ex) tasks 이벤트 핸들러를 통해 상태를 설정
  • 리듀서로 상태를 관리하는 것은 이벤트 핸들러에서 액션을 디스패치함으로서 "사용자가 방금 한 일(what the user just did)"에 대해 알려준다.(상태 업데이트 로직은 다른 곳에 존재) 따라서 사용자의 의도를 더 잘 설명한다.
    • ex) tasks 이벤트 핸들러를 통해 작업 추가/변경/삭제 작업을 전달

dispatch로 전달하는 객체를 "action"이라고 한다.

  • js object이며 모든 형태의 데이터를 가질 수 있고, 발생한 일에 대한 최소한의 정보를 포함한다.
  • Convention : 보통 type을 사용해 발생한 일(deleted)을 설명하는 문자열을 제공하고 다른 필드에 추가 정보를 전달한다. 
function handleDeleteTask(taskId) {
  dispatch(
    // "action" object:
    {
      type: 'deleted',
      id: taskId,
    }
  );
}

2. reducer 함수를 작성해라

reducer 함수에 상태 로직을 넣는다. 현재 상태(state)와 작업 개체(action)의 두 인수를 사용하고 다음 상태를 반환하도록 한다.

React는 reducer에서 반환한 상태로 상태를 설정한다.

function yourReducer(state, action) {
  // return next state for React to set
}
reducer 함수는 상태를 인수로 취하기 때문에 컴포넌트 외부에서 선언할 수 있다.
이렇게 하면 들여쓰기가 줄어들고 코드를 더 쉽게 읽을 수 있다.
Note

reducer 내부에서 보통 if/else문보다 switch문을 사용한다. 결과는 같지만 switch 문을 한눈에 읽는 것이 더 쉽다.
또한 서로 다른 내부에서 선언된 변수가 서로 충돌하지 않도록 각 case 블록을 중괄호로 묶는 것이 좋다.
case문은 return문으로 끝난다. 그렇지 않으면 코드가 다음 case로 "떨어질 수 있습니다(fall through)".
Deep dive : React의 reducer는 배열의 reduce()연산과 비슷한 형식을 가지고 있다.

reducer는 컴포넌트 내부의 코드 양을 줄일 수(reduce)도 있지만, 배열에서 사용하는 reduce()연산을 본떠서 이름이 지어졌다.
reduce는 배열을 선택하고 많은 값 중에서 단일 값을 "누적"할 수 있다.
여기서 reduce로 전달하는 함수를 reducer라고 한다.

React의 reducer는 같은 아이디어를 사용한다. 따라서 reducer는 지금까지의 상태와 액션을 취하고 다음 상태를 반환하도록 동작한다. 이런 식으로 시간이 지남에 따라 행동을 상태로 축적한다.

 

3. 컴포넌트에서 reducer를 사용해라

react의 useReducer Hook을 사용한다.

import {useReducer} from 'react';

그런 다음 useStateuseReducer로 변경한다.

// useState
const [tasks, setTasks] = useState(initialTasks);
// useReducer
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer Hook과 useState는 초기 상태 전달/상태 값을 반환/상태를 설정(dispatch 함수)과 유사하다.

하지만 조금 다른 점이 있다.(useReducer Hook 기준)

  • 두가지 인수를 취한다.
    • reducer function
    • initial state
  • 반환 값
    • 다음 상태 값
    • 디스패치 기능(사용자 작업을 reducer에게 디스 패치하기 위해)

 

결론적으로 이벤트 핸들러는 액션을 디스 패치하여 발생한 일만 지정하고 리듀서 함수는 이에 대한 응답으로 상태가 업데이트되는 방식을 결정한다.

 

useStateuseReducer 비교

컴포넌트에서 잘못된 상태 업데이트로 인해 버그가 자주 발생하고 해당 코드에 더 많은 구조를 도입하려는 경우 reducer를 사용하는 것이 좋다. 하지만 모든 것에 reducer를 사용할 필요는 없다. 자유롭게 장단점에 맞게 사용하면 된다.

  • code size
    • useState : 일반적으로 코드가 작다.
    • useReducer : 리듀서 함수와 디스패치 작업을 해야 하기 때문에 코드량이 많다. 하지만 많은 이벤트 핸들러가 유사한 방식으로 상태를 수정하는 경우 코드를 줄이는 데 도움이 될 수 있다.
  • 가독성
    • useState : 상태 업데이트가 단순할 때 읽기 쉽다.
    • useReducer : 상태 업데이트가 복잡해서 컴포넌트를 읽기 어려울 경우 사용한다. 그러면 이벤트 처리기에서 발생한 것과 상태 업데이트 로직이 분리되어 명확히 구분할 수 있다.
  • 디버깅
    • useState : 버그가 있는 경우, 상태가 잘못 설정된 위치와 이유를 구별하기 어렵다.
    • useReducer : console log를 리듀서에 추가해 모든 상태 업데이트와 발생한 이유를 확인할 수 있다. action 호출과 reducer 로직을 나누어서 파악할 수 있다. 하지만 useState를 사용하는 것보다 더 많은 코드를 단계별로 실행해야 한다.
  • 테스팅
    • useState : 컴포넌트에 의존되어있다.
    • useReducer : 리듀서는 순수 함수기 때문에 별도로 내보내고 테스트할 수 있다. 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만 복잡한 상태 업데이트 로직의 경우 reducer가 특정 초기 상태 및 작업에 대해 특정 상태를 반환한다고 assert하는 것이 유용할 수 있다.
  • 개인 취향
    • 개인 취향은 모두 다를 수 있다. 여기서는 useState <-> useReducer 는 상호 호환 가능하다.

reducer를 잘 사용하는 팁

Reducer는 순수해야 한다.

  • 상태 업데이터 함수와 비슷하게 reducer는 렌더링 중에 실행된다. 이는 reducer가 순수해야 함을 의미한다.
  • 동일한 입력은 항상 동일한 출력을 낳아야 한다. 요청을 보내거나 시간 초과를 예약하거나 side effect를 수행하면 안 된다.
  • 객체배열을 주의해서 업데이트해야 한다. 이는 immer를 사용하면 간결하게 할 수 있다.

각 action은 single user의 상호작용을 묘사한다. 이는 데이터가 여러 번 변경되는 경우도 마찬가지다.

  • 예를 들어 사용자가 reducer에서 관리하는 5개의 필드가 있는 양식에서 "재설정"을 누르면 5개의 개별 설정 작업보다 하나의 작업을 전달해야 한다.
  • 작업 기록 시 로그가 상호작용이나 응답이 어떤 순서로 발생했는지 재구성할 수 있을 만큼 명확해야 디버깅 시 도움이 된다.

Immer로 간결한 reducer 작성하기

Reducer는 순수해야 하므로 상태를 변경하면 안 된다.

Immer에서는 draft 객체를 사용해 변경을 안전하게 할 수 있다.

  • 예를 들어 useImmerReducer를 사용해 배열이나 객체를 업데이트할 때 간결하게 push, arr[i]= 할당 문을 사용해 상태를 변경할 수 있다.

 

challenges

4번 문제 : useReducer를 간단히 구현하라.

useReducer는 action을 dispatch하면 현재 state와 action이 있는 reducer를 호출하고 그 결과를 다음 state로 저장한다.

import {useState} from 'react';

export function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}

더 정확한 구현은 다음과 같다.

function dispatch(action) {
  setState((s) => reducer(s, action));
}

+ Recent posts