- Published on
리액트 공식문서 스터디 6-2 주차
- Authors

- Name
- junyeol kim
Passing Data Deeply with Context
부모에서 자식으로 값을 전달할 때 props는 가장 확실하고 효과적인 방법이다.
하지만 여러 단계를 거치거나 여러 컴포넌트가 동일한 값을 필요로하면 중간 컴포넌트들이 실제로 사용하지 않는 props를 전달하게 되고, 이 과정에서 prop drilling이 발생할 수 있다.
이러한 prop drilling을 방지하는 방법 중 하나로 Context를 사용할 수 있다.
Context는 부모 컴포넌트가 자신의 하위 tree 전체에 데이터를 제공할 수 있게 해준다.
- 예시를 한 번 봐보자
export default function Page() { return ( <Section> <Heading level={1}>Title</Heading> <Section> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Heading level={2}>Heading</Heading> <Section> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Heading level={3}>Sub-heading</Heading> <Section> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> <Heading level={4}>Sub-sub-heading</Heading> </Section> </Section> </Section> </Section> ) }- 현재 코드에서는 Heading 컴포넌트에 level props를 일일이 전달하고 있다.
<Section> <Heading level={3}>About</Heading> <Heading level={3}>Photos</Heading> <Heading level={3}>Videos</Heading> </Section>첫 번째 방식보다는 Section 컴포넌트에 level을 한 번만 전달하면 더 효율적일 것이다.
그렇다면 Heading 컴포넌트가 Section 컴포넌트의 level 값을 어떻게 알 수 있을까?
이런 작은 props만으로는 할 수 없기에 자식이 tree 위쪽에서 데이터를 가져올 수 있도록 도와주는 Context가 등장한 것이다.
Context를 작성하는 방법
- Create the context
import { createContext } from 'react';
export const LevelContext = createContext(1)); // default value로 value or object 모두 가능
- Use the context
import { useContext } from "react";
import { LevelContext } from "./LevelContext.js";
export default function Heading({ children }) {
const level = useContext(LevelContext);
...
}
- 기존에 props로 전달하던 level 값을 useContext Hook을 통해 직접 가져오게 된다.
- Provide the context
import { LevelContext } from './LevelContext.js'
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>{children}</LevelContext.Provider>
</section>
)
}
Section 컴포넌트는 이제 전달받은 level 값을 LevelContext.Provider을 통해 하위 컴포넌트에 제공한다.
이렇게 하면 Heading 컴포넌트는 useContext(LevelContext)를 통해 가장 가까운 Provider이 제공하는 level 값을 읽어온다.
Context는 가까운 부모가 제공한 값을 읽을 수 있는데, 그 값을 다시 제공하면 트리의 깊이에 따라 값이 자연스럽게 변화한다.
Context를 사용하기전 고려해야 할 것
- props로만 해결이 가능한지
- children 패턴을 사용하기
- 그래도 안된다면 context를 사용하기
Context를 언제 사용하면 좋을까?
Theming : 앱 전체에서 같은 테마 정보를 쉽게 공유하고 적용할 수 있다.
Current account : 여러 컴포넌트가 현재 로그인한 사용자 정보를 필요로 할 때 한 곳에서 관리할 수 있다.
Routing : Link나 page 컴포넌트들이 현재 어떤 경로인지 알 수 있도록 router가 내부적으로 사용한다.
Managin state : 여러 컴포넌트가 같은 상태를 읽고 변경해야 할 때, reducer + context로 함께 사용할 수 있다.
Scaling Up with Reducer and Context
reducer는 상태 업데이트 로직을 한 곳에서 관리해 컴포넌트를 깔끔하게 만든다.
하지만 상태와 dispatch는 기본적으로 reducer를 사용하는 최상위 컴포넌트에만 존재한다.
다른 컴포넌트가 상태를 읽거나 변경하려면 props로 계속 전달해야해서 prop drilling 문제가 발생한다.
이를 해결하기 위해 상태와 dispatch를 context에 넣으면, 하위 tree 모든 컴포넌트가 props 없이 상태를 읽고 dispatch를 호출할 수 있다.
- Create the context
- 예시코드의 useReducer는 현재 task와 이를 업데이트하는 dispatch 함수를 반환한다.
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
- 이 값들을 하위 tree로 전달하기 위해서는 아래 코드와 같이 두 개의 context를 만들어야 한다.
import { createContext } from 'react'
export const TasksContext = createContext(null)
export const TasksDispatchContext = createContext(null)
- Put state and dispatch into context
앞서 선언한 context 두 개를 TaskApp 컴포넌트에서 사용할 수 있다.
Context를 이용해 useReducer()이 반환하는 tasks와 dispatch를 하위 tree 전체에 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>
)
}
- Use context anywhere in the tree
하위 tree 전체가 context로 전달을 받기 때문에, 이제 더이상 tasks, event handler를 props로 하위 컴포넌트에 전달할 필요가 없다.
필요한 컴포넌트는 useContext()를 사용해 context에서 직접 값을 읽어온다.
import { useContext, useState } from "react";
import { TasksContext, TasksDispatchContext } from "./TasksContext.js";
// tasks 읽기
export default function TaskList() {
const tasks = useContext(TasksContext);
...
}
// dispatch 사용하기
function Task({ task }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useContext(TasksDispatchContext);
...
}
Moving all wiring into a single file
점점 복잡해지는 상태를 관리하기 위해 reducer와 context를 한 파일로 통합하여 관리할 수 있다.
TasksProvider로 컴포넌트를 감싸서 상태와 dispatch를 전역적으로 공유할 수 있다.
이를 통해 하위 어디에서든 useTasks와 useTasksDispatch로 가져와 사용할 수 있다.
import { createContext, useContext, useReducer } from 'react'
const TasksContext = createContext(null)
const TasksDispatchContext = createContext(null)
export function useTasks() {
return useContext(TasksContext)
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext)
}
const initialTasks = [
{ id: 0, text: 'React Docs 읽기', done: false },
{ id: 1, text: 'Context 연습하기', done: false },
]
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) => (t.id === action.task.id ? action.task : t))
case 'deleted':
return tasks.filter((t) => t.id !== action.id)
default:
throw new Error('Unknown action: ' + action.type)
}
}
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks)
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>{children}</TasksDispatchContext.Provider>
</TasksContext.Provider>
)
}