오늘은 다양한 React 상태 관리 라이브러리 중 Context API에 대해 알아보겠습니다.
[React] 상태 관리 라이브러리, 어떤 것을 선택해야 할까?
React로 애플리케이션을 개발하다 보면 상태 관리는 필수적인 과제입니다. 현재 사용되고 있는 React 상태 관리 라이브러리는 제일 많이 사용하는 Redux를 시작으로, Zustand, Jotai, Recoil, Context API, Rea
mi-dairy.tistory.com
React에서 부모 → 자식 → 자식의 자식... 순으로 데이터를 전달할 때 보통 props를 사용합니다.
하지만 이 방식은 트리 구조가 깊어질수록 문제가 발생할 수 있습니다.
데이터를 실제로 사용하지 않는 중간 컴포넌트들까지도 props를 계속 물려주어야 하는, Props Drilling 현상이 발생하기 때문입니다.
Props Drilling은 특정 데이터를 리프 노드에서만 사용하고자 할 때 문제가 특히 두드러집니다.
데이터를 전달하는 과정에서 중간 컴포넌트들이 그 데이터를 직접 사용하지 않더라도 데이터는 모든 중간 컴포넌트들을 거쳐가야 하며,
중간 컴포넌트들은 필요한 컴포넌트까지 데이터를 전달하기 위해 props를 받아서 자식 컴포넌트에게 넘겨줘야 합니다.
이로 인해 컴포넌트들이 받는 props가 많아지며 코드의 가독성이 떨어집니다.
또한 전달 과정 중 실수로 잘못된 데이터를 넘기거나 데이터를 수정하면, 부모 컴포넌트부터 문제를 추적해야 하는 번거로움이 발생합니다. 특히 앱의 규모가 크고 컴포넌트가 많다면 문제를 해결하기가 더욱 어려워집니다.
이러한 문제를 해결하기 위해, props 대신 Context API를 사용하여 데이터를 공유할 수 있습니다.
Context를 사용하면 한 곳에서 데이터를 정의하고, 해당 데이터를 모든 하위 컴포넌트에 방송하는 형태로 전달할 수 있으며,
Context로 공유된 데이터를 사용하고 싶은 컴포넌트는 useContext 훅을 통해 간편하게 접근할 수 있습니다.
1. Context API 란?
React에서 컴포넌트 계층 구조, 즉 트리 전체에 걸쳐 전역적인 데이터를 공유하기 위해 제공되는 기능입니다.

2. 왜 사용할까?
공통으로 필요한 데이터를 많은 컴포넌트가 사용할 때, 중간 단계 컴포넌트가 데이터를 단지 전달만 하는 것은 관리가 어렵습니다.
Context를 사용하면 트리 구조 어디에서든 간단히 데이터에 접근 가능하여, props를 한 단계씩 계속 넘겨줄 필요가 없어집니다.
Redux나 MobX 같은 상태 관리 라이브러리를 도입하기에는 규모가 작거나, 단순한 전역 데이터만 필요한 경우가 있습니다.
이럴 때 Context API는 간단한 전역 데이터를 다룰 때는 가볍고 쉽게 사용할 수 있어,
다크 모드, 다국어 설정 등 보통 앱 전반적으로 영향을 미치는 데이터를 쉽게 공유할 때 사용합니다.
3. 기본 사용법
기본적으로 React에 내장되어 있기 때문에 설치할 필요 없이, createContext() 함수를 사용하여 Context 객체를 만듭니다.
import { createContext } from 'react';
const CounterContext = createContext(null); // 전역 상태를 위한 Context 생성
export default CounterContext;
그 후 Provider를 사용하여 Context를 사용할 컴포넌트 트리에 상태를 공급합니다.
import React, { useState } from 'react';
import CounterContext from './store/CounterContext';
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
// 상태를 업데이트하는 함수들
const increase = () => setCount((prev) => prev + 1);
const decrease = () => setCount((prev) => prev - 1);
const reset = () => setCount(0);
// Context로 공유할 값
const value = { count, increase, decrease, reset };
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
);
};
export default CounterProvider;
그 후 APP에서 전역 상태를 사용하고자 하는 컴포넌트를 Provider로 감싸야 합니다.
import React from 'react';
import CounterProvider from './store/CounterProvider';
import Counter from './components/Counter';
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
export default App;
그 후 전역 상태를 사용하려면 useContext() 훅을 사용하여 Context 데이터를 가져옵니다.
참고로 useContext는 자기의 부모 중에 가장 먼저 등장하는 Provider의 value 값을 가져옵니다.
부모의 부모에서 value 값으로 A를 주고, 부모에서 value 값으로 B를 줬을 때
자식 컴포넌트에서 value 값을 출력하면 B가 나옵니다.
import React, { useContext } from 'react';
import CounterContext from '../store/CounterContext';
function Counter() {
const { count, increase, decrease, reset } = useContext(CounterContext);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increase}>증가</button>
<button onClick={decrease}>감소</button>
<button onClick={reset}>리셋</button>
</div>
);
}
export default Counter;
Redux와 달리 Provider 설정이 간단하며 상태를 어디서나 쉽게 가져올 수 있지만,
상태 변경 시 관련 모든 컴포넌트가 리렌더링되므로 성능 문제가 발생할 수 있습니다.
4. 왜 편할까?
// App.js
import React from 'react';
import Parent from './Parent';
function App() {
return (
<div>
<h1>App</h1>
<Parent userName="John" />
</div>
);
}
export default App;
// Parent.js
import React from 'react';
import Child from './Child';
function Parent({ userName }) {
return <Child userName={userName} />;
}
export default Parent;
// Child.js
import React from 'react';
import GrandChild from './GrandChild';
function Child({ userName }) {
return <GrandChild userName={userName} />;
}
export default Child;
// GrandChild.js
import React from 'react';
function GrandChild({ userName }) {
return <p>Hello, {userName}!</p>;
}
export default GrandChild;
위와 같이 Context API 사용없이 props로만 데이터를 전달한다면,
Props Drilling으로 인해 userName이 사용되지 않는 중간 컴포넌트(Parent, Child)까지 전달되어야 하며,
데이터가 실제로 사용되는 GrandChild 컴포넌트에 도달하기까지 불필요한 전달이 발생합니다.
똑같은 코드에 대해 Context API를 사용하면
// UserContext.js
import React, { createContext, useContext } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) {
const userName = 'John';
return (
<UserContext.Provider value={userName}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
// App.js
import React from 'react';
import { UserProvider } from './UserContext';
import Parent from './Parent';
function App() {
return (
<UserProvider>
<h1>App</h1>
<Parent />
</UserProvider>
);
}
export default App;
// Parent.js
import React from 'react';
import Child from './Child';
function Parent() {
return <Child />;
}
export default Parent;
// Child.js
import React from 'react';
import GrandChild from './GrandChild';
function Child() {
return <GrandChild />;
}
export default Child;
// GrandChild.js
import React from 'react';
import { useUser } from './UserContext';
function GrandChild() {
const userName = useUser();
return <p>Hello, {userName}!</p>;
}
export default GrandChild;
userContext를 사용하여 중앙에서 데이터를 관리해,
데이터가 필요한 GrandChild 컴포넌트에서 직접 useContext를 통해 데이터를 가져옴으로써 Props Drilling 문제가 없어집니다.
하지만 이러한 Context도 여러 단점이 있습니다.
Context를 사용하면 컴포넌트를 재사용하기 어려우며, 과도하게 사용 시 여러 Context가 난립하면서 관리가 어려워집니다.
또한 Provider의 value가 변경되면 하위 컴포넌트들이 모두 재렌더링되어 성능 저하가 발생할 수 있습니다.
때문에 Context를 사용할 때에도 진짜 필요한 컴포넌트에만 사용해야 되며,
React 문서에선 Prop Drilling을 피하기 위한 목적이라면 Component Composition이 더 간단한 해결책이라고 나와있어
여러 상태 관리 라이브러리와 비교하여 Context API가 최적일 때 활용하면 좋을 것 같습니다!
참고
https://www.youtube.com/watch?v=LwvXVEHS638&list=WL&index=2
https://www.youtube.com/watch?v=JQ_lksQFgNw&list=WL&index=3
'프론트엔드 > React' 카테고리의 다른 글
[React] Recoil에 대해 (0) | 2025.04.01 |
---|---|
[React] 상태 관리 라이브러리, 어떤 것을 선택해야 할까? (0) | 2025.03.30 |
[React] 서버에서 상태를 가져온다고? React-Query (0) | 2025.03.30 |
[React] Zustand쓰면 Redux 이제 못 씀, Zustand 파헤치기 (0) | 2025.03.29 |
[React] 사용자에게 빠른 UI 반응을 제공하려면? Web Worker, Debounce, Throttle (0) | 2025.03.29 |