useContext
Context Provider가 하위 컴포넌트에 값을 공급하는 흐름을 구현하고 useContext의 재렌더링 범위와 한계를 이해합니다.
우리는 useEffect 훅의 심화 개념을 다루며 컴포넌트 생명 주기 동안 발생하는 부수 효과를 효율적으로 관리하는 방법을 배웠습니다.
Context API와 useContext 훅은 컴포넌트 트리에서 데이터를 공유할 때 사용합니다.
리액트에서 데이터는 기본적으로 부모에서 자식으로 props를 통해 전달됩니다.
하지만 컴포넌트 트리가 깊어지면, 멀리 떨어진 자식 컴포넌트에 데이터를 전달하기 위해 중간 컴포넌트를 거쳐 props를 계속 넘겨야 하는 상황이 생깁니다.
이를 props drilling(프롭스 드릴링)이라고 부르며, 코드의 가독성을 해치고 유지보수를 어렵게 만듭니다.
useContext 훅은 이러한 props drilling 문제를 해결하고, 컴포넌트 트리 내 특정 데이터에 하위 컴포넌트가 직접 접근하도록 만들어 전역 데이터처럼 공유할 수 있게 해줍니다.
테마(다크/라이트 모드), 사용자 인증 정보, 언어 설정처럼 앱 전반에 걸쳐 필요한 데이터를 공유할 때 특히 유용합니다.
Context API의 개념
CONTEXT: NEAREST PROVIDER
useContext는 가장 가까운 상위 Provider의 값을 읽습니다
App이 theme와 toggleTheme을 value로 공급하고 ThemeToggle·ThemedComponent가 읽습니다. Layout·Footer·OtherRoot는 소비 범위 비교를 위한 확장 예이며, Context 변경과 일반 부모 렌더를 구별합니다.
값과 변경 함수의 경로
App state: light → dark
value: {theme, toggleTheme}
ThemeToggle: toggleTheme 호출
ThemedComponent: theme로 스타일콜백은 App의 setter를 호출하고, 다음 value를 소비자에게 전달합니다. 본문은 ThemedComponent를 두 위치에서 사용합니다.
전달 범위 · 통과와 외부
일반 선은 부모·자식 배치, 강조 선은 Context 값을 읽는 경로입니다. Provider의 내부 영역과 값을 실제 읽는 소비자를 구별합니다.
Layout·Footer는 Context 값을 props로 중계할 필요가 없습니다. Provider 밖은 그 값을 읽지 않으며 가장 가까운 다른 Provider 또는 createContext 기본값을 읽습니다.
value 변경과 렌더의 구별
Object.is로 다른 value이면 이 Context를 읽는 하위 소비자가 새 값을 받습니다. memo만으로 새 Context 반영을 막지 못합니다. 미사용 자식도 부모 렌더 같은 다른 이유로 렌더될 수 있습니다.
기본값과 객체 안정성
본문의 기본값은 문자열 light, 소비자는 객체의 theme/toggleTheme을 기대하므로 Provider 밖 사용은 같은 대안이 아닙니다. 객체 value와 함수가 매번 새로 만들어지면 useMemo만 추가해도 재생성이 계속될 수 있습니다.
Context API는 크게 세 가지 요소를 통해 작동합니다.
React.createContext(): Context 객체 생성- Context를 생성하는 함수입니다. 이 함수를 호출하면
Provider와Consumer두 가지 컴포넌트를 가진 Context 객체가 반환됩니다. Provider는 데이터를 제공하는 역할을,Consumer는 데이터를 사용하는 역할을 합니다. (함수형 컴포넌트에서는useContext훅이Consumer역할을 대신합니다.)
Context.Provider: 데이터를 제공하는 컴포넌트- Context 객체가 가진
Provider컴포넌트입니다.valueprop을 통해 하위 컴포넌트들에게 공유할 데이터를 제공합니다. Provider컴포넌트의 하위에 있는 모든 컴포넌트(아무리 깊어도)는 이value에 접근할 수 있습니다.
useContext(ContextObject): 데이터를 사용하는 훅- 함수형 컴포넌트에서 Context에 접근하여
Provider가 제공하는value를 읽어오는 훅입니다. 이 훅은 인자로 Context 객체를 받습니다.
useContext를 이용한 테마(Theme) 변경 예제
애플리케이션의 테마(예: 다크 모드/라이트 모드)를 전역적으로 변경하는 기능을 useContext를 활용하여 구현해 보겠습니다.
Context 생성 (ThemeContext.js)
먼저 Context 객체를 생성하는 파일을 만듭니다.
import React from 'react';
// (1) ThemeContext 객체 생성 및 기본값 설정
// 기본값은 Provider가 없을 때 사용되거나, Context의 형태를 유추하는 데 도움을 줍니다.
const ThemeContext = React.createContext('light'); // 기본 테마는 'light'
export default ThemeContext;Provider를 이용한 데이터 제공 (App.js)
이제 App.js에서 ThemeContext.Provider를 사용하여 App 컴포넌트의 모든 하위 컴포넌트들에게 테마 관련 데이터를 제공합니다.
import React, { useState } from 'react';
import './App.css';
import ThemeContext from './contexts/ThemeContext'; // ThemeContext 불러오기
import ThemeToggle from './components/ThemeToggle'; // 테마 토글 컴포넌트
import ThemedComponent from './components/ThemedComponent'; // 테마 적용 컴포넌트
function App() {
const [theme, setTheme] = useState('light'); // 현재 테마 상태
const toggleTheme = () => {
setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
// (1) ThemeContext.Provider로 하위 컴포넌트를 감싸고, value prop으로 데이터 전달
<ThemeContext.Provider value={{ theme: theme, toggleTheme: toggleTheme }}>
<div className="App" style={{
backgroundColor: theme === 'dark' ? '#333' : '#f0f2f5',
color: theme === 'dark' ? 'white' : '#333',
minHeight: '100vh',
padding: '20px',
transition: 'background-color 0.3s, color 0.3s'
}}>
<h1>Context API를 이용한 테마 변경</h1>
<ThemeToggle /> {/* 테마를 변경하는 버튼 컴포넌트 */}
<ThemedComponent /> {/* 테마가 적용될 컴포넌트 */}
<ThemedComponent /> {/* 여러 개의 컴포넌트가 동일한 Context에 접근 가능 */}
</div>
</ThemeContext.Provider>
);
}
export default App;ThemeContext.Provider로 전체 애플리케이션(div.App)을 감쌌습니다.valueprop에는 공유할 객체{ theme: theme, toggleTheme: toggleTheme }를 전달했습니다. 이 객체는 현재 테마(theme)와 테마를 변경하는 함수(toggleTheme)를 포함합니다.
useContext를 이용한 데이터 사용
이제 Provider 하위에 있는 컴포넌트들이 useContext 훅을 사용하여 Provider가 제공하는 데이터에 접근합니다.
src/components/ThemeToggle.js 생성
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext'; // ThemeContext 불러오기
function ThemeToggle() {
// (1) useContext 훅을 사용하여 ThemeContext의 value를 가져옴
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
padding: '10px 20px',
fontSize: '16px',
backgroundColor: theme === 'dark' ? '#666' : '#007bff',
color: 'white',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
marginBottom: '20px'
}}
>
현재 테마: {theme === 'light' ? '밝은 모드 ☀️' : '다크 모드 🌙'} (클릭하여 토글)
</button>
);
}
export default ThemeToggle;useContext(ThemeContext)를 호출하여ThemeContext.Provider가 제공하는value객체를 직접 가져옵니다.- 가져온 객체에서
theme과toggleTheme을 비구조화 할당하여 사용합니다.
src/components/ThemedComponent.js 생성
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext'; // ThemeContext 불러오기
function ThemedComponent() {
const { theme } = useContext(ThemeContext); // theme 값만 가져옴
const cardStyle = {
padding: '20px',
margin: '10px 0',
borderRadius: '8px',
border: theme === 'dark' ? '1px solid #777' : '1px solid #ddd',
backgroundColor: theme === 'dark' ? '#555' : 'white',
color: theme === 'dark' ? 'white' : '#333',
transition: 'background-color 0.3s, color 0.3s, border-color 0.3s'
};
return (
<div style={cardStyle}>
<h3>테마 적용된 컴포넌트</h3>
<p>현재 테마: {theme === 'light' ? '밝은 모드' : '다크 모드'}</p>
<p>이 컴포넌트는 Context로부터 테마 정보를 받았습니다.</p>
</div>
);
}
export default ThemedComponent;- 이 컴포넌트도
useContext(ThemeContext)를 사용하여theme값을 가져와 스타일링에 적용합니다.props drilling없이도 원하는 데이터에 직접 접근할 수 있습니다.
테마 전환 동작 점검
애플리케이션을 실행하고(혹은 저장하면 자동 새로고침), 현재 테마 버튼을 클릭해보세요.
전체 앱의 배경색과 텍스트 색상, 그리고 ThemedComponent들의 스타일이 한 번에 변경되는 것을 확인할 수 있을 것입니다.
가장 가까운 Provider를 찾는 연습
Context는 페이지 전체에서 하나의 값만 쓰도록 강제하는 저장소가 아닙니다. 소비자는 같은 Context의 Provider를 자신의 조상에서 찾고, 가장 가까운 Provider의 값을 읽습니다. 소비자가 자신의 반환 JSX에 Provider를 추가해도 그 소비자의 현재 훅 호출을 감싸는 조상이 되지는 않습니다.
별도 트리에서 바깥 Provider는 객체의 theme을 dark로, 그 안의 작은 영역에 있는 Provider는 light로 제공한다고 가정합니다. 바깥 영역의 소비자 A, 안쪽 영역의 소비자 B, Context를 읽지 않는 중간 Layout을 구별해 보세요. Provider 밖의 소비자 C는 이 절의 ThemeContext 기본값 자체를 읽기만 합니다. 원래 ThemeToggle을 Provider 밖에서 그대로 실행하는 조건은 아닙니다.
값의 범위와 소비 계약
A는 dark를, B는 light를 읽습니다. C가 읽는 기본값은 객체가 아닌 문자열 light입니다. 그래서 원래 객체 소비자에 필요한 theme·toggleTheme 계약을 만족하지 않습니다. Layout이 값을 읽지 않는다면 Context 구독 때문에 갱신되는 소비자로 세지 않습니다. 부모 렌더 등 다른 이유의 렌더 가능성과는 별개입니다. 바깥 값 변경은 그 값을 실제 읽는 A에 전달되며, 안쪽 Provider의 값을 읽는 B를 바깥 값의 직접 구독자로 보지 않습니다.
공식 useContext 안내는 조상 검색과 가장 가까운 Provider, 정적인 기본값을 함께 설명합니다.
useContext 사용 시 주의사항
Context의 기본값과 value 변경이 소비자에게 미치는 영향을 구분합니다.
성능 고려: Context는 편리하지만, Context의 value가 변경되면 해당 Provider의 값을 실제로 읽는 useContext 소비자들이 재렌더링됩니다.
만약 value 객체가 자주 변경되고, 이로 인해 불필요한 재렌더링이 많이 발생한다면 성능 문제가 될 수 있습니다.
- 해결책: Context를 변경 주기와 소비 범위에 맞게 분리합니다.
React.memo만으로 해당 Context를 읽는 컴포넌트의 새 value 반영을 막을 수는 없습니다.
value 객체의 안정성: Provider에 전달되는 value 객체가 매 렌더링마다 새로운 참조를 가진다면 (value={{ theme, toggleTheme }}처럼 객체 리터럴을 직접 사용하면), useContext를 통해 그 Provider의 값을 읽는 소비자에 새 값이 전달됩니다. 안쪽에 같은 Context의 다른 Provider가 있으면 그 안쪽 소비자의 기준은 가장 가까운 Provider입니다.
- 해결책:
useMemo훅을 사용하여value객체가 의존하는 값이 변경될 때만 새로운 객체를 생성하도록 최적화할 수 있습니다.
import React, { useState, useMemo } from 'react';
// ...
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => { /* ... */ };
// useMemo를 사용하여 value 객체가 theme이나 toggleTheme이 변경될 때만 재생성되도록 함
const themeContextValue = useMemo(() => ({
theme: theme,
toggleTheme: toggleTheme
}), [theme, toggleTheme]); // theme, toggleTheme이 변경될 때만 객체 재생성
return (
<ThemeContext.Provider value={themeContextValue}>
{/* ... */}
</ThemeContext.Provider>
);
}이 예제에서는 toggleTheme 함수도 useCallback으로 최적화하지 않으면 매 렌더링마다 새로운 함수가 생성되어 themeContextValue도 계속해서 재생성될 수 있으므로 주의해야 합니다. (이 부분은 후속 최적화 설명에서 다룰 useCallback/useMemo와 관련이 깊습니다.)
Provider 없는 useContext 호출: useContext 훅을 호출하는 컴포넌트가 해당 Context.Provider의 하위에 있지 않다면, createContext()에 설정된 기본값이 반환됩니다.
원문의 기본값은 문자열 light지만 소비자는 객체의 theme과 toggleTheme을 읽습니다. 따라서 이 예제는 Provider 내부에서 사용해야 하며, Provider 없는 호출을 같은 동작의 대안으로 사용할 수 없습니다.
Context의 한계와 상태 관리 라이브러리
Context API는 props drilling을 해결하고 간단한 전역 데이터를 공유하는 데 매우 유용합니다.
하지만 다음과 같은 경우에는 Context API만으로 복잡한 상태 관리를 하는 데 한계가 있습니다.
- 복잡한 전역 상태 관리: 여러 개의 액션과 리듀서를 통해 복잡한 상태 변화 로직을 관리해야 할 때.
- 불필요한 재렌더링 제어의 어려움: Context
value가 자주 바뀌고 많은 하위 컴포넌트가 영향을 받을 때, 최적화가 어려워질 수 있습니다. - 개발자 도구 부재: Context 자체에는 Redux DevTools와 같은 전문적인 상태 추적 도구가 없습니다.
이러한 문제들을 해결하기 위해 Redux Toolkit, Zustand, Jotai 등과 같은 전문적인 상태 관리 라이브러리들이 등장했습니다.
이 라이브러리들은 Context API를 기반으로 하거나, 혹은 다른 최적화 기법들을 사용하여 대규모 애플리케이션에서 발생하는 복잡한 상태 관리 문제를 더 효율적이고 체계적으로 해결할 수 있도록 돕습니다.
Context API는 이러한 라이브러리들의 동작 원리를 이해하는 데 좋은 기반이 됩니다.
Context를 추가하기 전에는 범위와 값 안정성, 재렌더링 영향도를 함께 점검합니다.
useContext는 편리하지만 Provider 범위와 value 변경 빈도를 함께 설계해야 불필요한 렌더링을 줄일 수 있습니다.
useContext: 전역 데이터 공유는 여기까지입니다.
이 장에서는 Context API의 개념과 useContext 훅을 사용하여 컴포넌트 트리 내에서 데이터를 효율적으로 공유하는 방법을 테마 변경 예제를 통해 상세히 설명했습니다.
또한 useContext 사용 시 주의사항과 성능 최적화, 그리고 Context API의 한계점 및 전문 상태 관리 라이브러리의 필요성까지 다루어 독자들의 이해를 넓혔습니다.
Context API는 props drilling을 줄이고 특정 전역 데이터를 컴포넌트 트리에서 공유할 때 사용합니다.
다음 절에서는 useReducer로 복잡한 상태 전이를 구조화하는 방법을 다룹니다.