둘을 같이 쓰면 무적의 state 관리자가 되겠지요?
이 문서에서는
- reducer와 context 같이 쓰기
- props로 state 넘기고 dispatch 하지 않는 법
- 관심사의 분리 : context랑 state logic을 다른 파일에서 관리하는 법
을 배워보겠다.
이전 문서 - Reducer에 State 로직 추출하기 에서는 state를 reducer로 관리했다. reducer 함수에는 state 업데이트 로직이 모두 들어있고, App.js 맨 밑에서 선언했다.
코드 임포트가 어려워 공식문서를 링크로 걸어놓겠다.
공식문서 - Scaling Up with Reducer and Context
Reducer 가 있어 이벤트 핸들러 함수가 간결해졌다. 그러나, 어플리케이션이 커지면 또 다른 문제점이 생김.
tasks
state와 dispatch
함수가 최상단 TaskApp
컴포넌트에서만 사용 가능하다는 것!하위의 다른 컴포넌트들이 이 state를 읽어오고 싶거나 변경하고 싶다면? prop으로 dispatch
함수랑 tasks
state를 쭈우우욱 내려줘야 한다는 말이다. 귀찮겠지?
해결책은 바로 context. TaskApp
컴포넌트 하위의 모든 녀석들에서 반복적인 prop drilling (props 불필요하게 쭈우우우욱 내려주기) 없이 이 state를 마음껏 불러오고 변경할 수 있게 된다.
reducer를 context와 함께 쓰려면 이렇게 하자.
useReducer
훅을 쓰자. 현재 상태 tasks
와 dispatch
함수를 리턴해준다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
이것들을 트리 하위로 내려주려면, Context를 두개 만들어야 함.
TasksContext
: 현재 tasks
state 리스트를 하위에 제공TasksDispatchContext
: dispatch
함수를 하위에 제공Context는 별도 파일로 만들어라 - 그래야 import 하기 쉬워짐.
// TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
두 context 모두 null
을 default 값으로 넣었다. 실제 값은 TaskApp
컴포넌트에서 줄거임.
TaskApp
컴포넌트에서 두 Context를 임포트해올 수 있다. useReducer()
에서 리턴되는 tasks
, dispatch
를 하위 트리에 제공하자. provide
// TaskApp.js
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default const TaskApp = () {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ... 사실 이까지는 기존 reducer 로직. initalTasks는 맨 밑에 선언돼있었다.
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value = {dispatch}>
// ... 어쩌구저쩌구 children들
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
이제 ... 어쩌구저쩌구 childern들
자리에 들어오는 하위 트리의 컴포넌트들이 모두 tasks
와 dispatch
함수를 사용할 수 있게 됨.
Context 적용 전 기존 코드 :
// App.js
return (
<AddTask
onAddTask={handleAddTask}
/> // props로 dispatch 함수를 넘겨주고 있다.
<TaskList
tasks={tasks} // props로 tasks state를 넘겨주고 있다.
onChangeTask={handleChangeTask} // props로 dispatch 함수를 넘겨주고 있다2.
onDeleteTask={handleDeleteTask} // props로 dispatch 함수를 넘겨주고 있다3.
/>
);
context 적용 후 코드 :
// App.js
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask /> // props 가 빠졌다. Provider에서 불러올 수 있기 때문
<TaskList /> // props 가 빠졌다. Provider에서 불러올 수 있기 때문2
</TasksDispatchContext.Provider>
</TasksContext.Provider>
// TaskList.js
export default function TaskList() {
const tasks = useContext(TasksContext); // useContext 훅으로 tasks state를 불러오고 있다.
// ...
// AddTask.js
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext); // useContext 훅으로 dispatch를 불러오고 있다.
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...
TaskApp
컴포넌트에 "살고 있다" - useReducer
의 관리를 받을 뿐.그러나 Context를 사용해서 하위 어디서나 접근할 수 있는 것.
굳이 할 필요는 없는 작업이긴 하다만, reducer 와 context를 한 파일에 때려박으면 코드 보기가 조금 더 편해진다. 현재 TasksContext.js
에는 Context 선언문 두개밖에 없지만... 점점 많아질 것이다.
TasksProvider
컴포넌트를 여기서 선언하자. 이 컴포넌트는 모든 조각을 잇는다 childeren
넣어서 JSX를 받을 수 있음.// TasksContext.js -> TasksProvider.js
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
컴포넌트의 복잡한 전선들이 사라진다.
// TaskApp.js
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>
);
}
우리가 만든 Context를 use 사용 하는 함수를 export할 수 있다.
export funciton useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
컴포넌트가 context를 읽어올 필요가 있을 때, 이렇게 넘겨주면 된다:
const tasks = useTasks();
const dispatch = useTasksDispatch();
파일 단위로 관심사의 분리가 이뤄진다 :
TasksProvider
: tasks를 다루기useTasks
: tasks를 읽어오기useTasksDispatch
: 하위 트리 어디서든 tasks 업데이트하기앱의 사이즈가 커지면서, 이렇게 reducer - context 콤보를 사용하는 경우가 많아질 것이다. 이렇게 하면 상태 끌어올리기를 너무 과도하게 하는 - props drilling을 안하게 될 수 있을 것이다~
TasksProvider
같은 provider 컴포넌트를 exportuseTasks
나 useTasksDispatch
같은 custom hook을 export