ReactNextCentral

Reducer와 Context 활용

Published on
리듀서와 컨텍스트를 결합하여 리액트 앱에서 상태를 관리하고 컴포넌트 간 데이터 공유를 효율적으로 처리하는 방법을 학습합니다.
Table of Contents

리듀서는 컴포넌트의 상태 업데이트 로직을 효율적으로 관리할 수 있게 도와줍니다. 컨텍스트는 정보를 다른 컴포넌트로 깊게 전달할 수 있게 해줍니다. 리듀서와 컨텍스트를 결합하여 복잡한 화면의 상태를 관리할 수 있습니다.

배울 내용

  • 리듀서와 컨텍스트를 결합하는 방법
  • 상태와 디스패치 함수를 프롭스를 통해 전달하지 않는 방법
  • 컨텍스트와 상태 로직을 별도의 파일로 유지하는 방법

리듀서와 컨텍스트를 결합하기

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

Edit bold-rui-3lvffx

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

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

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

그리고 TaskList는 이벤트 핸들러를 Task에 전달합니다.

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

이런 작은 예제에서는 잘 작동하지만, 중간에 수십 개 또는 수백 개의 컴포넌트가 있는 경우 모든 상태와 함수를 전달하는 것은 꽤 괴로울 수 있습니다!

따라서 이러한 프롭스 전달 대신에 tasks 상태와 디스패치 함수를 컨텍스트에 넣을 수도 있습니다. 이렇게 하면 TaskApp 아래에 있는 트리의 어떤 컴포넌트든 반복적인 "프롭 드릴링" 없이 tasks를 읽거나 액션을 디스패치할 수 있게 됩니다.

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

  1. 컨텍스트를 생성합니다.
  2. 상태와 디스패치를 컨텍스트에 넣습니다.
  3. 트리의 어디서든 컨텍스트를 사용합니다.

1단계: 컨텍스트 생성

useReducer 훅은 현재 tasks와 업데이트할 수 있는 dispatch 함수를 반환합니다.

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

트리를 통해 이들을 전달하기 위해 두 개의 별도 컨텍스트를 생성합니다.

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

다른 파일에서 이를 가져올 수 있도록 내보냅니다.

Edit unruffled-cookies-yfomxd

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

2단계: 상태와 디스패치 함수를 컨텍스트에 넣기

이제 TaskApp 컴포넌트에서 이 두 컨텍스트를 가져올 수 있습니다. useReducer()에서 반환된 tasks와 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>
  );
}

현재로서 정보를 프롭스와 컨텍스트를 통해 모두 전달하고 있습니다.

Edit condescending-brattain-gbluz5

다음 단계에서는 프롭스 전달을 제거할 것입니다.

3단계: 트리의 어디서든 컨텍스트 사용하기

이제 tasks 목록이나 이벤트 핸들러를 트리를 통해 전달할 필요가 없습니다.

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

대신, tasks 목록이 필요한 어떤 컴포넌트든 TaskContext에서 읽을 수 있습니다.

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

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

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 컴포넌트에 이벤트 핸들러를 전달하지 않습니다. 각 컴포넌트는 필요한 컨텍스트를 읽습니다.

Edit flamboyant-pasteur-v4f3rx

상태는 여전히 최상위 TaskApp 컴포넌트에 "살아있으며" useReducer로 관리됩니다. 그러나 tasks와 dispatch는 이제 상태를 사용하는 트리의 모든 컴포넌트에서 가져와 사용할 수 있도록 컨텍스트를 가져와 사용함으로써 이루어집니다.

모든 연결 작업을 단일 파일로 이동하기

이는 필수적인 작업은 아니지만, reducer와 context를 동일한 파일로 이동하여 컴포넌트를 정리할 수도 있습니다. 현재 TasksContext.js 파일에는 두 개의 컨텍스트 선언만 포함되어 있습니다.

import { createContext } from 'react';

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

이 파일이 혼잡해질 예정입니다! 리듀서도 동일한 파일로 이동시킬 것입니다. 그런 다음 동일한 파일에서 TasksProvider 컴포넌트를 선언합니다. 이 컴포넌트는 모든 요소를 함께 연결할 것입니다.

  1. 리듀서를 사용하여 상태를 관리합니다.
  2. 하위 컴포넌트에 두 개의 컨텍스트를 제공합니다.
  3. JSX를 전달할 수 있도록 children으로 가져옵니다.
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 컴포넌트에서 모든 복잡한 작업과 연결이 제거되었습니다.

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

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

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

컴포넌트가 컨텍스트를 읽기 위해 이러한 함수를 사용할 수 있습니다.

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

이로써 동작은 어떠한 변경도 없지만, 나중에 이러한 컨텍스트를 더 분리하거나 이러한 함수에 어떤 로직을 추가할 수 있게 됩니다. 이제 모든 컨텍스트와 리듀서 연결 작업이 TasksContext.js에 있습니다. 이를 통해 컴포넌트를 깔끔하게 유지하고 데이터를 어디에서 가져오는지가 아닌 어떤 내용을 표시하는 데 집중할 수 있게 됩니다.

Edit divine-snowflake-2ro1iu

TasksProvider는 tasks를 다루는 화면의 일부로, useTasks는 해당 tasks를 읽는 방법으로, useTasksDispatch는 트리 아래의 어떤 컴포넌트에서든 업데이트하는 방법으로 간주할 수 있습니다.

참고
useTasks와 useTasksDispatch와 같은 함수는 사용자 정의 훅(Custom Hooks)이라고 부릅니다. 이러한 함수는 use로 시작하는 경우 사용자 정의 훅으로 간주됩니다. 이를 통해 useContext와 같은 다른 훅을 내부에서 사용할 수 있습니다.

앱이 커짐에 따라 이와 유사한 많은 컨텍스트-리듀서 쌍이 있을 수 있습니다. 이는 트리 깊은 곳에서 데이터에 액세스하려고 할 때마다 상태를 스케일 업하고 상태를 공유하는 강력한 방법입니다.

요약

  • 리듀서와 컨텍스트를 결합하여 트리 위에서 상태를 읽고 업데이트할 수 있는 모든 컴포넌트를 허용할 수 있습니다.
  • 하위 컴포넌트에 상태와 디스패치 함수를 제공하기 위해:
    1. 두 개의 컨텍스트를 생성합니다 (상태를 위한 컨텍스트와 디스패치 함수를 위한 컨텍스트).
    2. 리듀서를 사용하는 컴포넌트에서 두 컨텍스트를 제공합니다.
    3. 필요한 컴포넌트에서 컨텍스트를 사용합니다.
  • 하나의 파일에 모든 연결 작업을 이동하여 컴포넌트를 정리할 수 있습니다.
    • 컨텍스트를 제공하는 TasksProvider와 같은 컴포넌트를 내보낼 수 있습니다.
    • TasksContext와 useTasks, useTasksDispatch와 같은 사용자 정의 훅을 내보낼 수도 있습니다.
  • 앱에 이와 유사한 여러 컨텍스트-리듀서 쌍을 가질 수 있습니다.