리듀서는 컴포넌트의 상태 업데이트 로직을 통합시켜줘요. 컨텍스트는 정보를 다른 컴포넌트까지 전달해줘요. 여러분은 복잡한 화면의 상태를 관리하기 위해 리듀서와 컨텍스트를 함께 사용할 수 있어요.
이 페이지에서는
- 리듀서를 컨텍스트와 어떻게 결합하는지
- props를 통해 상태와 디스패치를 전달하지 않는 방법은 무엇인지
- 어떻게 컨텍스트와 상태 로직을 각각 분리된 파일에 넣는지
를 알아볼 거예요.
Combining a reducer with context | 컨텍스트와 리듀서 결합하기
리듀서를 소개한 문서에서 나왔던 이 예시에서 상태는 리듀서로 관리돼요. 리듀서 함수는 모든 상태 업데이트 로직을 포함하고 이 파일의 가장 아래에서 선언되었어요.
리듀서는 이베느 헨들러가 더 짧고 간결해지도록 만들어줘요. 그러나 앱이 커지면 또 다른 어려움이 생겨요. 현재, task
상태와 dispatch
함수는 최상위 컴포넌트인 TaskApp
에서만 사용할 수 있어요. 다른 컴포넌트가 할 일 목록을 읽고 수정할 수 있도록 하려면 명시적으로 현재 상태와 props로 상태를 변경하는 이벤트 핸들러를 내려주어야해요.
예를 들어, TaskApp
은 할 일의 목록과 이벤트 핸들러를 TaskList
에 전달해요.
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
그리고 TaskList
는 Task
에 이벤트 핸들러를 전달해요.
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
이와 같이 작은 예시에서는 잘 작동하지만 만약 중간에 수십, 수백개의 컨포넌트가 있다면 모든 상태와 함수를 내리는 것은 꽤나 무서울 거예요!
이 떄문에 이들을 props로 내리는 대신 task
상태와 dispatch
함수를 모두 컨텍스트에 넣을 수 있어요. 이 방법으로 트리 안에서 TaskApp
의 하위에 있는 그 어떤 컴포넌트도 할 일을 읽을 수 있고 반복적인 "props drilling" 없이 액션을 디스패치 할 수 있어요.
컨텍스트와 리듀서를 결합하는 방법은 아래와 같아요.
- 컨텍스트를 생성하세요.
- 상태와 디스패치를 컨텍스트 안에 넣으세요.
- 트리의 어디에서든 컨텍스트를 사용하세요.
Step 1: Create the context | 1단계: 컨텍스트 생성하기
useReducer
훅은 현재 tasks
와 이 상태들을 업데이트해주는 dispatch
함수를 반환해요.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
이들을 트리의 안으로 내리기 위해서는 두 가지 별개의 컨텍스트를 생성하세요.
TasksContext
는 할 일의 현재 목록을 제공해요.TasksDispatchContext
는 컴포넌트의 디스패치 액션을 수행하는 함수를 제공해요.
나중에 다른 파일에서 이들을 불러올 수 있도록 각 파일에서 이들을 추출하세요.
이 예시에서 컨텍스트에 기본값으로 null
을 전달했어요. 실제 값은 TaskApp
컴포넌트에서 제공될 거예요.
Step 2: Put state and dispatch into context | 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>
);
}
이제 props와 컨텍스트를 둘 다 사용하여 정보를 전달하세요.
다음 단계에서 여러분은 prop 전달을 제거할 거예요.
Step 3: Use context anywhere in the tree | 3단계: 트리의 어느곳에서든 컨텍스트 사용하기
이제 여러분은 할일의 목록이나 이벤트 핸들러를 트리 아래로 내릴 필요가 없어요.
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
이 대신, 할 일 목록이 필요한 컴포넌트는 TaskContext
에서 이를 읽어올 수 있어요.
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
}
할일 목록을 업데이트하려면 컨텍스트에서 dispatch
함수를 읽고 이를 호출하세요.
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
컴포넌트로 전달하지 않아요. 각 컴포넌트는 필요한 컨텍스트를 읽어요.
상태는 여전히 useReducer
로 관리되며 최상위 컴포넌트인 TaskApp
에서 살아잇어요. 그러나 tasks
와 dispatch
는 이제 이 컨텍스트를 불러와서 사용하여 하위 트리에 있는 모든 컴포넌트에서 사용할 수 있어요.
Moving all wiring into a single file | 하나의 파일로 모든 연결을 옮기기
이 작업을 꼭 할 필요는 없지만 하나의 파일로 리듀서와 컨텍스트를 모두 옮기면 더 깔끔하게 컴포넌트를 정리할 수 있어요. 현재 TaskContext.js
는 단 두 개의 컨텍스트 선언만을 갖고 있어요.
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
이 파일은 더 꽉꽉 채워질 거예요! 같은 파일로 리듀서를 옮길 거예요. 그리고나면 새로운 TaskProvider
컴포넌트를 같은 파일에서 선언할 거예요. 이 컴포넌트는 모든 조각을 하나로 묶어주는 역할을 해요.
- 여기서 리듀서로 상태를 관리할 거예요.
- 여기서 하위 컴포넌트에 두 컨텍스트를 모두 제공할 거예요.
- 여기서 props으로
children
을 받아서 JSX로 이를 내릴 거예요.
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();
이런 방법이 코드의 작동을 바꾸지 않아요. 하지만 나중에 이 컨텍스트들을 더 분리하거나 이 함수에 로직을 더 추가할 수 있도록 만들어줘요. 이제 모든 컨텍스트와 리듀서 연결은 TaskContext.js
에 있어요. 이렇게 하면 컴포넌트는 깨끗하고 어수선하지 않으며 어디서 데이터를 가져오는지가 아니라 어떤 데이터를 보여줄지에 더 집중하게 돼요.
TasksProvider
는 한 일을 어떻게 다룰지를 알고 있는 화면의 일부이며, useTasks
는 할 일을 읽는 방법이고, useTasksDispatch
는 하위 트리에 있는 컴포넌트에서 할 일을 업데이트 해주는 방법이라고 생각할 수 있어요.
노트useTasks
나useTasksDispatch
와 같은 함수를 커스텀 훅이라고 불러요. 함수의 이름이use
로 시작하면 함수는 커스텀훅으로 여겨져요. 커스텀 훅 안에서는useContext
와 같은 다른 훅을 사용할 수 있어요.
앱이 커지면서 이런 컨텍스트-리듀서 패턴을 많이 사용할 거예요. 앱의 규모를 키우고 트리의 깊은 곳에서 데이터에 접근할 때마다 너무 많은 작업을 하지 않고 상태를 끌어올려주는 강력한 방법이에요.
Recap | 요약
- 상위의 컴포넌트에서 상태를 읽고 업데이트 하기 위해 리듀서를 컨텍스트와 함께 사용할 수 있어요.
- 상태와 디스패치 함수를 하위 컴포넌트에 제공하기 위해서는
- (상태와 디스패치 함수를 위한) 두 개의 컨텍스트를 생성하세요.
- 리듀서를 사용하는 컴포넌트로부터 이 컨텍스트들을 제공하세요.
- 이들을 읽어야하는 컴포넌트에서 컨텍스트를 사용하세요.
- 하나의 파일로 모든 연결을 옮기면 컴포넌트를 더 깔끔히 정리할 수 있어요.
-
- 컨텍스트를 제공하는 TasksProvider처럼 컴포넌트를 추출할 수 있어요.
- 컨텍스트를 읽기 위해 useTasks나 useTasksDispatch와 같은 커스텀 훅을 추출할 수 있어요
- 앱에서 이와 같은 컨텍스트-리듀서 패턴을 많이 사용할 수 있어요.