Context란
컴포넌트간의 데이터를 전달하는 또 다른 방법이다. 기존의 props의 단점인, props drilling을 해결할 수가 있다. props drilling이란 부모에서 자식으로 계속해서 props를 전달해야 하는 상황을 말한다. 전에 만든 투두리스트의 컴포넌트를 살펴보면.. App, List, TodoItem이 차례대로 계층 구조를 이루고 있는데,부모가 전달한 props의 이름이 바뀌면 해당 props를 받은 자식들 역시 모두 이름을 바꿔야 하는 문제가 있다. 참 귀찮을 것이다.
그래서 나타난 것이 Context이다. Context라는 객체 형태의 데이터 저장소를 만들어, 필요한 데이터를 자식 컴포넌트에게 그때그때 할당해 주는 것이다.

Context 사용하기
다음은 데이터를 넘겨줄 부모 컴포넌트의 코드 일부이다. Context 객체 생성은 보통 컴퍼넌트 외부에서 한다. 한 번만 생성하면 되기 때문이다. 객체를 export 해주어야 다른 컴퍼넌트에서도 쓸 수 있다. 그리고 TodoContext.Provider 로 데이터 전달을 해주는데, 이는 컴퍼넌트이므로 value 로 전달할 데이터들을 객체로 묶어 전달해준다.
import { createContext } from "react";
export const TodoContext = createContext();
function App() {
...
return (
<div className="App">
<Header />
<TodoContext.Provider
value={{
todos,
onCreate,
onUpdate,
onDelete,
}}
>
<Editor />
<List />
</TodoContext.Provider>
</div>
);
}
다음은 데이터를 받는 자식 컴포넌트의 코드 일부이다. useContext와 새롭게 생성한 TodoContext 객체를 임포트 해오고, 현재 자식 컴퍼넌트에서 사용할 데이터들을 구조 분해 할당했다.
import "./TodoItem.css";
import { useContext } from "react";
import { TodoContext } from "../App";
const TodoItem = ({ id, isDone, content, date }) => {
const { onUpdate, onDelete } = useContext(TodoContext);
...
데이터 전달이 정말 간편해졌다. 그런데 내가 궁금했던 것은 바로 위에서 왜 todos의 프로퍼티(id, isDone, content, date)는 props로 전달받았는가? 였다. context로 전달받으면 편할텐데.. 알고보니 useContext로 todos를 받아와 filter, map을 사용하는 것도 가능하지만, 이는 애초에 TodoItem 컴퍼넌트를 만든 목적을 해치기 때문이다. TodoItem 컴퍼넌트는 todo를 하나씩 받아오기 때문에 하나를 업데이트를 하면 전체 todos를 리렌더링할 필요가 없다. 그러므로 개별 데이터는 props를 이용하는 것이 효율적이다. 전역 상태인 onUpdate, onDelete나 배열 todos는 useContext로 전달하는 것이 더욱 직관적이고 말이다.
Context 분리하기
그런데, 지난 시간에 작성한 앱을 개발자 도구로 살펴보면, 최적화 적용이 되어있지 않은 것을 알 수 있다. TodoItem 컴포넌트엔 memo를 적용했음에도 말이다. 왜 그럴까? 그건 바로 useContext로 받아온 값이 리렌더링되면, props가 변경된 것과 동일하게 컴퍼넌트가 리렌더링 되기 때문이다. App.jsx에서 전달한 TodoContext의 todos 배열이 변경되면,
<TodoContext.Provider
value={{
todos,
onCreate,
onUpdate,
onDelete,
}}
>
위의 value Props, 즉 todos가 포함된 객체 자체가 재생성되기 때문에, useContext로 받아온 객체도 주소값이 바뀌어 해당 컴퍼넌트는 리렌더링되는 것이다.
그래서! Context를 값이 변경될 수 있는 TodoStateContext, 값이 바뀌지 않는 TodoDispatchContext 두 가지로 분리한다. 전자에는 todos, 후자에는 onCreate, onUpdate, onDelete가 해당된다. 정리하자면 아래와 같다.

그래서.. TodoDispatchContext는 변경되지 않는 값이 들어가야하므로 useMemo를 이용해 객체 자체가 재생성되지 않도록 묶어준다. 난 첨에 왜 useCallback으로 함수 재생성 방지를 했는데 또하지? 싶었는데 이건 {onCreate, onUpdate, onDelete} 객체를 재생성하지 않게 해주는 것이다.
// App.jsx 일부
export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext();
function App() {
const memoizedDispatch = useMemo(() => {
return {
onCreate,
onUpdate,
onDelete,
};
}, []);
return (
<div className="App">
<Header />
<TodoStateContext.Provider value={todos}>
<TodoDispatchContext.Provider value={memoizedDispatch}>
<Editor />
<List />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
</div>
);
}
// TodoItem.jsx 일부
import "./TodoItem.css";
import { memo, useContext } from "react";
import { TodoDispatchContext } from "../App";
const TodoItem = ({ id, isDone, content, date }) => {
const { onUpdate, onDelete } = useContext(TodoDispatchContext);
...'프론트엔드 > 한 입 크기로 잘라먹는 리액트' 카테고리의 다른 글
| React.js - 웹 스토리지 이용하기 (0) | 2025.03.01 |
|---|---|
| React.js - 페이지 라우팅 소개, 라우팅 설정하기, 페이지 이동, 동적 경로 (0) | 2025.02.10 |
| React.js - useMemo와 연산 최적화, React.memo와 컴포넌트 렌더링 최적화, useCallback과 함수 재생성 방지 (0) | 2025.02.05 |
| React.js - useReducer 소개 (0) | 2025.02.04 |
| React.js - 카운터 앱, State Lifting (0) | 2025.01.30 |