Context를 사용해 데이터를 깊게 전달하기
보통의 경우, 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 정보를 전달
ㄴ 그러나 중간에 많은 컴포넌트를 거쳐야 하거나, 애플리케이션의 많은 컴포넌트에서 동일한 정보가 필요한 경우에는 props를 전달하는 것이 번거롭고 불편할 수 O
Context는 부모 컴포넌트가 깊이에 상관 없이, 트리 아래에 있는 모든 컴포넌트에 정보를 명시적으로 사용할 수 있게 해줌
✔️ Props 전달하기의 문제점
Props 전달하기 : UI 트리를 통해 해당 데이터를 사용하는 컴포넌트에 명시적으로 데이터를 전달하는 방법
Props drilling : 데이터가 필요한 여러 컴포넌트의 가장 가까운 공통 조상이 트리 상 높이 위치하여 높게까지 state를 끌어올리는 것
✔️ Context: Props 전달하기의 대안
Context는 부모 컴포넌트가 그 아래의 트리 전체에 데이터를 전달할 수 있도록 해줌
크기 조정을 위해 level prop을 전달하는 코드
// App.js
import Heading from './Heading.js';
import Section from './Section.js';
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>
);
}
// Section.js
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
// Heading.js
export default function Heading({ level, children }) {
switch (level) {
case 1:
return <h1>{children}</h1>;
case 2:
return <h2>{children}</h2>;
case 3:
return <h3>{children}</h3>;
case 4:
return <h4>{children}</h4>;
case 5:
return <h5>{children}</h5>;
case 6:
return <h6>{children}</h6>;
default:
throw Error('Unknown level: ' + level);
}
}
<Section>
<Heading level={3}>About</Heading>
<Heading level={3}>Photos</Heading>
<Heading level={3}>Videos</Heading>
</Section>
^ 각각의 <Heading>에 level prop을 전달
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
^ <Section> 컴포넌트에 level prop을 전달
위와 같이 변경하려면, <Heading> 컴포넌트가 가장 가까운 <Section>의 레벨을 알아야 함
ㄴ 자식에게 트리 위 어딘가에 있는 데이터를 요청하는 방법이 필요
Props만으로는 불가능 --> Context 활용
- Context 생성
- 데이터가 필요한 컴포넌트에서 context를 사용
- 데이터를 지정하는 컴포넌트에서 context를 제공
* Context는 부모가 트리 내부 전체에, 심지어 멀리 떨어진 컴포넌트에조차 데이터를 제공할 수 있도록 함
✔️ 1단계: Context 생성하기
먼저 context를 생성, 이후 컴포넌트에서 사용할 수 있도록 파일에서 내보내야 함
// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);
createContext의 유일한 인자는 기본값
ㄴ 객체를 포함한 모든 종류의 값 전달 가능
✔️ 2단계: Context 사용하기
React에서 useContext 훅과 생성한 Context를 가져옴
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
지금은 Heading 컴포넌트가 props에서 level을 읽어오지만, level prop을 제거하고 위에서 가져온 context인 LevelContext에서 값을 읽도록 바꿈
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
Heading이 아닌 Section이 대신 level을 받도록 JSX 업데이트
<Section level={4}>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
<Heading>Sub-sub-heading</Heading>
</Section>
✔️ 3단계: Context 제공하기
// Section.js
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext value={level}>
{children}
</LevelContext>
</section>
);
}
이제 level prop을 각 Heading 컴포넌트에 전달할 필요 X
대신 위의 가장 가까운 Section에게 제목 레벨을 확인함
- level prop을 <Section>에 전달
- Section은 자식을 <LevelContext value={level}>로 감싸줌
- Heading은 useContext(LevelContext)를 사용해 가장 근처의 LevelContext의 값을 요청
✔️ 같은 컴포넌트에서 context를 사용하며 제공하기
export default function Page() {
return (
<Section level={1}>
...
<Section level={2}>
...
<Section level={3}>
...
^ 지금은 각각의 섹션에 level을 수동으로 지정해야 함
Context를 통해 위의 컴포넌트에서 정보를 읽을 수 있으므로, 각 Section은 위의 Section에서 level을 읽고 자동으로 level+1을 아래로 전달할 수 있음
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Section({ children }) {
const level = useContext(LevelContext);
return (
<section className="section">
<LevelContext value={level + 1}>
{children}
</LevelContext>
</section>
);
}
✔️ Context로 중간 컴포넌트 지나치기
Context를 사용하면 주변에 적응하고 렌더링되는 위치에 따라 자신을 다르게 표시하는 컴포넌트를 작성 가능
✔️ Context를 사용하기 전에 고려할 것
어떤 props를 여러 레벨 깊이로 전달해야 한다고 해서 해당 정보를 context에 넣어야 하는 것은 아님
context를 사용하기 전 고려해볼 몇 가지 대안
- Props 전달하기로 시작하기
- 사소한 컴포넌트들이 아니라면 여러 개의 props가 여러 컴포넌트를 거쳐 가는 것은 그리 이상한 일이 X
- 데이터의 흐름이 props를 통해 분명해져 코드를 유지보수 하기에도 좋음
- 컴포넌트를 추출하고 JSX를 children으로 전달하기
- 컴포넌트를 추출하는 것을 잊은 겨웅 多
- ex) posts처럼 직접 사용하지 않는 props를 <Layout posts={posts} />와 같이 전달 가능
✔️ Context 사용 예시
- 테마 지정하기
- 사용자가 모양을 변경할 수 있는 애플리케이션의 경우에 context provider를 앱 최상단에 두고 시각적으로 조정이 필요한 컴포넌트에서 context를 사용할 수 있음
- 현재 계정
- 로그인한 사용자를 알아야 하는 컴포넌트가 많을 수 있음
- Context에 놓으면 트리 어디에서나 편하게 알아낼 수 있음
- 라우팅
- 대부분의 라우팅 솔루션은 현재 경로를 유지하기 위해 내부적으로 context를 사용
- 이것이 모든 링크의 활성화 여부를 알 수 있는 방법
- 상태 관리
- 애플리케이션이 커지면 결국 앱 상단에 수많은 state가 생기게 됨
- reducer를 context와 함께 사용하는 것은 복잡한 state를 관리하고 번거로운 작업 없이 멀리 있는 컴포넌트까지 값을 전달하는 방법
Reducer와 Context로 앱 확장하기
Reducer를 사용하면 컴포넌트의 state 업데이트 로직 통합 가능
Context를 사용하면 다른 컴포넌트들에 정보 전달 가능
Reducer와 context를 함께 사용하면 복잡한 화면의 state를 관리 가능
✔️ Reducer와 context를 결합하기
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 }
];
// AppTask.js
import { useState } from 'react';
export default function AddTask({ onAddTask }) {
const [text, setText] = useState('');
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
onAddTask(text);
}}>Add</button>
</>
)
}
// TaskList.js
import { useState } from 'react';
export default function TaskList({
tasks,
onChangeTask,
onDeleteTask
}) {
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
</li>
))}
</ul>
);
}
function Task({ task, onChange, onDelete }) {
const [isEditing, setIsEditing] = useState(false);
let taskContent;
if (isEditing) {
taskContent = (
<>
<input
value={task.text}
onChange={e => {
onChange({
...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 => {
onChange({
...task,
done: e.target.checked
});
}}
/>
{taskContent}
<button onClick={() => onDelete(task.id)}>
Delete
</button>
</label>
);
}
^ 현재 task state 및 dispatch 함수는 최상위 TaskApp 컴포넌트에서만 사용 가능
ㄴ 다른 컴포넌트가 작업 목록을 읽거나 변경하려면 현재 state와 해당 state를 변경하는 이벤트 핸들러를 명시적으로 props로 전달해야 함
Reducer와 context를 결합하는 방법
- Context를 생성
- State와 dispatch 함수를 context에 넣음
- 트리 안에서 context를 사용
✔️ 1단계: Context 생성
useReducer 훅은 현재 tasks와 업데이트할 수 있는 dispatch 함수를 반환
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
트리를 통해 전달하려면, 두 개의 별개의 context를 생성해야 함
- TaskContext는 현재 tasks 리스트를 제공
- TaskDispatchContext는 컴포넌트에서 action을 dispatch하는 함수를 제공
// TasksContext.js
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
^ 두 context에 모두 기본값으로 null을 전달, 실제 값은 TaskApp 컴포넌트에서 제공될 것
✔️ 2단계: State와 dispatch 함수를 context에 넣기
이제 TaskApp 컴포넌트에서 두 context를 모두 가져올 수 있음
useReducer()에서 반환된 tasks 및 dispatch를 가져와 아래 트리 전체에 제공
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
}
✔️ 3단계: 트리 안에서 context 사용하기
// TaskList.js
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>
);
}
✔️ 하나의 파일로 합치기
반드시 이런 방식으로 작성하지 않아도 되지만, reducer와 context를 모두 하나의 파일에 작성하면 컴포넌트들을 조금 더 정리 가능
Reducer를 같은 파일로 옮기고 TasksProvider 컴포넌트를 새로 선언
ㄴ 해당 컴포넌트는 모든 것을 하나로 묶는 역할을 함
- Reducer로 state를 관리
- 두 context를 모두 하위 컴포넌트에 제공
- children을 prop으로 받기 때문에 JSX를 전달 가능
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
TasksContext.js에서 context를 사용하기 위한 use 함수들도 내보낼 수 있음
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
이 함수를 사용하여 컴포넌트에서 context를 읽을 수 있음
const tasks = useTasks();
const dispatch = useTasksDispatch();
TasksProvider는 tasks를 화면의 한 부분으로 tasks를 관리
useTasks로 tasks를 읽을 수 있고, useTasksDispatch로 컴포넌트들에서 tasks를 업데이트할 수 있음
'React > 공식 문서' 카테고리의 다른 글
| useReducer (0) | 2026.05.21 |
|---|---|
| 상태 구조 설계와 공유 (0) | 2026.05.07 |
| 불변 업데이트 패턴 (0) | 2026.04.28 |
| 렌더링과 State 업데이트 (0) | 2026.04.08 |
| 이벤트 처리와 State (0) | 2026.04.02 |