useContext
우리는 useEffect
훅의 심화 개념을 다루며 컴포넌트의 생명 주기 동안 발생하는 부수 효과를 효율적으로 관리하는 방법을 배웠습니다. 이제 리액트 애플리케이션에서 컴포넌트 간에 데이터를 공유하는 또 다른 강력한 방법인 Context API와 이를 함수형 컴포넌트에서 사용하는 useContext
훅에 대해 알아볼 차례입니다.
리액트에서 데이터는 기본적으로 부모에서 자식으로 props
를 통해 전달됩니다. 하지만 컴포넌트 트리가 깊어지면, 아주 멀리 떨어진 자식 컴포넌트에 데이터를 전달하기 위해 중간에 있는 여러 자식 컴포넌트들을 거쳐 props
를 계속해서 전달해야 하는 상황이 발생합니다. 이를 props drilling
(프롭스 드릴링) 이라고 부르며, 코드의 가독성을 해치고 유지보수를 어렵게 만듭니다.
useContext
훅은 이러한 props drilling
문제를 해결하고, 컴포넌트 트리 내에서 특정 데이터를 모든 하위 컴포넌트가 직접 접근할 수 있도록 하여 마치 전역 데이터처럼 공유할 수 있게 해줍니다. 테마(다크/라이트 모드), 사용자 인증 정보, 언어 설정 등 앱 전반에 걸쳐 필요한 데이터들을 공유할 때 유용하게 사용됩니다.
Context API의 개념
Context API는 크게 세 가지 요소를 통해 작동합니다.
React.createContext()
: Context 객체 생성
- Context를 생성하는 함수입니다. 이 함수를 호출하면
Provider
와Consumer
두 가지 컴포넌트를 가진 Context 객체가 반환됩니다. Provider
는 데이터를 제공하는 역할을,Consumer
는 데이터를 사용하는 역할을 합니다. (함수형 컴포넌트에서는useContext
훅이Consumer
역할을 대신합니다.)
Context.Provider
: 데이터를 제공하는 컴포넌트
- Context 객체가 가진
Provider
컴포넌트입니다.value
prop을 통해 하위 컴포넌트들에게 공유할 데이터를 제공합니다. 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
)을 감쌌습니다.value
prop에는 공유할 객체{ 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',
boxShadow: theme === 'dark' ? '0 4px 8px rgba(255,255,255,0.1)' : '0 4px 8px rgba(0,0,0,0.1)',
backgroundColor: theme === 'dark' ? '#555' : 'white',
color: theme === 'dark' ? 'white' : '#333',
transition: 'background-color 0.3s, color 0.3s, box-shadow 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
들의 스타일이 한 번에 변경되는 것을 확인할 수 있을 것입니다.
useContext
사용 시 주의사항
성능 고려: Context는 편리하지만, Context의 value
가 변경되면 해당 Provider
의 하위에 있는 모든 useContext
를 사용하는 컴포넌트들이 재렌더링됩니다. 만약 value
객체가 자주 변경되고, 이로 인해 불필요한 재렌더링이 많이 발생한다면 성능 문제가 될 수 있습니다.
- 해결책:
React.memo
를 사용하여 불필요한 자식 컴포넌트의 렌더링을 막거나, Context를 더 작은 단위로 분리하여 필요한 데이터만 구독하도록 할 수 있습니다.
value
객체의 안정성: Provider
에 전달되는 value
객체가 매 렌더링마다 새로운 참조를 가진다면 (value={{ theme, toggleTheme }}
처럼 객체 리터럴을 직접 사용하면), useContext
를 사용하는 모든 하위 컴포넌트가 항상 재렌더링됩니다.
- 해결책:
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()
에 설정된 기본값이 반환됩니다. 이는 디버깅 시 혼란을 줄 수 있으니 Provider
의 범위를 항상 명확히 이해해야 합니다.
Context
의 한계와 상태 관리 라이브러리
Context API는 props drilling
을 해결하고 간단한 전역 데이터를 공유하는 데 매우 유용합니다. 하지만 다음과 같은 경우에는 Context API만으로 복잡한 상태 관리를 하는 데 한계가 있습니다.
- 복잡한 전역 상태 관리: 여러 개의 액션과 리듀서를 통해 복잡한 상태 변화 로직을 관리해야 할 때.
- 불필요한 재렌더링 제어의 어려움: Context
value
가 자주 바뀌고 많은 하위 컴포넌트가 영향을 받을 때, 최적화가 어려워질 수 있습니다. - 개발자 도구 부재: Context 자체에는 Redux DevTools와 같은 전문적인 상태 추적 도구가 없습니다.
이러한 문제들을 해결하기 위해 Redux, Zustand, Recoil, Jotai 등과 같은 전문적인 상태 관리 라이브러리들이 등장했습니다. 이 라이브러리들은 Context API를 기반으로 하거나, 혹은 다른 최적화 기법들을 사용하여 대규모 애플리케이션에서 발생하는 복잡한 상태 관리 문제를 더 효율적이고 체계적으로 해결할 수 있도록 돕습니다. Context API는 이러한 라이브러리들의 동작 원리를 이해하는 데 좋은 기반이 됩니다.
"useContext: 전역 데이터 공유"는 여기까지입니다. 이 장에서는 Context API의 개념과 useContext
훅을 사용하여 컴포넌트 트리 내에서 데이터를 효율적으로 공유하는 방법을 테마 변경 예제를 통해 상세히 설명했습니다. 또한 useContext
사용 시 주의사항과 성능 최적화, 그리고 Context API의 한계점 및 전문 상태 관리 라이브러리의 필요성까지 다루어 독자들의 이해를 넓혔습니다.
이제 여러분은 props drilling
문제에 대한 해결책을 알게 되었고, 애플리케이션의 특정 전역 데이터를 효과적으로 관리할 수 있게 되었습니다. 다음 장에서는 useRef
훅을 이용하여 DOM 요소에 직접 접근하는 방법과 그 활용 사례에 대해 알아보겠습니다.