Reducer와 Context 활용
- Published on
- 리듀서와 컨텍스트를 결합하여 리액트 앱에서 상태를 관리하고 컴포넌트 간 데이터 공유를 효율적으로 처리하는 방법을 학습합니다.
Table of Contents
리듀서는 컴포넌트의 상태 업데이트 로직을 효율적으로 관리할 수 있게 도와줍니다. 컨텍스트는 정보를 다른 컴포넌트로 깊게 전달할 수 있게 해줍니다. 리듀서와 컨텍스트를 결합하여 복잡한 화면의 상태를 관리할 수 있습니다.
배울 내용
- 리듀서와 컨텍스트를 결합하는 방법
- 상태와 디스패치 함수를 프롭스를 통해 전달하지 않는 방법
- 컨텍스트와 상태 로직을 별도의 파일로 유지하는 방법
리듀서와 컨텍스트를 결합하기
리듀서 소개에서 가져온 이 예제에서 상태는 리듀서에 의해 관리됩니다. 리듀서 함수에는 모든 상태 업데이트 로직이 포함되어 있으며 파일의 맨 아래에 선언되어 있습니다.
리듀서는 이벤트 핸들러를 짧고 간결하게 유지하는 데 도움이 됩니다. 그러나 앱이 커짐에 따라 다른 어려움에 직면할 수도 있습니다. 현재로서 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단계: 컨텍스트 생성
useReducer 훅은 현재 tasks와 업데이트할 수 있는 dispatch 함수를 반환합니다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
트리를 통해 이들을 전달하기 위해 두 개의 별도 컨텍스트를 생성합니다.
- TasksContext는 현재 tasks 목록을 제공합니다.
- TasksDispatchContext는 컴포넌트가 액션을 디스패치할 수 있는 함수를 제공합니다.
다른 파일에서 이를 가져올 수 있도록 내보냅니다.
여기서 두 컨텍스트에 대해 기본값으로 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>
);
}
현재로서 정보를 프롭스와 컨텍스트를 통해 모두 전달하고 있습니다.
다음 단계에서는 프롭스 전달을 제거할 것입니다.
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 컴포넌트에 이벤트 핸들러를 전달하지 않습니다. 각 컴포넌트는 필요한 컨텍스트를 읽습니다.
상태는 여전히 최상위 TaskApp 컴포넌트에 "살아있으며" useReducer로 관리됩니다. 그러나 tasks와 dispatch는 이제 상태를 사용하는 트리의 모든 컴포넌트에서 가져와 사용할 수 있도록 컨텍스트를 가져와 사용함으로써 이루어집니다.
모든 연결 작업을 단일 파일로 이동하기
이는 필수적인 작업은 아니지만, reducer와 context를 동일한 파일로 이동하여 컴포넌트를 정리할 수도 있습니다. 현재 TasksContext.js 파일에는 두 개의 컨텍스트 선언만 포함되어 있습니다.
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
이 파일이 혼잡해질 예정입니다! 리듀서도 동일한 파일로 이동시킬 것입니다. 그런 다음 동일한 파일에서 TasksProvider 컴포넌트를 선언합니다. 이 컴포넌트는 모든 요소를 함께 연결할 것입니다.
- 리듀서를 사용하여 상태를 관리합니다.
- 하위 컴포넌트에 두 개의 컨텍스트를 제공합니다.
- 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에 있습니다. 이를 통해 컴포넌트를 깔끔하게 유지하고 데이터를 어디에서 가져오는지가 아닌 어떤 내용을 표시하는 데 집중할 수 있게 됩니다.
TasksProvider는 tasks를 다루는 화면의 일부로, useTasks는 해당 tasks를 읽는 방법으로, useTasksDispatch는 트리 아래의 어떤 컴포넌트에서든 업데이트하는 방법으로 간주할 수 있습니다.
참고
useTasks와 useTasksDispatch와 같은 함수는 사용자 정의 훅(Custom Hooks)이라고 부릅니다. 이러한 함수는 use로 시작하는 경우 사용자 정의 훅으로 간주됩니다. 이를 통해 useContext와 같은 다른 훅을 내부에서 사용할 수 있습니다.
앱이 커짐에 따라 이와 유사한 많은 컨텍스트-리듀서 쌍이 있을 수 있습니다. 이는 트리 깊은 곳에서 데이터에 액세스하려고 할 때마다 상태를 스케일 업하고 상태를 공유하는 강력한 방법입니다.
요약
- 리듀서와 컨텍스트를 결합하여 트리 위에서 상태를 읽고 업데이트할 수 있는 모든 컴포넌트를 허용할 수 있습니다.
- 하위 컴포넌트에 상태와 디스패치 함수를 제공하기 위해:
- 두 개의 컨텍스트를 생성합니다 (상태를 위한 컨텍스트와 디스패치 함수를 위한 컨텍스트).
- 리듀서를 사용하는 컴포넌트에서 두 컨텍스트를 제공합니다.
- 필요한 컴포넌트에서 컨텍스트를 사용합니다.
- 하나의 파일에 모든 연결 작업을 이동하여 컴포넌트를 정리할 수 있습니다.
- 컨텍스트를 제공하는 TasksProvider와 같은 컴포넌트를 내보낼 수 있습니다.
- TasksContext와 useTasks, useTasksDispatch와 같은 사용자 정의 훅을 내보낼 수도 있습니다.
- 앱에 이와 유사한 여러 컨텍스트-리듀서 쌍을 가질 수 있습니다.