11.1) Context란
Props의 단점
1. Props Drilling

데이터를 가져올 때 중단 다리를 거쳐서 Props를 가져와야함..

만약 서비스의 규모가 커지게 되어서 지금처럼 List컴포넌트와 TodoItem 컴포넌트 사이에 굉장히 많은 컴포넌트들이 존재하게 된다면 이때에는 많은 컴포넌트를 거쳐서 데이터를 전달해야하기 때문에 타이핑해야하는 양이 많아진다.
심지어 Props의 이름이 중간에 바뀌기라도 한다면 모든 컴포넌트를 찾아가서 일일이 이름을 바꿔줘야 한다.
해결방법
React Context
컴포넌트 간의 데이터를 전달하는 또 다른 방법.
기존의 Props가 가지고 있는 단점을 해결할 수 있음.
💡Props의 어떤 단점을 가지고 있길래? => Props Drilling을 해결

Context는 일종의 데이터 보관소 역할을 하는 객체이다.
그래서 Context를 새롭게 생성한 다음에 App 컴퍼넌트에서 원래 자식 컴퍼넌트에세 전달되던 이 ToDos, OnCreate, Onupdate, OnDelete와 같은 함수들을 이 Context에 보관해 놓으면 Props를 이용하지 않고 다이렉트로 Context를 통해서 필요한 데이터를 공급할 수 있다.
그렇게 Props Drilling해결할 수 있다.
Context는 여러개 만드는 것도 가능하다.

이렇게 생긴 컴포넌트 트리가 있을 때, 왼쪽에 있는 자식 컴포넌트들은 A Context의 데이터만 공급받을 수 있도록 설정해줄 수 있다.
반대로 오른쪽에 있는 자식 컴포넌트들은 B Context라는 새로운 컨텍스트를 통해서 데이터를 공급받을 수 있도록 해줄 수 있다.
11.2) Context 사용하기
Context는 보통 컴포넌트 외부에 사용하게 된다.
왜냐하면 App컴포넌트 안쪽에서 Context객체를 생성하게 되면, 문제가 발생하는 건 아니지만,
App컴포넌트가 리렌더링될 때마다 계속해서 Context를 실행하기 때문에서 계속해서 새로운 Context를 생성하게 된다.
그런데 현재 우리의 Context역할은 그냥 데이터를 하위에 있는 컴포넌트들에게 공급만해주면 되는 것이기 때문에
App컴포넌트가 리렌더링 될 때마다 다시 생성될 필요는없다.
그래서 App컴포넌트 외부에 생성한다.

// # APP.js
const TodoContext = createContext(); 👈 createContext 생성(주로 외부에 생성!)
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
}, []);
return (
<div className="App">
<Header />
<TodoContext.Provider value={{ todos, onCreate, onUpdate, onDelete }}>
// 👈 Provider를 사용, value안에 넘겨줄 메소드 적음
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</TodoContext.Provider>
</div>
);
}
export default App;
// # List.js
import { TodoContext } from "../App";
import "./List.css";
import TodoItem from "./TodoItem";
import { useContext, useMemo, useState } from "react";
const List = () => {
const { todos } = useContext(TodoContext); 👈 useContext 사용
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === "") {
return todos;
}
return todos.filter((todo) =>
todo.content.toLowerCase().includes(search.toLowerCase())
);
};
const filteredTodos = getFilteredData();
const getAnalyzedData = () => {
console.log("호출");
const totalCount = todos.length;
const doneCount = todos.filter((todo) => todo.isDone).length;
const notDoneCount = totalCount - doneCount;
return {
totalCount,
doneCount,
notDoneCount,
};
};
const { totalCount, doneCount, notDoneCount } = getAnalyzedData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>
<div>total: {totalCount}</div>
<div>done: {doneCount}</div>
<div>notDone: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"
/>
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return (
<TodoItem
key={todo.id}
{...todo}
// onUpdate={onUpdate} // 👈 TodoItem에서 직접 불러와서 사용할 것이기 때문에, 여기서부터 데이터를 가지고 가지 않아도 된다..
// onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
import { TodoContext } from "../App";
import "./TodoItem.css";
import { memo, useContext } from "react";
const TodoItem = ({ id, isDone, content, date }) => {
const { onUpdate, onDelete } = useContext(TodoContext); 👈 useContext 사용
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem);
11.3) Context 분리하기
이렇게 하면 Context는 잘 작동하지만, 이전에 적용해두었던 최적화가 풀리게 된다.
왜 그럴까?
원인)
Provider도 엄연히 React의 컴포넌트이기 때문에, 앱컴포넌트로부터 value props로 제공받는 todostate와 onCreate, onUpdate, onDelete를 감싸고 있는 객체가 바뀌게 되면(props가 바뀌게 되면), 리렌더링이 발생하게 된다.
TodoContext.Provider가 리렌더링되면서 하위 객체인 Editor, List, TodoItem도 리렌더링 된다.


문제2)
이전에 TodoItem 컴포넌트에 최적화를 적용하기 위해서 Memo메서드를 써서 자신이 받는 props가 바뀌지 않으면 아예 리런더링을 발생시키지 않돌고 설정해둔 적이 있다.
근데 왜 지금은 리렌더링이 발생할까?

이유는 새로운 Todo를 추가하거나 기존 Todo를 수정하거나 삭제할 경우, 아까 말했던 App컴포넌트에 todos가 변경되면서 App 컴포넌트가 리렌더링될 텐데 이때 TodosContext.Provider의 props로 전달하는 객체 자체가 다시 생성되기 때문이다.


memo를 적용했더라도, useContext로부터 불러오는 값이 변경이 되면, 이것은 props가 변경된 것과 동일하게 리렌더링을 발생시키기 때문이다.
해결방법)
이 문제는 TodoContext를 두 개의 Context로 분리해서 해결할 수 있다.

// # App.js
import {
useCallback,
useReducer,
useRef,
useState,
createContext,
useMemo,
} from "react";
import "./App.css";
import Editor from "./components/Editor";
import Header from "./components/Header";
import List from "./components/List";
.. 생략 ...
// export const TodoContext = createContext(); 👈 이전 버전
export const TodoStateContext = createContext(); 👈 이렇게 2개로 나눔
export const TodoDispatchContext = createContext();
function App() {
const [todos, dispatch] = useReducer(reducer, mockData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onUpdate = useCallback((targetId) => {
dispatch({
type: "UPDATE",
targetId: targetId,
});
}, []);
const onDelete = useCallback((targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
}, []);
// #3-1번 TodoStateContext가 변경이 되어서 리렌더링될 때마다 이 객체를 다시 생성될 것이기 때문에 -> useMemo로 기억해두어야함..
// value={{ onCreate, onUpdate, onDelete }} 이런식으로 넣어주게 되면, 컴포넌트가 리렌더링 될 때마다 이 객체가 계속 다시 생성될 것임.
// 하지만 원하는 건 변하지 않는 값들만 공급하는 것을 원함(그래서 만든게 memoizedDispatch임)
// 그래서 useMemo로 다시 생성하지 않도록 감싸줬음.
const memoizedDispatch = useMemo(() => {
return { onCreate, onUpdate, onDelete };
}, []);
return (
<div className="App">
<Header />
{/* <TodoContext.Provider value={{ todos, onCreate, onUpdate, onDelete }}> */}
{/* #1번 TodoStateContext.Provider 생성 */}
<TodoStateContext.Provider value={todos}> // 👈 이렇게 넣어줌
{/* #2번 TodoDispatchContext.Provider 생성 */}
{/* #3-2번 vaule=에 useMome해둔 거 넣기*/}
<TodoDispatchContext.Provider value={memoizedDispatch}>
<Editor />
<List />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
{/* </TodoContext.Provider> */}
</div>
);
}
export default App;
// # Editor.js
import { useContext, useState } from "react";
import "./Editor.css";
import { useRef } from "react";
import { TodoDispatchContext } from "../App";
const Editor = () => {
const { onCreate } = useContext(TodoDispatchContext); // 👈 구조 분해 할당으로 data 가져옴..
const [content, setContent] = useState("");
const contentRef = useRef();
... 생략 ...
return (
<div className="Editor">
<input
ref={contentRef}
onKeyDown={onKeyDown}
value={content}
onChange={onChangeContent}
placeholder="새로운 Todo..."
></input>
<button onClick={onSubmit}>추가</button>
</div>
);
};
export default Editor;
// # List.js
import { TodoStateContext } from "../App";
import "./List.css";
import TodoItem from "./TodoItem";
import { useContext, useMemo, useState } from "react";
const List = () => {
// const { todos } 구조 분해할당에서 왜 바로 변수에 담는 것으로 바뀐 것인지?
// 이전에 Context에는 여러 객체들이 있었지만, 이제는 변하는 값인 todos만 있음..
// 그래서 구조분해 할당이 아니라 그냥 value props로 전달된 값을 그대로 todos라는 변수로 꺼내서 사용할 수 있다.
const todos = useContext(TodoStateContext); 👈 context 가져옴
... 생략 ...
return (
<div className="List">
<h4>Todo List 🌱</h4>
<div>
<div>total: {totalCount}</div>
<div>done: {doneCount}</div>
<div>notDone: {notDoneCount}</div>
</div>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요"
/>
<div className="todos_wrapper">
{filteredTodos.map((todo) => {
return (
<TodoItem
key={todo.id}
{...todo}
// onUpdate={onUpdate} // TodoItem에서 직접 불러와서 사용할 것이기 때문에, 여기서부터 데이터를 가지고 가지 않아도 된다..
// onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
// #TodoItem.js
import { TodoDispatchContext } from "../App";
import "./TodoItem.css";
import { memo, useContext } from "react";
const TodoItem = ({ id, isDone, content, date }) => {
const { onUpdate, onDelete } = useContext(TodoDispatchContext); 👈 context 여기
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
return (
<div className="TodoItem">
<input
onChange={onChangeCheckbox}
readOnly
checked={isDone}
type="checkbox"
/>
<div className="content">{content}</div>
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
</div>
);
};
export default memo(TodoItem);

'공부 > frontend' 카테고리의 다른 글
| 페이지 라우팅 (0) | 2025.03.10 |
|---|---|
| MobX6 (0) | 2025.03.03 |
| ?. (옵셔널 체이닝)과 ?? (널 병합 연산자)란? (0) | 2025.02.13 |
| React Hook 최적화: useMemo, memo, useCallback (1) | 2025.02.06 |
| useReducer (0) | 2025.01.31 |