Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen. Reducer를 사용하면 컴포넌트의 state 업데이트 로직을 통합할 수 있습니다. Context를 사용하면 다른 컴포넌트들에 정보를 전달할 수 있습니다. Reducer와 context를 함께 사용하여 복잡한 화면의 state를 관리할 수 있습니다.
You will learn학습 내용
- How to combine a reducer with context
- How to avoid passing state and dispatch through props
- How to keep context and state logic in a separate file
- reducer와 context를 결합하는 방법
- state와 dispatch 함수를 prop으로 전달하지 않는 방법
- context와 state 로직을 별도의 파일에서 관리하는 방법
Combining a reducer with contextreducer와 context를 결합하기
In this example from the introduction to reducers, the state is managed by a reducer. The reducer function contains all of the state update logic and is declared at the bottom of this file: Reducer의 개요에서 reducer로 state를 관리하는 방법에 대해 알아보았습니다. 해당 예시에서 state 업데이트 로직을 모두 포함하는 reducer 함수를 App.js 파일의 맨 아래에 선언했습니다:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty. Currently, the tasks
state and the dispatch
function are only available in the top-level TaskApp
component. To let other components read the list of tasks or change it, you have to explicitly pass down the current state and the event handlers that change it as props.
Reducer를 사용하면 이벤트 핸들러를 간결하고 명확하게 만들 수 있습니다. 그러나 앱이 커질수록 다른 어려움에 부딪힐 수 있습니다. 현재 tasks
state와 dispatch
함수는 최상위 컴포넌트인 TaskBoard
에서만 사용할 수 있습니다. 다른 컴포넌트들에서 tasks의 리스트를 읽고 변경하려면 prop을 통해 현재 state와 state를 변경할 수 있는 이벤트 핸들러를 명시적으로 전달해야 합니다.
For example, TaskApp
passes a list of tasks and the event handlers to TaskList
:
예를 들어, 아래 TaskApp
컴포넌트에서 TaskList
컴포넌트로 task 리스트와 이벤트 핸들러를 전달합니다:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
And TaskList
passes the event handlers to Task
:
그리고 TaskList
컴포넌트에서 Task
컴포넌트로 이벤트 핸들러를 전달합니다:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
In a small example like this, this works well, but if you have tens or hundreds of components in the middle, passing down all state and functions can be quite frustrating! 지금처럼 간단한 예시에서는 잘 동작하지만, 수십 수백 개의 컴포넌트를 거쳐 state나 함수를 전달하기는 쉽지 않습니다!
This is why, as an alternative to passing them through props, you might want to put both the tasks
state and the dispatch
function into context. This way, any component below TaskApp
in the tree can read the tasks and dispatch actions without the repetitive “prop drilling”.
그래서 tasks
state와 dispatch
함수를 props를 통해 전달하는 대신 context에 넣어서 사용하고 싶을 겁니다. 그러면 반복적인 “prop drilling” 없이 TaskBoard
아래의 모든 컴포넌트 트리에서 tasks를 읽고 dispatch 함수를 실행할 수 있습니다.
Here is how you can combine a reducer with context: Reducer와 context를 결합하는 방법은 아래와 같습니다:
- Create the context.
- Put state and dispatch into context.
- Use context anywhere in the tree.
- Context를 생성한다.
- State과 dispatch 함수를 context에 넣는다.
- 트리 안에서 context를 사용한다.
Step 1: Create the contextContext 생성하기
The useReducer
Hook returns the current tasks
and the dispatch
function that lets you update them:
useReducer
훅은 현재 tasks
와 업데이트할 수 있는 dispatch
함수를 반환합니다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
To pass them down the tree, you will create two separate contexts: 트리를 통해 전달하려면, 두 개의 별개의 context를 생성해야 합니다.
TasksContext
provides the current list of tasks.TasksDispatchContext
provides the function that lets components dispatch actions.
- TasksContext는 현재 tasks 리스트를 제공합니다.
TasksDispatchContext
는 컴포넌트에서 action을 dispatch 하는 함수를 제공합니다.
Export them from a separate file so that you can later import them from other files: 두 context는 나중에 다른 파일에서 가져올 수 있도록 별도의 파일에서 내보냅니다.
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Here, you’re passing null
as the default value to both contexts. The actual values will be provided by the TaskApp
component.
두 개의 context에 모두 기본값을 null
로 전달하고 있습니다. 실제 값은 TaskBoard
컴포넌트에서 제공합니다.
Step 2: Put state and dispatch into contextState와 dispatch 함수를 context에 넣기
Now you can import both contexts in your TaskApp
component. Take the tasks
and dispatch
returned by useReducer()
and provide them to the entire tree below:
이제 TaskBoard
컴포넌트에서 두 context를 모두 불러올 수 있습니다. 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>
);
}
For now, you pass the information both via props and in context: 지금은 props와 context를 모두 이용하여 정보를 전달하고 있습니다:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
In the next step, you will remove prop passing. 다음 단계에서 이제 prop을 통한 전달을 제거합니다.
Step 3: Use context anywhere in the tree트리 안에서 context 사용하기
Now you don’t need to pass the list of tasks or the event handlers down the tree: 이제 tasks 리스트나 이벤트 핸들러를 트리 아래로 전달할 필요가 없습니다:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Instead, any component that needs the task list can read it from the TaskContext
:
대신 필요한 컴포넌트에서는 TaskContext
에서 task 리스트를 읽을 수 있습니다:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
To update the task list, any component can read the dispatch
function from context and call it:
task 리스트를 업데이트하기 위해서 컴포넌트에서 context의 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>
// ...
The TaskApp
component does not pass any event handlers down, and the TaskList
does not pass any event handlers to the Task
component either. Each component reads the context that it needs:
TaskBoard
컴포넌트는 자식 컴포넌트에, TaskList
는 Task
컴포넌트에 이벤트 핸들러를 전달하지 않습니다. 각 컴포넌트에서 필요한 context를 읽을 수 있습니다:
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
The state still “lives” in the top-level TaskApp
component, managed with useReducer
. But its tasks
and dispatch
are now available to every component below in the tree by importing and using these contexts.
State와 state를 관리하는 useReducer
는 여전히 최상위 컴포넌트인 TaskBoard
에 있습니다. 그러나 tasks
와 dispatch
는 하위 트리 컴포넌트 어디서나 context를 불러와서 사용할 수 있습니다.
Moving all wiring into a single file하나의 파일로 합치기
You don’t have to do this, but you could further declutter the components by moving both reducer and context into a single file. Currently, TasksContext.js
contains only two context declarations:
반드시 이런 방식으로 작성하지 않아도 되지만, reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리할 수 있습니다. 현재, TasksContext.js
는 두 개의 context만을 선언하고 있습니다:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
This file is about to get crowded! You’ll move the reducer into that same file. Then you’ll declare a new TasksProvider
component in the same file. This component will tie all the pieces together:
이제 이 파일이 좀 더 복잡해질 예정입니다. Reducer를 같은 파일로 옮기고 TasksProvider
컴포넌트를 새로 선언합니다. 이 컴포넌트는 모든 것을 하나로 묶는 역할을 하게 됩니다:
- It will manage the state with a reducer.
- It will provide both contexts to components below.
- It will take
children
as a prop so you can pass JSX to it.
- Reducer로 state를 관리합니다.
- 두 context를 모두 자식 컴포넌트에 제공합니다.
children
을 prop으로 받기 때문에 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>
);
}
This removes all the complexity and wiring from your TaskApp
component:
이렇게 하면 TaskBoard
컴포넌트에서 복잡하게 얽혀있던 부분을 깔끔하게 정리할 수 있습니다.
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> ); }
You can also export functions that use the context from TasksContext.js
:
TasksContext.js
에서 context를 사용하기 위한 use 함수들도 내보낼 수 있습니다.
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
When a component needs to read context, it can do it through these functions: 그렇게 만들어진 함수를 사용하여 컴포넌트에서 context를 읽을 수 있습니다.
const tasks = useTasks();
const dispatch = useTasksDispatch();
This doesn’t change the behavior in any way, but it lets you later split these contexts further or add some logic to these functions. Now all of the context and reducer wiring is in TasksContext.js
. This keeps the components clean and uncluttered, focused on what they display rather than where they get the data:
이렇게 하면 동작이 바뀌는 건 아니지만, 다음에 context를 더 분리하거나 함수들에 로직을 추가하기 쉬워집니다. 이제 모든 context와 reducer는 TasksContext.js
에 있습니다. 이렇게 컴포넌트들이 데이터를 어디서 가져오는지가 아닌 무엇을 보여줄 것인지에 집중할 수 있도록 깨끗하게 정리할 수 있습니다.
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
You can think of TasksProvider
as a part of the screen that knows how to deal with tasks, useTasks
as a way to read them, and useTasksDispatch
as a way to update them from any component below in the tree.
TasksProvider
는 tasks를 화면의 한 부분으로 tasks를 관리합니다. useTasks
로 tasks를 읽을 수 있고, useTasksDispatch
로 컴포넌트들에서 tasks를 업데이트 할 수 있습니다.
As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app and lift state up without too much work whenever you want to access the data deep in the tree. 앱이 커질수록 context-reducer 조합이 더 많아질 겁니다. 앱을 확장하고 큰 노력 없이 트리 아래에서 데이터에 접근할 수 있도록 state를 끌어올리기 위한 강력한 방법이기 때문입니다.
Recap요약
- You can combine reducer with context to let any component read and update state above it.
- To provide state and the dispatch function to components below:
- Create two contexts (for state and for dispatch functions).
- Provide both contexts from the component that uses the reducer.
- Use either context from components that need to read them.
- You can further declutter the components by moving all wiring into one file.
- You can export a component like
TasksProvider
that provides context. - You can also export custom Hooks like
useTasks
anduseTasksDispatch
to read it.
- You can export a component like
- You can have many context-reducer pairs like this in your app.
- Reducer와 context를 결합해서 컴포넌트가 상위 state를 읽고 수정할 수 있도록 할 수 있습니다.
- State와 dispatch 함수를 자식 컴포넌트들에 제공하는 방법
- 두 개의 context를 만듭니다. (각각 state와 dispatch 함수를 위한 것).
- 하위 컴포넌트들에서 필요한 context를 사용합니다.
- 하위 컴포넌트들에서 필요한 context를 사용합니다.
- 더 나아가 하나의 파일로 합쳐서 컴포넌트들을 정리할 수 있습니다.
- Context를 제공하는
TasksProvider
같은 컴포넌트를 내보낼 수 있습니다. - 바로 사용할 수 있도록
useTasks
와useTasksDispatch
같은 사용자 Hook을 내보낼 수 있습니다.
- Context를 제공하는
- context-reducer 조합을 앱에 여러 개 만들 수 있습니다.