[React] useReducer 파헤치기

Narcoker·2023년 10월 15일
0

React

목록 보기
29/32

reducer란?

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

아래 예시가 여러 개의 state 업데이트가 여러 이벤트 핸들러에 분산되어 있는 컴포넌트 이다.
tasks 라는 상태를 여러 핸들러 함수,
handleAddTask, handleChangeTask, handleDeleteTask 로 업데이트 하는 코드다.

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},
];

Reducer 로의 전환법

1. state 설정을 action들의 전달로 바꾸기

reducer를 사용한 state 관리는 state를 직접 설정하는 것과 약간 다르다.
state를 설정하여 React에게 무엇을 할 지를 지시하는 대신,
이벤트 핸들러에서 action을 전달하여 사용자가 방금 한 일을 지정합니다.
state 업데이트 로직은 다른 곳에 있다!

즉, 이벤트 핸들러를 통해 tasks를 설정하는 대신,
task를 추가/변경/삭제하는 action을 전달하는 것이다.
이러한 방식이 사용자의 의도를 더 명확하게 설명한다.

dispatch 함수에 넣어준 객체를 action 이라고 한다.
이 객체는 일반적인 JavaScript 객체이다.
여기에 무엇을 넣을지는 개발자가 결정하지만,
일반적으로 무슨 일이 일어났는지 에 대한 최소한의 정보(type 속성)를 포함해야 한다.

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

2. reducer 함수 작성하기

reducer 함수에 state 업데이트 로직을 둘 수 있다.
두 개의 매개변수를 가지는데, 하나는 현재 state이고 하나는 action 객체이다.
그리고 이 함수가 변경될 state를 반환한다.

if-else 문을 사용해도 괜찮으나 react 공식문서에서는 가독성 상 Switch 문을 권장하고 있다.

그리고 case 블럭을 모두 중괄호 { }로 감싸는 걸 권장한다.
이렇게 하면 다양한 case들 안에서 선언된 변수들이 서로 충돌하지 않는다. 블록 레벨 스코프

또한, 하나의 case는 보통 return으로 끝나야한다.
만약 return을 잊는다면 이 코드는 다음 case에 빠지게 될 것이고,
이는 실수로 이어질 수 있다.

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      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);
    }
  }
}

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

마지막으로, 컴포넌트에 tasksReducer를 연결해야 한다.
React에서 useReducer Hook을 import해야한다.

import { useReducer } from 'react';

그런 다음 useState 대신 useReducer로 바꾼다.

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

useReducer

useReducer Hook은 reducer 함수초기 state 두 개의 인자를 받는다.

그리고 state값dispatch 함수 (사용자의 action을 reducer에 “전달”해주는 함수)
를 반환한다.

최종 코드

App.js

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(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},
];

taskReducer.js

export default function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      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);
    }
  }
}

useState와 useReducer 비교

코드 크기

일반적으로 useState를 사용하면 미리 작성해야 하는 코드가 줄어든다.
useReducer를 사용하면 reducer 함수 와 action을 전달하는 부분 모두 작성해야 한다.

하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우,
useReducer를 사용하면 코드를 줄이는 데 도움이 될 수 있다.

가독성

useState로 간단한 state를 업데이트 하는 경우 가독성이 좋다.
그렇지만 state의 구조가 더욱 복잡해지면,
컴포넌트의 코드의 양이 부풀어 오르고 한눈에 읽기 어려워질 수 있다.

이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작 하는지와
이벤트 핸들러를 통해 무엇이 일어났는지 를 깔끔하게 분리할 수 있습니다.

디버깅

useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지,
그리고 왜 그런지 알기 어려울 수 있다.

useReducer를 사용하면, reducer에 콘솔 로그를 추가하여
모든 state 업데이트와 왜 (어떤 action으로 인해) 버그가 발생했는지 확인할 수 있다.

각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 알 수 있다.
하지만 useState를 사용할 때보다 더 많은 코드를 살펴봐야 한다.

테스팅

reducer는 컴포넌트에 의존하지 않는 순수한 함수이다.

즉, 별도로 분리해서 내보내거나 테스트할 수 있다.

일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만,
복잡한 state 업데이트 로직의 경우,
reducer가 특정 초기 state와 action에 대해
특정 state를 반환한다고 단언하는 것이 유용할 수 있다.

개인 취향

어떤 사람은 reducer를 좋아하고 어떤 사람은 싫어한다.
괜찮다. 취향의 문제다.
useState 와 useReducer는 언제든지 앞뒤로 변환할 수 있으며, 서로 동등하다!

정리

Reducer란 State를 여러 방식으로 업데이트 하는 공간 이다.
Dispatch란 State 업데이트를 제어하는 수단 그자체(동사) 이다.
Action이란 Dispatch의 내용, 즉 요구의 내용 이다.

이때 Action은 자유 형식의 일반적인 객체이다.
그리고, 어떤 내용인지를 정의하는 type 속성은 반드시 있어야 한다.

즉, Reducer에는 상태를 여러 방식으로 업데이트하기 위한 로직들을 가지고 있으며
Dispatch로 Reducer에 있는 로직을 실행할 수 있다.

단, Reducer는 여러 로직을 가지고 있기 때문에 하나의 로직을 실행하기 위해서
Action 객체로 어떤 로직을 수행할 것인지 Dispatch에게 알려주어야 한다.

useReducer상태상태를 제어하기 위한 수단을 만들어주는(반환) React 훅이다.
이를 만들어 내기 위해서 는 상태를 업데이트하는 수단을 만들어 받아들일 수 있는 공간
상태의 초기값을 지정(파라미터 지정)해줘야한다.

이때 공간이 하는 역할의 코드는 파일 분리를 해서 관리할 수 있다.
이러한 흐름으로 다음과 같은 사용법이 만들어진다.

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

참고

profile
열정, 끈기, 집념의 Frontend Developer

0개의 댓글