https://react.dev/learn/scaling-up-with-reducer-and-context를 정리한다.

 

Reducer와 Context를 같이 사용하여 복잡한 화면의 상태를 관리할 수 있다.

  • Reducer를 사용하면 컴포넌트의 state 업데이트 logic을 통합할 수 있다.
  • context를 사용하면 정보를 다른 컴포넌트로 deep하게 전달할 수 있다.

앱이 성장함에 따라 이와 같은 context-reducer 쌍이 많이 있을 수 있다. 이는 트리의 깊숙한 데이터에 액세스하려고 할 때마다 너무 많은 작업 없이 앱을 확장하고 state를 위로 올릴 수 있는 강력한 방법이다.

Reducer와 Context 같이 사용하기

Reducer 소개 단원 예제에서 state는 reducer로 관리되고 있다. reducer 함수는 모든 state update logic을 포함한다.

reducer는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 된다. 그러나 앱이 성장함에 따라 다른 어려움이 생길 수 있다. "현재 tasks state와 dispatch function은 최상위 컴포넌트(TaskApp)에서만 사용할 수 있다".

다른 컴포넌트가 task 목록을 읽거나 변경할 수 있도록 하려면 현재 상태와 이를 변경하는 이벤트 핸들러를 props을 사용해 명시적으로 전달해야한다.

// TaskApp은 task 목록과 이벤트 핸들러를 TaskList로 전달한다.
<TaskList
  tasks={tasks}
  onChangeTask={handleChangeTask}
  onDeleteTask={handleDeleteTask}
/>

// TaskList는 Task에 이벤트 핸들러를 전달한다.
<Task
  task={task}
  onChange={onChangeTask}
  onDelete={onDeleteTask}
/>

위의 예제는 잘 돌아가지만 중간에 많은 수의 컴포넌트가 있을 경우, 모든 state와 function을 아래로 전달해야하고 이는 별로다!

이를 개선하기 위해 tasks state와 dispatch함수를 모두 컨텍스트에 넣을 수 있다.  이 방법은 트리에서 TaskApp아래의 모든 컴포넌트가 반복적인 "prop drilling" 없이 tasks을 읽고 action을 dispatch할 수 있습니다.

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

 

1. context를 만든다.

`useReducer` Hook은 현재 `tasks`와 이를 업데이트 할 수 있는 `dispatch` 함수를 반환한다.

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

트리 아래로 반환된 값을 전달하기 위해, 두개의 분리된 context를 생성 후 다른 파일에서 import할 수 있게 export한다.

  • `TasksContext` : 현재 task 목록을 제공
  • `TastsDispatchContext` : 컴포넌트가 dispatch action을 할 수 있는 함수를 제공
// TasksContext.js
import { createContext } from 'react';

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

현재 두 context는 default value로 `null`을 전달하고 있다. 실제 값은 `TaskApp` 컴포넌트에게 제공받을 것이다.

2. statue와 dispatch를 ​​context에 넣는다.

`TaskApp` 컴포넌트에서 두 context를 import한 후, `useReducer()`에서 반환된 `tasks`와 `dispatch`를 context에 제공한다.

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, context)에서 정보를 전달하고 있다. 다음 차례에 props를 제거하자. 

3. 트리의 아무 곳에서나 context를 사용한다.

이제 task 목록과 이벤트 핸들러를 아래로 내리지 않아도 된다.

대신 `TaskContext`를 통해 어떤 컴포넌트든 task 목록이 필요하면 읽을 수 있다.

const tasks = useContext(TasksContext);

또한, `TaskDispatchContext`를 통해 task 목록을 업데이트 하기위해 어떤 컴포넌트든 `dispatch` 함수를 읽을 수 있다. 

const dispatch = useContext(TasksDispatchContext);

위의 모든 단계를 적용하면 아래와 같다.

state는 여전히  top-level(TaskApp) 컴포넌트에 존재하고, `useReducer`로 관리된다. 그러나 `tasks`, `dispatch`는 context를 사용해 아래의 모든 컴포넌트에서 사용할 수 있다.

 

모든 것을 하나의 파일로 옮기기

이렇게 할 필요는 없지만 reducer와 context를 단일 파일로 이동하여 컴포넌트를 더 깔끔하게 정리할 수 있습니다.

현재 TasksContext.js는 두 개의 컨텍스트 선언만 포함하고 있다.

import { createContext } from 'react';

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

reducer를 동일한 파일로 이동한 후, 새로운 `TasksProvider` 컴포넌트를 선언한다. 이 컴포넌트는 모든 조각을 함께 묶는다.

  1. reducer로 상태를 관리한다.
  2. 아래 구성 요소에 두 context를 모두 제공한다.
  3. JSX 를 전달할 수 있도록 prop으로 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` 컴포넌트에 있는 복잡성과 wiring을 제거할 수 있다.(아래에서 App.js, TasksContext.js를 확인해보라)

 

추가로, TasksContext.js에서 context를 사용하는 함수를 export할 수 있다.

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

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

컴포넌트가 context를 읽어야할 때, 이 함수를 사용할 수 있다.

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

동작이 절대 변경되지 않지만, 나중에 이런 context를 더 분할하거나 일부 logic를 추가할 수 있다. 이제 모든 context 및 reducer wiring이 TasksContext.js에 있다. 이렇게 하면 데이터를 가져오는 위치보다 표시되는 내용에 집중하여 컴포넌트를 깨끗하고 깔끔하게 유지한다.(TaskList 참고)

결과적으로 아래처럼 분리해서 생각할 수 있다.

  • `TasksProvider` : tasks를 다루는 방법을 아는 part of the screen
  • `useTasks` : tasks를 읽는 방법
  • `useTasksDispatch` : tasks를 변경하는 방법
`userTask`, `userTasksDispatch` 함수는 커스텀 훅이라고 불린다. 이름이 use로 시작하는 함수는 커스텀 훅이다. 이렇게 하면 내부에서 다른 `useContext`와 같은 다른 훅을 사용할 수 있다.

+ Recent posts