SPACE RUMI

Hi, I am rumi. Let's Splattack!

[STUDY] 스터디/React Docs

[5주차] React docs 한글 번역 : Scaling Up with Reducer and Context - 리듀서 및 컨텍스트를 사용한 스케일업

백루미 2023. 3. 18. 01:44
반응형

** react docs Beta가 이제 공식문서가 되었다. https://react.dev/

 

리듀서 및 컨텍스트를 사용한 스케일업

리듀서를 사용하면 컴포넌트의 상태 업데이트 로직을 통합할 수 있습니다. 컨텍스트를 사용하면 다른 컴포넌트에 정보를 깊숙이 전달할 수 있습니다. 리듀서와 컨텍스트를 결합하여 복잡한 화면의 상태를 관리할 수 있습니다.

 

학습 내용

  • 리듀서를 컨텍스트와 결합하는 방법
  • props를 통해 상태와 디스패치를 전달하지 않도록 하는 방법
  • 컨텍스트와 상태 로직을 별도의 파일에 보관하는 방법

 

리듀서와 컨텍스트 결합 

리듀서 소개에서 나온 이 예제에서 상태는 리듀서에 의해 관리됩니다. 리듀서 함수에는 모든 상태 업데이트 로직이 포함되어 있으며 이 파일의 맨 아래에 선언되어 있습니다:

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>Day off in Kyoto</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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

리듀서는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 됩니다. 그러나 앱이 성장함에 따라 또 다른 어려움에 직면할 수 있습니다. 현재 작업 상태와 디스패치 함수는 최상위 TaskApp 컴포넌트에서만 사용할 수 있습니다. 다른 컴포넌트가 작업 목록을 읽거나 변경할 수 있도록 하려면 현재 상태와 이를 변경하는 이벤트 핸들러를 props로 명시적으로 전달해야 합니다.

예를 들어, TaskApp은 작업 목록과 이벤트 핸들러를 TaskList에 전달합니다:

<TaskList
  tasks={tasks}
  onChangeTask={handleChangeTask}
  onDeleteTask={handleDeleteTask}
/>

그리고 태스크 리스트는 이벤트 핸들러를 태스크에 전달합니다:

<Task
  task={task}
  onChange={onChangeTask}
  onDelete={onDeleteTask}
/>

이와 같은 작은 예제에서는 이 방법이 잘 작동하지만 중간에 수십, 수백 개의 컴포넌트가 있는 경우 모든 상태와 함수를 전달하는 것은 상당히 답답할 수 있습니다!

그렇기 때문에 prop을 통해 전달하는 대신 작업 상태와 디스패치 함수를 모두 컨텍스트에 넣을 수 있습니다. 이렇게 하면 트리의 TaskApp 아래에 있는 모든 컴포넌트가 반복적인 "prop 드릴링" 없이 작업을 읽고 작업을 디스패치할 수 있습니다.

리듀서와 컨텍스트를 결합하는 방법은 다음과 같습니다:

  1. 컨텍스트를 만듭니다.
  2. 상태와 디스패치를 컨텍스트에 넣습니다.
  3. 트리의 어느 곳에서나 컨텍스트를 사용합니다.

 

1단계: 컨텍스트 생성 

useReducer Hook은 현재 작업과 이를 업데이트할 수 있는 dispatch 함수를 반환합니다:

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

트리에 전달하려면 두 개의 컨텍스트를 별도로 만들어야 합니다:

  • TasksContext는 현재 작업 목록을 제공합니다.
  • TasksDispatchContext는 컴포넌트가 작업을 디스패치할 수 있는 함수를 제공합니다.

나중에 다른 파일에서 가져올 수 있도록 별도의 파일에서 내보내세요:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

여기서는 두 컨텍스트 모두에 기본값으로 null을 전달하고 있습니다. 실제 값은 TaskApp 컴포넌트에서 제공합니다.

2단계: state와 dispatch를 컨텍스트에 넣기 

이제 TaskApp 컴포넌트에서 두 컨텍스트를 모두 가져올 수 있습니다. useReducer()가 반환한 작업과 dispatch를 가져와서 아래 전체 트리에 제공하세요:

import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
  // ...
  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        ...
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

지금은 props와 컨텍스트를 통해 정보를 전달합니다:

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksContext, TasksDispatchContext } from './TasksContext.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 (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        <h1>Day off in Kyoto</h1>
        <AddTask
          onAddTask={handleAddTask}
        />
        <TaskList
          tasks={tasks}
          onChangeTask={handleChangeTask}
          onDeleteTask={handleDeleteTask}
        />
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

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: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

다음 단계에서는 props 전달을 제거합니다.

3단계: 트리의 어느 곳에서나 컨텍스트 사용 

이제 작업 목록이나 이벤트 핸들러를 트리 아래로 전달할 필요가 없습니다:

<TasksContext.Provider value={tasks}>
  <TasksDispatchContext.Provider value={dispatch}>
    <h1>Day off in Kyoto</h1>
    <AddTask />
    <TaskList />
  </TasksDispatchContext.Provider>
</TasksContext.Provider>

대신 작업 목록이 필요한 모든 컴포넌트는 작업 컨텍스트에서 해당 목록을 읽을 수 있습니다:

export default function TaskList() {
  const tasks = useContext(TasksContext);
  // ...

작업 목록을 업데이트하기위해 모든 컴포넌트가 컨텍스트에서 디스패치 함수를 읽고 호출할 수 있습니다:

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useContext(TasksDispatchContext);
  // ...
  return (
    // ...
    <button onClick={() => {
      setText('');
      dispatch({
        type: 'added',
        id: nextId++,
        text: text,
      });
    }}>Add</button>
    // ...

TaskApp 컴포넌트는 어떠한 이벤트 핸들러도 전달하지 않으며, TaskList 역시 어떠한 이벤트 핸들러도 Task 컴포넌트로 전달하지 않습니다. 각 컴포넌트는 필요한 컨텍스트를 읽습니다:

import { useState, useContext } from 'react';
import { TasksContext, TasksDispatchContext } from './TasksContext.js';

export default function TaskList() {
  const tasks = useContext(TasksContext);
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useContext(TasksDispatchContext);
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

상태는 여전히 최상위 TaskApp 컴포넌트에 '존재'하며, useReducer로 관리됩니다. 하지만 이제 이러한 컨텍스트를 가져와서 사용하면 트리 아래의 모든 컴포넌트에서 해당 작업과 디스패치를 사용할 수 있습니다.

모든 배선을 단일 파일로 이동 

이렇게 할 필요는 없지만 리듀서와 컨텍스트를 모두 단일 파일로 이동하면 컴포넌트를 더욱 깔끔하게 정리할 수 있습니다. 현재 TasksContext.js에는 컨텍스트 선언이 두 개만 포함되어 있습니다:

import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

이 파일은 곧 혼잡해질 것입니다! 리듀서를 같은 파일로 옮깁니다. 그런 다음 같은 파일에 새 TasksProvider 컴포넌트를 선언합니다. 이 컴포넌트가 모든 조각을 하나로 묶어줄 것입니다:

  • 리듀서로 상태를 관리합니다.
  • 이 컴포넌트는 아래 컴포넌트에 두 가지 컨텍스트를 모두 제공합니다.
  • JSX를 전달할 수 있도록 자식을 prop으로 받습니다.
export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

이렇게 하면 TaskApp 컴포넌트에서 모든 복잡성과 배선이 제거됩니다:

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

TasksContext.js에서 컨텍스트를 사용하는 함수를 내보낼 수도 있습니다:

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

컴포넌트가 컨텍스트를 읽어야 하는 경우 이러한 함수를 통해 이를 수행할 수 있습니다:

const tasks = useTasks();
const dispatch = useTasksDispatch();

이렇게 해도 동작이 변경되지는 않지만 나중에 이러한 컨텍스트를 더 분할하거나 이러한 함수에 로직을 추가할 수 있습니다. 이제 모든 컨텍스트와 리듀서 배선이 TasksContext.js에 있습니다. 이렇게 하면 컴포넌트를 깔끔하고 정돈된 상태로 유지하면서 데이터를 가져오는 위치가 아닌 표시되는 내용에 집중할 수 있습니다:

import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

TasksProvider는 작업을 처리하는 방법을 알고 있는 화면의 일부로, useTasks는 작업을 읽는 방법으로, useTasksDispatch는 트리 아래의 모든 컴포넌트에서 작업을 업데이트하는 방법으로 생각할 수 있습니다.

 

Note

useTasks 및 useTasksDispatch와 같은 함수를 사용자 정의 훅이라고 합니다. 함수의 이름이 use로 시작하면 사용자 정의 Hook으로 간주됩니다. 이를 통해 그 안에서 useContext와 같은 다른 Hook을 사용할 수 있습니다.

앱이 성장함에 따라 이와 같은 컨텍스트 리듀서 쌍이 많이 생길 수 있습니다. 이는 트리 깊숙한 곳의 데이터에 액세스하고 싶을 때마다 많은 작업 없이 앱을 확장하고 상태를 끌어올릴 수 있는 강력한 방법입니다.

 

요약

리듀서와 컨텍스트를 결합하면 모든 컴포넌트가 그 위에 있는 상태를 읽고 업데이트할 수 있습니다.
상태와 디스패치 함수를 아래 컴포넌트에 제공하려면:

  • 상태와 디스패치 함수에 대한 컨텍스트 두 개를 생성합니다.
  • 리듀서를 사용하는 컴포넌트에서 두 컨텍스트를 모두 제공합니다.
  • 컨텍스트를 읽어야 하는 컴포넌트에서 두 컨텍스트 중 하나를 사용합니다.

모든 배선을 하나의 파일로 이동하여 컴포넌트를 더욱 깔끔하게 정리할 수 있습니다.

  • 컨텍스트를 제공하는 TasksProvider와 같은 컴포넌트를 내보낼 수 있습니다.
  • 또한, 사용태스크나 사용태스크디스패치 같은 사용자 정의 훅을 내보내서 읽을 수도 있습니다.

앱에서 이와 같은 컨텍스트 리듀서 쌍을 여러 개 가질 수 있습니다.

 

발표요약)

상태가 많고 뎁스가 깊어질 때 props 드릴링을 피하고, 리듀서와 컨텍스트를 조합하여 상태관리를 할 수 있다.

- 컨텍스트 2개를 만든다. (상태를 관리할 컨텍스트, 디스패치를 관리할 컨텍스트)
- 리듀서를 만든다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

- 상위 컴포넌트에 provider 로 감싸준다.
<TasksContext.Provider value={tasks}>
   <TasksDispatchContext.Provider value={dispatch}> 

- 이제 provider 내 하위 컴포넌트 어디서나 리듀서로 상태관리가 가능하다.
<button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button>

 

반응형