본문 바로가기

개발 공부

[리액트 공식 문서] state 로직을 reducer로 추출하기

reducer를 쓰는 경우


- 여러 개의 state 업데이트가 여러 이벤트 핸들러에 분산되어 있는 컴포넌트는 과부하가 걸릴 수 있는데, 이 경우에 reducer라고 하는 단일 함수를 통해 컴포넌트 외부의 모든 state 업데이트 로직을 통합할 수 있다.

 

reducer로 state 로직 통합하기


- 컴포넌트가 복잡해지면 컴포넌트의 state가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워진다. (삭제, 추가 , 수정)

- 컴포넌트가 커질수록 여기저기 흩어져 있는 state로직의 양도 늘어난다.

- 복잡성을 줄이고 모든 로직을 접근하기 쉽게 한 곳에 모으려면, state 로직을 컴포넌트 외부의 reducer라고 하는 단일 함수로 옮긴다.

useState에서 useReducer로 마이그레이션하는 방법

1. state를 설정하는 것에서 action들을 전달하는 것으로 변경한다.

2. reducer 함수를 작성한다.

3. 컴포넌트에서 reducer를 사용한다.

 

Step1: state 설정을 action들의 전달로 바꾸기


- 기존 useState로 관리하고 있는 코드

function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

function handleChangeTask(task) {
  setTasks(
    tasks.map((t) => {
      if (t.id === task.id) {
        return task;
      } else {
        return t;
      }
    })
  );
}

function handleDeleteTask(taskId) {
  setTasks(tasks.filter((t) => t.id !== taskId));
}

- reducer를 사용하여 state를 관리하는 코드

  • React에게 '무엇을 할 지' 를 지시하는 대신, 이벤트 핸들러에서 'action'을 전달하여 '사용자가 방금 한 일'을 지정한다.
  • 즉, 이벤트 핸들러를 통해 'task를 설정'하는 대신 'task를 추가/변경/삭제'하는 action을 전달하는 것이다.
function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

function handleDeleteTask(taskId) {
  dispatch({
    type: 'deleted',
    id: taskId,
  });
}
더보기

참고

  1. action 객체는 어떤 형태든 될 수 있다.
  2. 무슨 일이 일어나는지 설명하는 문자열 타입의 type을 지정하고 추가적인 정보는 다른 필드를 통해 전달하도록 작성하는게 일반적이다. (type 예시: 'added' or 'added_task'

 

Step2: reducer 함수 작성하기


- reducer 함수에 state 로직을 둘 수 있다.

- 이 함수의 경우 두개의 매개변수를 가진다. 하나는 현재 state이고 하나는 action 객체 이다. 그리고 다음 state를 반환한다.

function yourReducer(state, action) {
  // return next state for React to set
}

- React는 reducer로부터 반환된 것을 state로 설정합니다.

state 설정 로직을 이벤트 핸들러에서 reducer함수로 옮기기

  1. 현재의 state(tasks)를 첫 번째 매개 변수로 선언한다.
  2. action 객체를 두 번째 매개 변수로 선언한다.
  3. 다음 state를 reducer함수에 반환한다.
function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === 'changed') {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}
더보기

참고

- reducer안에서는 switch 구문을 사용하는 게 일반적입니다.

- case 블럭을 모두 중괄호 { 와}로 감싸는 것을 추천하며, retun으로 끝나야한다.

 

왜 reducer라고 부를까?

- 배열에서 사용하는 reduce() 연산을 따서 지은 이름인데, reduce() 연ㅅ나은 배열을 가지고 많은 값들을 하나의 값으로 누적할 수 있다.

그래서, reducer는 지금까지의 결과와 현재의 아이템을 가지고, 다음 결과를 반환합니다. 즉, 지금까지의 state와 action을 가지고 다음 state를 반환한다. 시간이 지나면서 action들은 state로 모으게 됩니다.

 

Step3: 컴포넌트에서 reducer 사용하기

1. React에서 useReducer Hook을 import합니다.

import { useReducer } from 'react';

2. useState를 useReducer로 바꿔줍니다.

const [tasks, setTasks] = useState(initialTasks);
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
더보기

useState와 useReducer의 다른 점

1. useReducer는 reducer 함수와 초기 state를 인자로 넘겨 받는다.

2.  state값과 dispatch 함수(사용자의 action을 reducer에 전달해주는 함수)를 반환한다

관심사를 분리하면 이점은 이벤트 핸들러는 action을 전달하여 무슨 일이 일어났는지만 지정하고, reducer함수는 action에 대한 응답으로 state가 어떻게 변경되는지를 결정합니다.

 

useState와 useReducer 비교하기


  useState useReducer
코드 크기 미리 작성 코드 감소 복잡한 코드 일 경우 작성 코드 감소 용이
가독성 간단한 업데이트 경우 좋음 복잡한 업데이트 경우 좋음
디버깅 버그 찾기 복잡함 버그 찾기 용이
테스팅   순수 함수로 테스트하기 용이

reducer 잘 작성하기

  1. 반드시 순수해야한다. state 설정 함수와 비슷하게 렌더링 중에 실행된다. 요청을 보내거나 timeout을 스케쥴링하거나 사이드 이팩트를 수행해서는 안된다.
  2. 각 action은 여러 데이터가 변경되더라도, 하나의 사용자 상호작용을 설명해야 합니다. 모든 action을 reducer에 기록하면 어떤 상호작용이나 응답이 어떤 순서로 일어났는지 재구성할 수 있을 만큼 로그가 명확해야 합니다. 

Immer를 사용하여 간결한 reducer 작성하기

- userImmerReducer를 사용하면 push 또는 arr[i]= 할당으로 state를 변이할 수 있습니다.

- reducer는 순수해야해서 state를 변이하지 않아야하지만, Immer는 안전하게 변이할 수 있는 특별한 draft 객체를 제공합니다.

 

import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

function tasksReducer(draft, action) {
  switch (action.type) {
    case 'added': {
      draft.push({
        id: action.id,
        text: action.text,
        done: false,
      });
      break;
    }
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export default function TaskApp() {
  const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

참고

https://react-ko.dev/learn/extracting-state-logic-into-a-reducer