React - useReducer

sarang_daddy·2023년 8월 6일
0

React

목록 보기
11/26
post-thumbnail

useState Hook처럼 state를 생성하고 관리해주는 useReducer Hook에 대해서 알아보자.

useReducer는 언제 사용할까?

여러개의 하위 값을 표함하는 복잡한 스테이트를 다워야 할 때 useState보다 간결하고 유지보수를 쉽게 사용할 수 있다.

Reducer & Dispatch & Action

reducer

  • state를 업데이트해주는 역할을 한다.
  • state를 변경하고 싶다면 reducer를 이용해야 한다.
  • reducer에게 state를 변경해달라는요구를 전달하면 내용을 확인하고 변경해준다.
  • action.type(요구 내용 정보)에 따라 분기를 나눈다.

dispatch

  • 사용자의 요구에 해당한다.
  • state 개념에서는 설정자함수(setState)와 같다.
  • action(요구 내용) 이라고 하는 객체를 전달하는 역할을 한다.

action

  • 사용자의 요구에 들어가는 내용에 해당한다.
  • type이라고 하는, reducer에서 분기 처리를 할 때 필요한 문자열정보를 포함한다.
  • reducer에서 해당 명령 수행에 필요한 추가정보들을 담은 일반 객체다.

useReducer 사용하기

useState로 관리되고 있는 아래 예제 TaskApp을 useReducer로 변경해보자.

// state로 관리되는 코드
import { useState } from "react";
import AddTask from "./AddTask.js";
import TaskList from "./TaskList.js";

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  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));
  }

  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 },
];
  • TaskApp 컴포넌트는 state에 tasks 배열을 보유한다.
  • 세 가지의 이벤트 핸들러는 task를 추가, 제거, 수정 한다.
  • 각 이벤트 핸들러는 state 업데이트를 위해 setTasks를 호출한다.
  • 컴포넌트가 커질수록 state를 변경하는 로직도 늘어난다.
  • 이러한 모든 로직을 한곳에 모으기 위해 외부의 reducer라고 하는 단일 함수로 옯길 수 있다.

Step 1 : state 설정을 action들의 전달로 바꾸기 (dispatch로 만들기)

  • 모든 state 설정 로직을 제거한다.
  • 이벤트 핸들러만 남긴다.
    • 사용자가 “Add”를 누르면 handleAddTask(text)가 호출된다.
    • 사용자가 task를 토글하거나 “Save”를 누르면 handleChangeTask(task)가 호출된다.
    • 사용자가 “Delete”를 누르면 handleDeleteTask(taskId)가 호출된다.
  • reducer를 사용한 state 관리는 state를 직접 설정하는 것이 아니다.
  • 이벤트핸들러(dispatch)에서 action을 전달하여 reducer에게사용자가 방금 한 일을 알려준다.
  • 이벤트 핸들러는 tasks를 설정해가 아닌 task를 추가/변경/삭제 해라는 action을 전달한다.
// state 설정로직에서 dispatch로 변경된 코드
  function handleAddTask(text: string) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

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

  function handleDeleteTask(taskId: number) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }
  • dispatch 함수에 넣어둔 객체가 요구 action이다.
  • action 객체에 들어가있는 정보가 내용이 된다.
  • reducer는 내용을 확인하고 반영해서 state를 변경한다.

Step 2 : reducer 함수 작성하기

  • reducer 함수에 state 로직을 포함한다.
  • reducer 함수는 두 개의 매개변수를 가지는데, 하나는 현재 state이고 다른 하나는 action 객체다.
  • reducer 함수의 반환값이 다음 state가 된다.
function yourReducer(state, action) {
  // return next state for React to set
}
  • 이벤트 핸들러에 있던 state 설정 로직을 reducer 함수로 옮긴다.
    • 현재의 state(tasks)를 첫 번째 매개변수로 선언한다.
    • action 객체를 두 번째 매개변수로 선언한다.
    • 다음 state를 reducer함수에서 반환한다.
export function tasksReducer(tasks: TaskType[], action: ActionType) : TaskType[] {
  switch (action.type) {
    case 'added': {
      if (action.id === undefined || action.text === undefined) {
        // 적절한 처리 로직 (예: 에러 던지기, 기본 값 사용 등)
        throw new Error('ID and text must be provided for "added" action.');
      }
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task?.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

export interface TaskType {
  id: number;
  text: string;
  done: boolean;
}

interface ActionType {
  type: 'added' | 'changed' | 'deleted';
  id?: number;
  text?: string;
  task?: TaskType;
}

interface TasksReducerType {
  tasks: TaskType[];
  action: ActionType;
}

Step 3. : 컴포넌트에서 reducer 사용하기

  • state를 관리하던 컴포넌트에서 useReducer Hook을 가져온다.
import { useReducer } from "react";
  • 기존 useState Hook에서 useReducer Hook으로 변경한다.
// 기존 useState 코드
const [tasks, setTasks] = useState(initialTasks);

// reducer로 변경된 코드
const [tasks, dispatch] = useReducer(1️⃣ tasksReducer, 2️⃣ initialTasks);

// useReducer Hook은 두 개의 인자를 받는다.
// 1️⃣ 생성한 reduecer 함수, 2️⃣ 초기 state 값
  • useReducer Hook은 두 개의 인자 1️⃣ 생성한 reducer 함수, 2️⃣ 초기 state 값을 받는다.
  • state와 dispatch 함수를 반환한다.[tasks, dispatch]
  • [tasks, dispatch][state, setState] 와 같이 dispatch를 통해 tasks를 변경 한다.
  • dispatch가 직접 변경하는 것이 아닌 함수로 action을 전달하고 reducer 함수에서 state를 변경하고 반환한다.
// reducer로 변경된 TaskApp 코드
import { useReducer } from 'react';
import { tasksReducer, TaskType } from './TasksReducer';

import AddTask from './AddTask/AddTask';
import TaskList from './TaskList/TaskList';

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false },
];

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

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

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

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

  return (
    <>
      <h1>Day off in Kyoto</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

이벤트핸들러(dispatch)는 action(요구)을 전달하여 무슨 일이 있어났는지만 지정한다.
reducer 함수는 action(요구)의 내용으로 state가 어떻게 변경되는지 결정한다.

useState vs useReducer

  • 코드 크기
    : useReducer를 사용하면 reducer 함수 와 action을 전달하는 부분 모두 작성해야 하기에 useState 보다 코드의 길이가 길어진다. 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer를 사용하면 오히려 코드를 줄이는 데 도움이 될 수 있다.

  • 가독성
    : 간단한 state를 업데이트 하는 경우는 useState가 가독성이 좋다. 그렇지만 state의 구조가 더욱 복잡해지면, 컴포넌트의 코드의 양이 부풀어 오르고 한눈에 읽기 어려워질 수 있다. 이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작 하는지와 이벤트 핸들러를 통해 무엇이 일어났는지 를 깔끔하게 분리할 수 있다.

  • 디버깅
    : useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지, 그리고 왜 그런지 알기 어려울 수 있다. useReducer를 사용하면, reducer에 콘솔 로그를 추가하여 모든 state 업데이트와 왜 (어떤 action으로 인해) 버그가 발생했는지 확인할 수 있다. 각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 알 수 있다. 하지만 useState를 사용할 때보다 더 많은 코드를 살펴봐야 한다.

  • 테스팅
    : reducer는 컴포넌트에 의존하지 않는 순수한 함수다. 즉, 별도로 분리해서 내보내거나 테스트할 수 있다. 일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만, 복잡한 state 업데이트 로직의 경우 reducer가 특정 초기 state와 action에 대해 특정 state를 반환한다고 단언하고 테스트 하는 것이 유용할 수 있다.

일부 컴포넌트에서 잘못된 state 업데이트로 인해 버그가 자주 발생하고 더 많은 구조가 필요하다면 reducer를 사용하자.

참고자료

별코딩 - React Hooks에 취한다
React 공식문서

profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글