본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Extracting State Logic into a Reducer(State 로직을 Reducer로 추출하기)

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

Consolidate state logic with a reducer(reducer로 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},
];

위 예제 코드에서 각 이벤트 핸들러는 state를 업데이트하기 위해 setTasks를 호출하는데, 컴포넌트가 커지게 되면 state 로직의 양도 늘어나게 된다. 따라서, 복잡성을 줄이고 모든 로직을 접근하기 쉽게 한 곳에 모으려면, state 로직을 컴포넌트 외부의 reducer라고 하는 단일 함수로 옮길 수 있다.


Step 1: Move form setting state to dispatching actions (state 설정을 action들의 전달로 바꾸기)



reducer를 사용한 state 관리는 state를 직접 설정하는 것과 약간 다르다. state를 설정하는 React에게는 "무엇을 할지"를 지시했다면, 이벤트 핸들러에서 "action"을 전달해서 "사용자가 방금 한 일"을 지정한다.(state 업데이트 로직은 다른 곳에 있기 때문)

function handleDeleteTask(taskId) {
  dispatch(
    // "action" object:
    {
      type: 'deleted',
      id: taskId,
    }
  );
}

* dispatch 함수에 넣어준 객체를 "action"이라고 한다.


Step 2: Write a reducer function(reducer 함수 작성하기)

 

- reducer 함수는 현재 state와 action 객체를 매개변수로 갖고, 다음 state를 반환한다.

- reducer 함수는 state를 매개변수로 갖기 때문에, 컴포넌트 밖에서 reducer 함수를 선언할 수 있다.(들여쓰기 단계도 줄이고 코드를 읽기 쉽게 만드는 방법)

- reducer 안에서는 if/else 구문보다는 switch 구문을 사용하는게 일반적이다. switch 문 내부의 case 블럭을 모두 중괄호({})로 감싸는 것이 좋고, 하나의 case는 보통 return으로 끝나야 한다.

 

* reduce() 메서드를 initialState와 actions 배열을 사용해서 reducer로 최종 state를 계산하는 예시

import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

// reduce 사용
let finalState = actions.reduce(tasksReducer, initialState);

const output = document.getElementById('output');
output.textContent = JSON.stringify(finalState, null, 2);

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

Step 3: Use the reducer from your component(컴포넌트에서 reducer 사용하기)

 

- useReducer는 reducer 함수와 초기 state를 인자로 받고, state 값과 dispatch 함수(사용자의 action을 reducer에 "전달"해주는 함수)를 반환한다.

- reducer 함수를 다른 파일로 분리하는 것도 가능하다.

 

* 위의 예제를 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);
    }
  }
}

Comparing useState and useReducer(useState와 useReducer 비교하기)

 

- Code size(코드 크기) : useState를 사용하면 미리 작성해야 하느 코드가 줄어들지만, useReducer를 사용하면 reducer 함수와 action을 전달하는 부분 모두 작성해야 한다.(많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer를 사용하면 코드를 줄이는데 도움이 된다.)

 

- Readability(가독성) : useState의 경우 state 구조가 복잡해지면 가독성이 떨어질 수 있다. 이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작하는지 이벤트 핸들러를 통해 무엇이 일어났는지를 깔끔하게 분리할 수 있다.

 

- Debugging(디버깅) : useReducer를 사용하면, reducer에 콘솔 로그를 추가하여 모든 state 업데이트와 왜(어떤 action으로 인해) 버그가 발생했는지 확인할 수 있다.(다만, useState를 사용할 때보다 더 많은 코드를 살펴봐야한다)

 

- Testing(테스팅) : reducer는 컴포넌트에 의존하지 않는 순수한 함수이기 때문에 별도로 분리해서 내보내거나 테스트를 할 수 있다.


Writing reducers well(reducer 잘 작성하기)

- reducer는 반드시 순수해야 한다. state 설정 함수와 비슷하게 reducer는 렌더링 중에 실행되기 때문에 항상 순수해야 한다.

- 각 action은 여러 데이터가 변경되더라도, 하나의 사용자 상호작용을 설명해야 한다. 예를들어 사용자 reducer가 관리하는 5개의 필드가 있는 양식에서 '재설정' 버튼을 누른 경우, 5개의 개별 필드를 변화시키는 action보다는 모든 필드를 reset을 전송하는 것이 더 합리적이다.


* 참고 : React 공식문서(https://react-ko.dev/learn)