
1. Context
Context: 컨포넌트 트리 전체를 대상을 데이터를 공급하는 기능
1. Context를 사용하는 이유
Props Drilling문제를 해결하기 위해
- 리액트 컴포넌트 계층 구조에서 컴포넌트 간에 값을 전달할 때 발생합니다.
- 컨포넌트 간에 데이터를 전달할때 Props를 사용합니다. (부모 -> 자식: 단방향)
- 컨포넌트 사이의 데이터 교환구조를 파악하기 어렵게 만듭니다.
- Props를 수정하면 여러 컨포넌트를 살펴봐야 하므로 코드 유지 보수를 어렵게 합니다.

2. Context란?
Context: 같은 문맥 아래에 있는 컴포넌트 그룹에 데이터를 공급하는 기능
- A문장과 B문장이 동일한 문맥 아래에 있다 = A와 B가 동일한 목적(기능)을 가지고 있다.
- Props Drilling문제 해결가능

3. ContextAPI
ContextAPI: Context를 만들고 다루는 리액트 기능입니다.
<Context 만들기>
import React from 'react';
const MyContext = React.createContext(defaultValue);
<Context에 데이터 공급하기>
- Context.Provider기능 사용합니다.
- Provider 컴포넌트는 Props로 공급할 데이터를 받아, 컴포넌트 트리에서 자신보다 하위에 있는 모든 컴포넌트에 데이터를 공급한다.
import React from 'react';
const MyContext = React.createContext(defaultValue);
function App() {
const data = 'data';
return (
<div>
<Header/>
<MyContext.Provider vakue={data}>
<Body/>
</MyContext.Provider>
</div>
);
}
export default App;

<Context가 공급하는 데이터 사용하기>
- useContext를 사용하여 자신이 속한 그룹의 Context가 공급하는 데이터를 불러옵니다.
import React, {useContext} from 'react';
const MyContext = React.createContext(defaultValue);
function App() {
const data = 'data';
return (
<div>
<Header/>
<MyContext.Provider vakue={data}>
<Body/>
</MyContext.Provider>
</div>
);
}
fouction Main() {
const data = useContext(MyContext);
(...)
}
export default App;

2. Context 로 [할 일 관리]앱 리팩토링하기
리팩토링이란? 사용자에게 제공하는 기능은 변경하지 않으면서 내부 구조를 개선하는 작업입니다.
[할 일 관리]앱
- 데이터 전달 구조: State와 Props로만 이루어져 있어 Props Drilling문제 방생
- 개선: Props Drilling을 제거하면서 추가, 수정, 삭제, 검색 기능은 변함 x
1. 어떻게 Context를 적용할지 생각하기


2. TodoCcontext를 만들어 데이터를 공급하기
<TodoContext 만들기>
<데이터 공급하기>
// src/App.js
// 1.2 TodoCcontext를 만들어 데이터를 공급하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import React, { useCallback, useReducer, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function reducer(state, action) {
switch (action.type){
case "CREATE":{
return [action.newItem, ...state]
}
case "UPDATE": {
return state.map((it) =>
it.id === action.targetId
?{
...it,
isDone: !it.isDone,
}
: it
);
}
case "DELETE":{
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
}
const TodoContext = React.createContext();
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
newItem: {
id: idRef.current,
content,
isDone: false,
createDate: new Date().getTime(),
},
})
idRef.current += 1;
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId,
});
},[]);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId,
});
},[]);
return (
<div className="App">
<Header />
<TodoContext.Provider value={{ todo, onCreate, onUpdate, onDelete }}>
<TodoEditor />
<TodoList />
</TodoContext.Provider>
</div>
);
}
export default App;
// 2025-01-08
// src/component/TodoList.js
// 1.2 TodoCcontext를 만들어 데이터를 공급하기
import { useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
const TodoList = ({ todo = [], onUpdate, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
const analyzeTodo = useMemo(() => {
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
}, [todo]);
const {totalCount, doneCount, notDoneCount} = analyzeTodo;
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem
key={it.id}
{...it}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
</div>
);
};
TodoList.defaultProps = {
todo: []
};
export default TodoList;
3. TodoList 컴포넌트에서 Context 데이터 사용하기
(데이터를 꺼내 사용하기)
// src/App.js
export const TodoContext = React.createContext();
// src/component/TodoList.js
import { useContext, useMemo, useState } from "react";
import {TodoContext} from "../App.js";
(...)
const TodoList = () => {
const { todo, onUpdate, onDelete } = useContext(TodoContext);
const storeData = useContext(TodoContext);
(...)
4. TodoItem 컴포넌트에서 Context 데이터 사용하기
- 이전에는 onUpdate와 onDelete를 TodoList에서 받아서 사용한 반면, 이제는 Context에서 직접 불러와 사용할 수 있습니다.
- 함수들을 전달할 필요가 없으므로 Props로 받을 필요가 없습니다.
// src/component/TodoList.js
// 1.4 TodoItem 컴포넌트에서 Context 데이터 사용하기
import { useContext, useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
import {TodoContext} from "../App.js";
const TodoList = () => {
const { todo } = useContext(TodoContext);
const storeData = useContext(TodoContext);
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
const analyzeTodo = useMemo(() => {
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
}, [todo]);
const {totalCount, doneCount, notDoneCount} = analyzeTodo;
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
TodoList.defaultProps = {
todo: []
};
export default TodoList;
// src/component/TodoItem.js
// 1.4 TodoItem 컴포넌트에서 Context 데이터 사용하기
import "./TodoItem.css";
import React, { useContext} from "react";
import {TodoContext} from "../App.js";
const TodoItem = ({ id, content, isDone, createdDate}) => {
const {onUpdate, onDelete} = useContext(TodoContext);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button>
</div>
</div>
);
};
export default React.memo(TodoItem);
5. TodoEditor 컴포넌트에 데이터 공급하기
// 2025-01-08
// src/component/TodoItem.js
// 1.5 TodoEditor 컴포넌트에 데이터 공급하기
import { useContext, useState, useRef } from "react";
import "./TodoEditor.css";
import {TodoContext} from "../App.js";
const TodoEditor = () => {
const { onCreate } = useContext(TodoContext);
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
const onKeyDown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
return(
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
onKeyDown={onKeyDown}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
};
export default TodoEditor;
6. 리팩토링 잘 되었는지 확인하기1
지금까지 모두 오류없이 실행이 되었다면 잘 리팩토링이 되었다.
이제 최적화를 해보겠다.
최적화를 위해 React.memo가 리팩토링 이후에도 제대로 동작하는지 다시 확인해야 합니다.
-> 모든 TodoItem컴포넌트가 리렌더된다.
-> React.memo가 리팩토링 이후 정상적으로 동작하지 않았다.
7. 문제의 원인 찾기
Context 리팩토링 이후 TodoItem 컴포넌트가 불필요한 상황에서도 리렌더 되고 있다는 점입니다.
Props의 Value값이 변경 -> Context.Provider 리렌더

8. 구조 재설계하기
원인: State변수 todo와 onCreate, onUpdate, onDelete와 같은 dispatch관련 함수들이 하나의 객체로 묶여 동일한 Context에 Props로 전달되는 것입니다.
<해결>
- TodoStateContext: todo가 업데이트 되면 영향받는 컴포넌트를 위한 Context
- TodoDispatchContext: dispatch함수 onCreate, onUpdate, onDelete가 변경되면 영향을 받는 컴포넌트를 위한 Context


9. 재설계된 구조로 변경하기
<Context분리하기>
// src/App.js
// 1.9 재설계된 구조로 변경하기
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";
import React, { useMemo, useCallback, useReducer, useRef } from "react";
const mockTodo = [
{
id: 0,
isDone: false,
content: "React 공부하기",
createdDate: new Date().getTime(),
},
{
id: 1,
isDone: false,
content: "빨래 널기",
createdDate: new Date().getTime(),
},
{
id: 2,
isDone: false,
content: "노래 연습하기",
createdDate: new Date().getTime(),
},
];
function reducer(state, action) {
switch (action.type){
case "CREATE":{
return [action.newItem, ...state]
}
case "UPDATE": {
return state.map((it) =>
it.id === action.targetId
?{
...it,
isDone: !it.isDone,
}
: it
);
}
case "DELETE":{
return state.filter((it) => it.id !== action.targetId);
}
default:
return state;
}
}
//export const TodoContext = React.createContext();
export const TodoStateContext = React.createContext();
export const TodoDispatchContext = React.createContext();
function App() {
const [todo, dispatch] = useReducer(reducer, mockTodo);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
newItem: {
id: idRef.current,
content,
isDone: false,
createDate: new Date().getTime(),
},
})
idRef.current += 1;
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId,
});
},[]);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId,
});
},[]);
const memoizedDispatches = useMemo(() => {
return { onCreate, onUpdate, onDelete };
}, []);
return (
<div className="App">
<Header />
<TodoStateContext.Provider value={todo}>
<TodoDispatchContext.Provider value={memoizedDispatches}>
<TodoEditor />
<TodoList />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
</div>
);
}
export default App;
- todo가 변경되어 App컨포넌트를 리렌더라면 TodoDispatchContex 에 Peops를 전달하는 3개의 함수를 다시 생성합니다.
- useMemo를 이용해 TodoDispatchContext.Provider에 전달할 dispatch함수를 다시 생성하지 않도록 만들어야 합니다.
- useCallback을 적용한 함수 onUpdate, onDelete는 다시 생성되지 않으나, Props로 전달하지 위해 묶은 3개의 함수 객체는 다시 생성됩니다. (따라서 useMemo 사용)
<TodoEditor 수정하기>
- TodoStateContext에서 데이터를 받을 필요는 없으며, TodoDispatchContext에서 함수 onCreate만 받으면 됩니다.
// src/component/TodoItem.js
// 1.9 재설계된 구조로 변경하기
import { TodoDispatchContext } from "../App";
import { useContext, useState, useRef } from "react";
import "./TodoEditor.css";
const TodoEditor = () => {
const { onCreate } = useContext(TodoDispatchContext);
const [content, setContent] = useState("");
const inputRef = useRef();
const onChangeContent = (e) => {
setContent(e.target.value);
};
const onSubmit = () => {
if (!content) {
inputRef.current.focus();
return;
}
onCreate(content);
setContent("");
};
const onKeyDown = (e) => {
if (e.keyCode === 13) {
onSubmit();
}
};
return(
<div className="TodoEditor">
<h4>새로운 Todo 작성하기 ✏ </h4>
<div className="editor_wrapper">
<input
ref={inputRef}
value={content}
onChange={onChangeContent}
onKeyDown={onKeyDown}
placeholder="새로운 Todo..."
/>
<button onClick={onSubmit}>추가</button>
</div>
</div>
)
};
export default TodoEditor;
<TodoList 수정하기>
- todo를 TodoStateContext에서 받도록 수정
// src/component/TodoList.js
// 1.9 재설계된 구조로 변경하기
import { useContext, useMemo, useState } from "react";
import TodoItem from "./TodoItem";
import "./TodoList.css";
import {TodoStateContext} from "../App";
const TodoList = () => {
const todo = useContext(TodoStateContext);
const storeData = useContext(TodoStateContext);
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getSearchResult = () => {
return search === ""
? todo
: todo.filter((it) =>
it.content.toLowerCase().includes(search.toLowerCase())
);
};
const analyzeTodo = useMemo(() => {
const totalCount = todo.length;
const doneCount = todo.filter((it) => it.isDone).length;
const notDoneCount = totalCount - doneCount;
return{
totalCount,
doneCount,
notDoneCount,
};
}, [todo]);
const {totalCount, doneCount, notDoneCount} = analyzeTodo;
return (
<div className="TodoList">
<h4>Todo List 🌱</h4>
<div>
<div>총개수: {totalCount}</div>
<div>완료된 할 일: {doneCount}</div>
<div>아직 완료하지 못한 할 일: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
className="searchbar"
placeholder="검색어를 입력하세요"
/>
<div className="list_wrapper">
{getSearchResult().map((it) => (
<TodoItem key={it.id} {...it} />
))}
</div>
</div>
);
};
TodoList.defaultProps = {
todo: []
};
export default TodoList;
<TodoItem 수정하기>
- onUpdate와 onDelete를 TodoDispatchContext에서 해당 함수를 받도록 수정
// src/component/TodoItem.js
// 1.9 재설계된 구조로 변경하기
import "./TodoItem.css";
import React, { useContext} from "react";
import {TodoDispatchContext} from "../App";
const TodoItem = ({ id, content, isDone, createdDate}) => {
console.log(`${id} TodoItem 업데이트`);
const {onUpdate, onDelete} = useContext(TodoDispatchContext);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDelete = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<div className="checkbox_col">
<input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
</div>
<div className="title_col">{content}</div>
<div className="date_col">
{new Date(createdDate).toLocaleDateString()}
</div>
<div className="btn_col">
<button onClick={onClickDelete}>삭제</button>
</div>
</div>
);
};
export default React.memo(TodoItem);

출처 : 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023).
'DEVELOPMENT > react' 카테고리의 다른 글
| [React Native] 리액트 네이티브 개발하기 - 개발환경 세팅 ( 시작하기 ) (0) | 2025.05.06 |
|---|---|
| 14. [감정 일기장] 만들기 1 (0) | 2025.01.13 |
| 12. 최적화 (0) | 2025.01.06 |
| 11장 useReducer와 상태 관리 (0) | 2025.01.06 |
| 10. 할일 관리 앱 만들기 (0) | 2024.12.30 |