Context API 기초
리액트 애플리케이션에서 여러 컴포넌트가 상태를 공유할 때 발생하는 프롭스 드릴링(Props Drilling) 의 문제점과, 이를 해결하기 위한 상태 관리 솔루션의 필요성에 대해 알아보았습니다. 이제 7장 "상태 관리 입문"의 두 번째 절에서는 리액트 자체적으로 제공하는 강력한 상태 관리 도구인 Context API에 대해 깊이 있게 알아보겠습니다.
Context API는 프롭스 드릴링 없이 컴포넌트 트리의 깊이에 상관없이 데이터를 전달하고 공유할 수 있게 해주는 메커니즘입니다. 복잡한 외부 라이브러리 없이도 전역 상태와 유사한 기능을 구현할 수 있어, 중간 규모의 애플리케이션에서 매우 유용하게 사용될 수 있습니다.
Context API란?
React Context API는 컴포넌트 트리를 통해 데이터를 "내려주는(passing down)" 방식 없이도 컴포넌트 간에 데이터를 공유할 수 있는 방법을 제공합니다. 즉, 프롭스를 일일이 전달하지 않고도, 특정 종류의 값을 컴포넌트 트리의 여러 레벨에 걸쳐 "제공(provide)"할 수 있게 해줍니다.
주요 사용 사례
- 테마(Theme) 설정: 다크 모드/라이트 모드와 같이 애플리케이션 전반에 걸쳐 적용되는 UI 스타일.
- 사용자 인증(Authentication) 정보: 로그인한 사용자 정보, 로그인 상태 등.
- 언어 설정: 다국어 지원 시 현재 언어 설정.
이러한 "전역적"이거나 "준-전역적"인 데이터를 전달하는 데 특히 효과적입니다.
Context API의 주요 요소
Context API를 사용하기 위해서는 다음과 같은 세 가지 주요 요소를 이해해야 합니다.
-
React.createContext()
: Context 객체 생성- Context를 사용하기 위한 가장 첫 단계입니다.
createContext()
함수를 호출하여 Context 객체를 생성합니다. 이 객체는Provider
와Consumer
(또는useContext
훅) 컴포넌트를 포함합니다.- 인자로는 Context의 기본값(default value) 을 받습니다. 이 기본값은 Context를 제공하는
Provider
가 없을 때, 또는Provider
에서 제공하는 값이undefined
일 때 사용됩니다.
src/contexts/ThemeContext.js // src/contexts/ThemeContext.js import React from 'react'; const ThemeContext = React.createContext('light'); // 'light'를 기본값으로 설정 export default ThemeContext;
-
<Context.Provider>
: Context 값 제공- Context 객체에 포함된
Provider
컴포넌트는 Context를 제공할 컴포넌트 트리의 최상단에 위치합니다. value
prop을 통해 자식 컴포넌트들에게 전달할 실제 데이터를 명시합니다. 이value
는 어떤 타입(문자열, 숫자, 객체, 함수 등)이든 될 수 있습니다.Provider
는 중첩될 수 있으며, 가장 가까운Provider
의value
가 하위 컴포넌트에 전달됩니다.
// src/App.js (ThemeContext.js가 있다고 가정) import React, { useState } from 'react'; import ThemeContext from './contexts/ThemeContext'; // Context 임포트 import MyComponent from './MyComponent'; // Context 값을 사용할 컴포넌트 function App() { const [theme, setTheme] = useState('light'); // 테마 상태 const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( // ThemeContext.Provider로 하위 컴포넌트들을 감싸고 value를 제공 <ThemeContext.Provider value={{ theme, toggleTheme }}> <div style={{ padding: '20px' }}> <h1>Context API 예제</h1> <p>현재 앱 테마: {theme}</p> <MyComponent /> {/* MyComponent 및 그 하위에서 theme, toggleTheme 접근 가능 */} </div> </ThemeContext.Provider> ); }
- Context 객체에 포함된
-
useContext(Context)
Hook: Context 값 사용- 함수형 컴포넌트에서 Context 값을 쉽게 구독(subscribe)하고 사용할 수 있는 React Hook입니다.
useContext()
훅은 인자로 Context 객체를 받아서, 해당 Context의 현재value
를 반환합니다.- 컴포넌트가 가장 가까운
Provider
에 의해 제공되는value
를 받습니다.Provider
가 없다면createContext()
에 전달된 기본값을 받습니다. - Context
value
가 변경되면,useContext
를 사용하는 모든 컴포넌트가 자동으로 다시 렌더링됩니다.
// src/MyComponent.js import React, { useContext } from 'react'; import ThemeContext from './contexts/ThemeContext'; // Context 임포트 import DeeplyNestedComponent from './DeeplyNestedComponent'; function MyComponent() { // useContext 훅을 사용하여 ThemeContext의 현재 값(theme, toggleTheme)을 가져옴 const { theme, toggleTheme } = useContext(ThemeContext); return ( <div style={{ padding: '15px', border: `1px solid ${theme === 'light' ? '#ccc' : '#555'}`, marginTop: '20px' }}> <h3>MyComponent</h3> <p>현재 테마 (MyComponent): {theme}</p> <button onClick={toggleTheme}>테마 전환 (MyComponent)</button> <DeeplyNestedComponent /> </div> ); } export default MyComponent; // src/DeeplyNestedComponent.js import React, { useContext } from 'react'; import ThemeContext from './contexts/ThemeContext'; function DeeplyNestedComponent() { const { theme } = useContext(ThemeContext); // 테마 값만 필요하면 theme만 가져옴 return ( <div style={{ marginTop: '15px', padding: '10px', backgroundColor: theme === 'light' ? '#f0f0f0' : '#444', color: theme === 'light' ? '#333' : '#ddd', borderRadius: '5px' }}> <h4>깊게 중첩된 컴포넌트</h4> <p>이 컴포넌트도 프롭스 없이 테마를 받았습니다: {theme}</p> </div> ); } export default DeeplyNestedComponent;
Context API를 이용한 테마 전환 예제 (통합)
이전 장에서 프롭스 드릴링의 예시로 들었던 테마 전환 기능을 Context API로 재구현해 보겠습니다.
Context 정의
// src/contexts/ThemeContext.js
import React from 'react';
// Context를 생성하고 기본값을 설정합니다.
// 기본값은 Provider가 없을 때 사용되거나, 개발 도구에서 타입 힌트를 제공하는 데 사용됩니다.
const ThemeContext = React.createContext({
theme: 'light', // 기본 테마
toggleTheme: () => {}, // 기본 함수 (아무것도 하지 않음)
});
export default ThemeContext;
Context Provider를 사용한 컴포넌트
// src/App.js
import React, { useState } from 'react';
import ThemeContext from './contexts/ThemeContext'; // Context 임포트
import Header from './components/Header';
import MainContent from './components/MainContent';
import Footer from './components/Footer';
function App() {
const [theme, setTheme] = useState('light'); // 테마 상태 관리
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
// Context.Provider의 value prop을 통해 { theme, toggleTheme } 객체를 하위 컴포넌트에 제공
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Context API 예제</h1>
<p>최상위 앱 컴포넌트의 테마: {theme}</p>
<Header />
<MainContent />
<Footer />
</div>
</ThemeContext.Provider>
);
}
export default App;
Context 값을 사용하는 컴포넌트들
// src/components/Header.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext'; // Context 임포트
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext); // useContext 훅으로 Context 값 접근
return (
<header style={{
backgroundColor: theme === 'light' ? '#eee' : '#333',
color: theme === 'light' ? '#333' : '#eee',
padding: '15px',
borderRadius: '5px',
marginBottom: '20px'
}}>
<h2>헤더</h2>
<p>현재 테마 (헤더): {theme}</p>
<button onClick={toggleTheme}>테마 전환 (헤더)</button> {/* 함수도 사용 가능 */}
</header>
);
}
export default Header;
// src/components/MainContent.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';
import ContentSection from './ContentSection'; // 더 깊은 자식 컴포넌트
function MainContent() {
const { theme } = useContext(ThemeContext); // theme 값만 필요
return (
<div style={{
backgroundColor: theme === 'light' ? '#f8f8f8' : '#555',
color: theme === 'light' ? '#333' : '#eee',
padding: '20px',
borderRadius: '5px',
marginBottom: '20px'
}}>
<h3>메인 콘텐츠</h3>
<p>이곳은 주요 내용이 표시되는 공간입니다.</p>
<ContentSection /> {/* props 전달 없이 바로 사용 */}
</div>
);
}
export default MainContent;
// src/components/ContentSection.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';
function ContentSection() {
const { theme } = useContext(ThemeContext); // theme 값만 필요
return (
<div style={{
border: `1px solid ${theme === 'light' ? '#ccc' : '#888'}`,
padding: '15px',
borderRadius: '5px',
marginTop: '15px'
}}>
<h4>콘텐츠 섹션</h4>
<p>이 텍스트의 색깔도 테마에 따라 변합니다. 프롭스 드릴링 없이!</p>
<p style={{ color: theme === 'light' ? 'blue' : 'lightblue' }}>현재 테마: {theme}</p>
</div>
);
}
export default ContentSection;
// src/components/Footer.js
import React, { useContext } from 'react';
import ThemeContext from '../contexts/ThemeContext';
function Footer() {
const { theme } = useContext(ThemeContext); // theme 값만 필요
return (
<footer style={{
backgroundColor: theme === 'light' ? '#eee' : '#333',
color: theme === 'light' ? '#333' : '#eee',
padding: '15px',
borderRadius: '5px'
}}>
<p>© 2024 Context API 앱. 현재 테마: {theme}</p>
</footer>
);
}
export default Footer;
Context API의 장점 및 고려 사항
장점
- 프롭스 드릴링 해결: 중간 컴포넌트를 거치지 않고 직접적으로 필요한 데이터에 접근할 수 있습니다.
- 코드 간결성:
props
를 계속해서 전달하는 코드가 사라져 가독성이 향상됩니다. - 리액트 내장 기능: 별도의 라이브러리 설치 없이 리액트 기본 기능으로 사용 가능합니다.
- 재사용성 증가: 전역적으로 필요한 상태를 중앙에서 관리하고 여러 컴포넌트에서 재사용하기 용이합니다.
고려 사항
- 재렌더링 문제: Context
value
가 변경될 때마다 해당 Context를useContext
로 구독하고 있는 모든 하위 컴포넌트가 불필요하게 재렌더링될 수 있습니다. 만약 Contextvalue
가 자주 변경되거나, 그value
에 여러 가지 상태(객체, 함수)가 복합적으로 포함되어 있다면 성능 문제가 발생할 수 있습니다.- 이를 완화하기 위해
React.memo
,useCallback
,useMemo
와 같은 메모이제이션 기법을 함께 사용할 수 있습니다. - Context를 여러 개로 분리하여 특정 상태에만 관심 있는 컴포넌트가 해당 Context만 구독하도록 할 수도 있습니다.
- 이를 완화하기 위해
- 단일 책임 원칙: Context는 "전역적인" 데이터를 전달하기 위한 것이지, 복잡한 비즈니스 로직이나 매우 자주 업데이트되는 상태를 관리하기 위한 완전한 상태 관리 솔루션은 아닙니다. 너무 많은 상태를 하나의 Context에 넣으면 관리하기 어려워집니다.
- 디버깅의 어려움:
props
처럼 명시적으로 전달되지 않기 때문에, 어떤 컴포넌트가 어떤 Context 값을 사용하고 있는지 파악하기 어려울 수 있습니다. React 개발자 도구의 Components 탭에서 Context 값을 확인할 수 있습니다.
언제 Context API를 사용할까?
- 애플리케이션 전반에 걸쳐 자주 사용되지만, 빈번하게 업데이트되지 않는 데이터: 테마, 사용자 인증 정보, 언어 설정 등.
- 중간 규모의 애플리케이션: 복잡한 외부 상태 관리 라이브러리(Redux, Zustand 등)를 도입하기에는 오버헤드가 크다고 판단될 때.
- 컴포넌트 트리가 깊어 프롭스 드릴링이 심각할 때.
"Context API 기초"는 여기까지입니다. 이 장에서는 Context API의 개념, createContext()
, <Context.Provider>
, useContext()
훅이라는 세 가지 핵심 요소를 자세히 살펴보고, 실제 테마 전환 예제를 통해 Context API의 강력함을 경험했습니다. 또한 Context API의 장점과 함께 고려해야 할 재렌더링 문제 등도 짚어보았습니다.
이제 여러분은 리액트 애플리케이션에서 프롭스 드릴링 없이 전역적인 상태를 효율적으로 공유하는 방법을 이해하게 되었습니다. 다음 장에서는 useReducer
훅과 Context API를 함께 사용하여 좀 더 복잡한 전역 상태를 관리하는 방법을 알아보겠습니다.