비공식 사이트. 24.12.31. 폐쇄예정
공식사이트 바로가기

Extracting State Logic into a ReducerState로직을 Reducer로 추출하기

Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer. 여러 개의 state 업데이트가 여러 이벤트 핸들러에 분산되어 있는 컴포넌트는 과부하가 걸릴 수 있습니다. 이러한 경우, reducer 라고 하는 단일 함수를 통해 컴포넌트 외부의 모든 state 업데이트 로직을 통합할 수 있습니다.

You will learn학습 내용

  • What a reducer function is
  • How to refactor useState to useReducer
  • When to use a reducer
  • How to write one well
  • reducer 함수란 무엇인가
  • useStateuseReducer로 리팩토링 하는 방법
  • reducer를 사용해야 하는 경우
  • reducer를 잘 작성하는 방법

Consolidate state logic with a reducerreducer로 state 로직 통합하기

As your components grow in complexity, it can get harder to see at a glance all the different ways in which a component’s state gets updated. For example, the TaskApp component below holds an array of tasks in state and uses three different event handlers to add, remove, and edit tasks: 컴포넌트가 복잡해지면 컴포넌트의 state가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워질 수 있습니다. 예를 들어, 아래의 TaskApp 컴포넌트는 state에 tasks 배열을 보유하고 있으며, 세 가지의 이벤트 핸들러를 사용하여 task를 추가, 제거 및 수정합니다:

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

Each of its event handlers calls setTasks in order to update the state. As this component grows, so does the amount of state logic sprinkled throughout it. To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”. 각 이벤트 핸들러는 state를 업데이트하기 위해 setTasks를 호출합니다. 컴포넌트가 커질수록 여기저기 흩어져 있는 state 로직의 양도 늘어납니다. 복잡성을 줄이고 모든 로직을 접근하기 쉽게 한 곳에 모으려면, state 로직을 컴포넌트 외부의 reducer라고 하는 단일 함수로 옮길 수 있습니다.

Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps: Reducer는 state를 관리하는 다른 방법입니다. useState에서 useReducer로 마이그레이션하는 방법은 세 단계로 진행됩니다:

  1. Move from setting state to dispatching actions.
  2. Write a reducer function.
  3. Use the reducer from your component.
  1. state를 설정하는 것에서 action들을 전달하는 것으로 변경하기
  2. reducer 함수 작성하기
  3. 컴포넌트에서 reducer 사용하기

Step 1: Move from setting state to dispatching actionsstate 설정을 action들의 전달로 바꾸기

Your event handlers currently specify what to do by setting state: 현재 이벤트 핸들러는 state를 설정하여 수행할 작업 을 지정하고 있습니다:

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

Remove all the state setting logic. What you are left with are three event handlers: 모든 state 설정 로직을 제거합니다. 이제 세 개의 이벤트 핸들러만 남았습니다:

  • handleAddTask(text) is called when the user presses “Add”.
  • handleChangeTask(task) is called when the user toggles a task or presses “Save”.
  • handleDeleteTask(taskId) is called when the user presses “Delete”.
  • 사용자가 “Add”를 누르면 handleAddTask(text)가 호출됩니다.
  • 사용자가 task를 토글하거나 “Save”를 누르면 handleChangeTask(task)가 호출됩니다.
  • 사용자가 “Delete”를 누르면 handleDeleteTask(taskId)가 호출됩니다.

Managing state with reducers is slightly different from directly setting state. Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. (The state update logic will live elsewhere!) So instead of “setting tasks” via an event handler, you’re dispatching an “added/changed/deleted a task” action. This is more descriptive of the user’s intent. reducer를 사용한 state 관리는 state를 직접 설정하는 것과 약간 다릅니다. state를 설정하여 React에게 “무엇을 할 지”를 지시하는 대신, 이벤트 핸들러에서 “action”을 전달하여 “사용자가 방금 한 일”을 지정합니다. (state 업데이트 로직은 다른 곳에 있습니다!) 즉, 이벤트 핸들러를 통해 “tasks를 설정”하는 대신 “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,
});
}

The object you pass to dispatch is called an “action”: dispatch 함수에 넣어준 객체를 “action” 이라고 합니다:

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

It is a regular JavaScript object. You decide what to put in it, but generally it should contain the minimal information about what happened. (You will add the dispatch function itself in a later step.) 이 객체는 일반적인 JavaScript 객체입니다. 여기에 무엇을 넣을지는 여러분이 결정하지만, 일반적으로 무슨 일이 일어났는지 에 대한 최소한의 정보를 포함해야 합니다. (dispatch 함수 자체는 이후 단계에서 추가할 것입니다.)

Note

An action object can have any shape. action 객체는 어떤 형태든 될 수 있습니다.

By convention, it is common to give it a string type that describes what happened, and pass any additional information in other fields. The type is specific to a component, so in this example either 'added' or 'added_task' would be fine. Choose a name that says what happened! 그렇지만 무슨 일이 일어나는지 설명하는 문자열 타입의 type을 지정하고 추가적인 정보는 다른 필드를 통해 전달하도록 작성하는게 일반적입니다. type은 컴포넌트에 따라 다르므로 이 예에서는 'added' 또는 'added_task'를 사용하면 됩니다. 무슨 일이 일어나는지를 설명하는 이름을 선택하세요!

dispatch({
// specific to component
type: 'what_happened',
// other fields go here
});

Step 2: Write a reducer functionreducer 함수 작성하기

A reducer function is where you will put your state logic. It takes two arguments, the current state and the action object, and it returns the next state: reducer 함수에 state 로직을 둘 수 있습니다. 이 함수는 두 개의 매개변수를 가지는데, 하나는 현재 state이고 하나는 action 객체입니다. 그리고 이 함수가 다음 state를 반환합니다:

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

React will set the state to what you return from the reducer. React는 reducer로부터 반환된 것을 state로 설정할 것입니다.

To move your state setting logic from your event handlers to a reducer function in this example, you will: state를 설정하는 로직을 이벤트 핸들러에서 reducer 함수로 옮기기 위해서 다음과 같이 진행해 보세요:

  1. Declare the current state (tasks) as the first argument.
  2. Declare the action object as the second argument.
  3. Return the next state from the reducer (which React will set the state to).
  1. 현재의 state(tasks)를 첫 번째 매개변수로 선언하세요.
  2. action 객체를 두 번째 매개변수로 선언하세요.
  3. 다음 state를 reducer 함수에서 반환하세요. (React가 state로 설정할 것입니다.)

Here is all the state setting logic migrated to a reducer function: 아래는 모든 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);
}
}

Because the reducer function takes state (tasks) as an argument, you can declare it outside of your component. This decreases the indentation level and can make your code easier to read. reducer 함수는 state(tasks)를 매개변수로 갖기 때문에, 컴포넌트 밖에서 reducer 함수를 선언할 수 있습니다. 이렇게 하면 들여쓰기 단계도 줄이고 코드를 읽기 쉽게 만들 수 있습니다.

Note

The code above uses if/else statements, but it’s a convention to use switch statements inside reducers. The result is the same, but it can be easier to read switch statements at a glance. 위에 있던 코드는 if/else 구문을 사용합니다. 그러나 reducer 안에서는 switch 구문을 사용하는 게 일반적입니다. 결과는 똑같지만 switch 구문이 한눈에 봐도 읽기 더 편합니다.

We’ll be using them throughout the rest of this documentation like so: 우리는 이 문서의 나머지 부분에서 다음과 같이 reducer 함수를 작성할 것입니다:

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

We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. If you forget to return, the code will “fall through” to the next case, which can lead to mistakes! case 블럭을 모두 중괄호 {}로 감싸는 걸 추천합니다. 이렇게 하면 다양한 case들 안에서 선언된 변수들이 서로 충돌하지 않습니다. 또한, 하나의 case는 보통 return으로 끝나야합니다. 만약 return을 잊는다면 이 코드는 다음 case에 빠지게 될 것이고, 이는 실수로 이어질 수 있습니다.

If you’re not yet comfortable with switch statements, using if/else is completely fine. 아직 switch 구문에 익숙하지 않다면, if/else를 사용하는 것도 전혀 지장이 없습니다.

Deep Dive | 심층 탐구

Why are reducers called this way?왜 reducer라고 부를까요?

Although reducers can “reduce” the amount of code inside your component, they are actually named after the reduce() operation that you can perform on arrays. reducer들이 비록 컴포넌트 안에 있는 코드의 양을 “줄여주긴” 하지만, 이건 사실 배열에서 사용하는 reduce() 연산을 따서 지은 이름입니다.

The reduce() operation lets you take an array and “accumulate” a single value out of many: reduce() 연산은 배열을 가지고 많은 값들을 하나의 값으로 “누적”할 수 있습니다.

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

The function you pass to reduce is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state. reduce로 넘기는 함수가 “reducer”로 알려져 있습니다. 지금까지의 결과현재의 아이템 을 가지고, 다음 결과 를 반환합니다. React reducer는 이 아이디어와 똑같은 예시입니다. React reducer도 지금까지의 stateaction 을 가지고 다음 state 를 반환합니다. 이런 방식으로 시간이 지나면서 action들을 state로 모으게 됩니다.

You could even use the reduce() method with an initialState and an array of actions to calculate the final state by passing your reducer function to it: 심지어 reduce() 메서드를 initialStateactions 배열을 사용해서 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'},
];

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

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

You probably won’t need to do this yourself, but this is similar to what React does! 이 작업을 직접 할 필요는 없겠지만, 이것은 React가 하는 것과 비슷합니다!

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

Finally, you need to hook up the tasksReducer to your component. Import the useReducer Hook from React: 마지막으로, 컴포넌트에 tasksReducer를 연결해야 합니다. React에서 useReducer Hook을 import하세요:

import { useReducer } from 'react';

Then you can replace useState: 그런 다음, useState 대신:

const [tasks, setTasks] = useState(initialTasks);

with useReducer like so: useReducer로 바꿔주세요:

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

The useReducer Hook is similar to useState—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it’s a little different. useReducer Hook은 useState와 비슷합니다. 초기 state 값을 전달해야 하며, 그 결과로 state 값과 state 설정자 함수(useReducer의 경우 dispatch 함수)를 반환합니다. 하지만 조금 다른 점이 있습니다.

The useReducer Hook takes two arguments: useReducer Hook은 두 개의 인자를 받습니다:

  1. A reducer function
  2. An initial state
  1. reducer 함수
  2. 초기 state

And it returns: 그리고 아래 내용을 반환합니다:

  1. A stateful value
  2. A dispatch function (to “dispatch” user actions to the reducer)
  1. state값
  2. dispatch 함수 (사용자의 action을 reducer에 “전달”해주는 함수)

Now it’s fully wired up! Here, the reducer is declared at the bottom of the component file: 이제 완전히 연결되었습니다! 이제 reducer는 컴포넌트 파일 하단에 선언되어 있습니다:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.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}
      />
    </>
  );
}

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

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

If you want, you can even move the reducer to a different file: 원한다면, reducer를 다른 파일로 분리하는 것도 가능합니다:

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

Component logic can be easier to read when you separate concerns like this. Now the event handlers only specify what happened by dispatching actions, and the reducer function determines how the state updates in response to them. 이렇게 관심사를 분리하면 컴포넌트 로직을 더 쉽게 읽을 수 있습니다. 이제 이벤트 핸들러는 action을 전달하여 무슨 일이 일어났는지 만 지정하고, reducer 함수는 action에 대한 응답으로 state가 어떻게 변경되는지 를 결정합니다.

Comparing useState and useReduceruseStateuseReducer 비교하기

Reducers are not without downsides! Here’s a few ways you can compare them: Reducer도 좋은 점만 있는 것은 아닙니다! 다음은 useState 와 useReducer 를 비교할 수 있는 몇 가지 방법입니다:

  • Code size: Generally, with useState you have to write less code upfront. With useReducer, you have to write both a reducer function and dispatch actions. However, useReducer can help cut down on the code if many event handlers modify state in a similar way. 코드 크기: 일반적으로 useState를 사용하면 미리 작성해야 하는 코드가 줄어듭니다. useReducer를 사용하면 reducer 함수 action을 전달하는 부분 모두 작성해야 합니다. 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer를 사용하면 코드를 줄이는 데 도움이 될 수 있습니다.
  • Readability: useState is very easy to read when the state updates are simple. When they get more complex, they can bloat your component’s code and make it difficult to scan. In this case, useReducer lets you cleanly separate the how of update logic from the what happened of event handlers. 가독성: useState로 간단한 state를 업데이트 하는 경우 가독성이 좋습니다. 그렇지만 state의 구조가 더욱 복잡해지면, 컴포넌트의 코드의 양이 부풀어 오르고 한눈에 읽기 어려워질 수 있습니다. 이 경우 useReducer를 사용하면 업데이트 로직이 어떻게 동작 하는지와 이벤트 핸들러를 통해 무엇이 일어났는지 를 깔끔하게 분리할 수 있습니다.
  • Debugging: When you have a bug with useState, it can be difficult to tell where the state was set incorrectly, and why. With useReducer, you can add a console log into your reducer to see every state update, and why it happened (due to which action). If each action is correct, you’ll know that the mistake is in the reducer logic itself. However, you have to step through more code than with useState. 디버깅: useState에 버그가 있는 경우, state가 어디서 잘못 설정되었는지, 그리고 왜 그런지 알기 어려울 수 있습니다. useReducer를 사용하면, reducer에 콘솔 로그를 추가하여 모든 state 업데이트와 (어떤 action으로 인해) 버그가 발생했는지 확인할 수 있습니다. 각 action이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 알 수 있습니다. 하지만 useState를 사용할 때보다 더 많은 코드를 살펴봐야 합니다.
  • Testing: A reducer is a pure function that doesn’t depend on your component. This means that you can export and test it separately in isolation. While generally it’s best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action. 테스팅: reducer는 컴포넌트에 의존하지 않는 순수한 함수입니다. 즉, 별도로 분리해서 내보내거나 테스트할 수 있습니다. 일반적으로 보다 현실적인 환경에서 컴포넌트를 테스트하는 것이 가장 좋지만, 복잡한 state 업데이트 로직의 경우 reducer가 특정 초기 state와 action에 대해 특정 state를 반환한다고 단언하는 것이 유용할 수 있습니다.
  • Personal preference: Some people like reducers, others don’t. That’s okay. It’s a matter of preference. You can always convert between useState and useReducer back and forth: they are equivalent! 개인 취향: 어떤 사람은 reducer를 좋아하고 어떤 사람은 싫어합니다. 괜찮습니다. 취향의 문제니까요. useStateuseReducer는 언제든지 앞뒤로 변환할 수 있으며, 서로 동등합니다!

We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even useState and useReducer in the same component. 일부 컴포넌트에서 잘못된 state 업데이트로 인해 버그가 자주 발생하고 코드에 더 많은 구조를 도입하려는 경우 reducer를 사용하는 것이 좋습니다. 모든 컴포넌트에 reducer를 사용할 필요는 없으니 자유롭게 섞어서 사용하세요! 심지어 같은 컴포넌트에서 useStateuseReducer를 함께 사용할 수도 있습니다.

Writing reducers wellreducer 잘 작성하기

Keep these two tips in mind when writing reducers: reducer를 작성할 때 다음 두 개의 팁을 기억하세요:

  • Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output. They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations. reducer는 반드시 순수해야 합니다. state 설정 함수와 비슷하게, reducer는 렌더링 중에 실행됩니다! (action은 다음 렌더링까지 대기합니다.) 즉, reducer는 반드시 순수해야 합니다. 즉, 입력 값이 같다면 결과 값도 항상 같아야 합니다. 요청을 보내거나 timeout을 스케쥴링하거나 사이드 이펙트(컴포넌트 외부에 영향을 미치는 작업)을 수행해서는 안 됩니다. reducer는 객체배열을 변이 없이 업데이트해야 합니다.

  • Each action describes a single user interaction, even if that leads to multiple changes in the data. For example, if a user presses “Reset” on a form with five fields managed by a reducer, it makes more sense to dispatch one reset_form action rather than five separate set_field actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging! 각 action은 여러 데이터가 변경되더라도, 하나의 사용자 상호작용을 설명해야 합니다. 예를 들어, 사용자가 reducer가 관리하는 5개의 필드가 있는 양식에서 ‘재설정’을 누른 경우, 5개의 개별 set_field action보다는 하나의 reset_form action을 전송하는 것이 더 합리적입니다. 모든 action을 reducer에 기록하면 어떤 상호작용이나 응답이 어떤 순서로 일어났는지 재구성할 수 있을 만큼 로그가 명확해야 합니다. 이는 디버깅에 도움이 됩니다!

Writing concise reducers with ImmerImmer를 사용하여 간결한 reducer 작성하기

Just like with updating objects and arrays in regular state, you can use the Immer library to make reducers more concise. Here, useImmerReducer lets you mutate the state with push or arr[i] = assignment: 일반 state의 객체배열을 변경할 때와 마찬가지로 Immer 라이브러리를 사용해 reducer를 더 간결하게 만들 수 있습니다. 여기서 useImmerReducer를 사용하면 push 또는 arr[i] = 할당으로 state를 변이할 수 있습니다:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Reducers must be pure, so they shouldn’t mutate state. But Immer provides you with a special draft object which is safe to mutate. Under the hood, Immer will create a copy of your state with the changes you made to the draft. This is why reducers managed by useImmerReducer can mutate their first argument and don’t need to return state. reducer는 순수해야 하므로 state를 변이하지 않아야 합니다. 하지만 Immer는 안전하게 변이할 수 있는 특별한 draft 객체를 제공합니다. 내부적으로 Immer는 사용자가 변경한 draft로 state의 복사본을 생성합니다. 이 방식을 통해 useImmerReducer로 관리되는 reducer는 첫 번째 인수를 변경할 수 있고, state를 반환할 필요가 없습니다.

Recap요약

  • To convert from useState to useReducer:
    1. Dispatch actions from event handlers.
    2. Write a reducer function that returns the next state for a given state and action.
    3. Replace useState with useReducer.
  • Reducers require you to write a bit more code, but they help with debugging and testing.
  • Reducers must be pure.
  • Each action describes a single user interaction.
  • Use Immer if you want to write reducers in a mutating style.
  • useSate에서 useReducer로 변환하려면:
    1. 이벤트 핸들러에서 action을 전달합니다.
    2. 주어진 state와 action에 대해 다음 state를 반환하는 reducer 함수를 작성합니다.
    3. useStateuseReducer로 바꿉니다.
  • reducer를 사용하면 코드를 조금 더 작성해야 하지만 디버깅과 테스트에 도움이 됩니다.
  • reducer는 반드시 순수해야 합니다.
  • 각 action은 단일 사용자 상호작용을 설명해야 합니다.
  • 변이 스타일로 reducer를 작성하려면 Immer를 사용하세요.

Challenge 1 of 4: Dispatch actions from event handlers이벤트 핸들러에서 action을 dispatch하기

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient. 현재 ContactList.jsChat.js의 이벤트 핸들러에는 // TODO 주석이 있습니다. 이 때문에 input에 타이핑해도 작동하지 않고 버튼을 클릭해도 선택한 수신자가 변경되지 않습니다.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js. 두 개의 // TODO를 해당 작업을 dispatch하는 코드로 바꾸세요. 예상되는 모양과 action의 유형을 확인하려면 messengerReducer.js에서 reducer를 확인하세요. reducer는 이미 작성되어 있으므로 변경할 필요가 없습니다. ContactList.jsChat.js에서 action을 전달하기만 하면 됩니다.

import { useReducer } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';
import { initialState, messengerReducer } from './messengerReducer';

export default function Messenger() {
  const [state, dispatch] = useReducer(messengerReducer, initialState);
  const message = state.message;
  const contact = contacts.find((c) => c.id === state.selectedId);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedId={state.selectedId}
        dispatch={dispatch}
      />
      <Chat
        key={contact.id}
        message={message}
        contact={contact}
        dispatch={dispatch}
      />
    </div>
  );
}

const contacts = [
  {id: 0, name: 'Taylor', email: 'taylor@mail.com'},
  {id: 1, name: 'Alice', email: 'alice@mail.com'},
  {id: 2, name: 'Bob', email: 'bob@mail.com'},
];