Context API 기초
React 19의 Context identity, Provider 범위, fallback, 구독 갱신과 value 안정화 계약을 테마 예제로 익힙니다.
여러 하위 컴포넌트가 같은 값을 읽을 때 모든 중간 컴포넌트에 props를 반복해서 전달하면 변경 경로가 길어질 수 있습니다.
React Context는 부모가 특정 하위 트리에 값을 제공하고, 그 아래 컴포넌트가 깊이와 관계없이 그 값을 읽게 하는 전달 채널입니다.
Context 객체 자체가 상태를 저장하거나 소유하는 것은 아닙니다. 상태와 변경 함수는 여전히 컴포넌트의 useState, useReducer 또는 별도 데이터 계층이 소유하고, Context는 그 값을 필요한 범위에 전달합니다.
REACT STATE · OWNERSHIP & TRANSITION
Context는 소유자의 값을 가장 가까운 Provider에서 전달합니다
ThemeProvider가 theme과 toggleTheme을 소유하고 ThemeContext는 전달할 identity를 정합니다. Provider가 중첩되면 각 소비자는 바로 위의 일치하는 값을 읽습니다.
본문의 override · fallback과 서로 다른 경우
바깥 Provider: darkTheme → Page
안쪽 Provider: lightTheme → Footer
Provider 없음 → null → useTheme 오류
value = undefined → undefinednull sentinel의 오류는 예제 custom hook의 정책입니다. undefined를 제공한 Provider는 defaultValue로 되돌아가지 않습니다.
createContext(null)
Context 객체는 제공하고 읽을 값의 종류를 식별합니다. 현재 상태를 직접 보관하거나 변경하지 않습니다.
undefined는 fallback이 아니다
기본값은 일치하는 Provider가 없을 때만 사용됩니다.
value={undefined}를 제공하면 소비자도 undefined를 받습니다.
-
State/action owner
ThemeProvider컴포넌트가theme과toggleTheme을 소유합니다. Context 자체는 소유자가 아닙니다. -
React 19 Provider
<ThemeContext>에value={value}를 전달해 필요한 가장 작은 하위 트리를 감쌉니다.ThemeContext.Provider는 이전 문법입니다. -
Pass-through
중간 컴포넌트는 props를 중계하거나 Context를 직접 읽지 않아도 됩니다. 직접 구독하지 않는다는 뜻이지 부모 렌더와 무관하다는 보장은 아닙니다.
-
Consumer
useContext(ThemeContext)를 호출한 컴포넌트가 현재 값을 구독합니다. Provider는 소비자보다 위에 있어야 합니다. -
Nearest provider
Provider가 중첩되면 가장 가까운 값이 이깁니다. Provider가 전혀 없을 때만 정적인 기본값을 읽으며,
nullsentinel은 custom hook에서 누락을 드러낼 수 있습니다.
같은 하위 트리가 읽는 UI 계약
테마, 현재 사용자, 언어처럼 여러 먼 소비자가 같은 값을 읽을 때 범위를 명시합니다.
Local · URL · server/cache
입력 필드는 가까운 상태에, 경로·검색 조건은 router에, 서버 응답과 캐시 수명은 데이터 계층에 둡니다.
Context는 props 전달을 생략할 수 있지만 소유권을 대신하지 않습니다. 범위와 fallback은 API 계약이고, 최적화는 실제 구독과 렌더를 측정한 뒤 적용합니다.
Context가 잘 맞는 경우
Context는 다음처럼 하나의 하위 트리에서 여러 컴포넌트가 읽는 값에 잘 맞습니다.
- 테마와 색상 모드
- 현재 사용자와 권한 UI
- 언어와 지역 설정
- 특정 기능 영역의 상태와 변경 명령
앱의 규모만으로 Context 사용 여부를 정하지는 않습니다. 값의 소유자, 필요한 소비자, Provider 범위, 변경 빈도를 함께 보고 필요를 충족하는 최소 범위를 선택합니다.
Context API의 세 역할
1. createContext: 채널 identity와 fallback 정의
createContext(defaultValue)는 컴포넌트가 제공하고 읽을 Context identity를 만듭니다. 이 객체 자체에는 현재 상태가 들어 있지 않습니다.
import { createContext, useContext } from 'react';
const ThemeContext = createContext(null);
export function useTheme() {
const value = useContext(ThemeContext);
if (value === null) {
throw new Error('useTheme must be used within ThemeProvider');
}
return value;
}
export default ThemeContext;기본값은 위쪽 트리에 일치하는 Provider가 전혀 없을 때만 쓰는 정적인 마지막 fallback입니다. Provider가 value={undefined}를 제공하면 기본값으로 돌아가지 않고 undefined가 전달됩니다.
이 예제는 Provider 누락을 조용히 숨기지 않도록 null을 sentinel로 두고 useTheme()에서 오류를 냅니다. 반대로 Provider 없이도 의미 있게 동작해야 하는 Context라면 'light'처럼 실제 fallback 값을 사용할 수 있습니다.
2. Provider: 상태 소유자가 현재 값을 제공
설치된 React 19에서는 Context 객체 자체를 Provider로 렌더링합니다. 이전 버전의 <ThemeContext.Provider> 문법도 동작하지만 React 19 공식 문서에서는 legacy 방식입니다.
import { useCallback, useMemo, useState } from 'react';
import ThemeContext from './ThemeContext';
export default function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = useCallback(() => {
setTheme(current => (current === 'light' ? 'dark' : 'light'));
}, []);
const value = useMemo(
() => ({ theme, toggleTheme }),
[theme, toggleTheme],
);
return (
<ThemeContext value={value}>
{children}
</ThemeContext>
);
}ThemeProvider 컴포넌트가 theme 상태와 toggleTheme 동작을 소유합니다. ThemeContext는 이 값을 전달할 채널 identity입니다.
useCallback과 useMemo는 정확성을 위한 필수 요소가 아니라 성능 최적화입니다. 여기서는 Provider가 다른 이유로 다시 렌더될 때 같은 theme에 대해 새 함수와 새 객체를 만들지 않게 합니다. theme가 실제로 바뀌면 value도 바뀌어야 합니다.
3. useContext: 가장 가까운 Provider의 값 구독
import { useTheme } from '../contexts/ThemeContext';
export default function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={`header header--${theme}`}>
<p>현재 테마: {theme}</p>
<button type="button" onClick={toggleTheme}>
테마 전환
</button>
</header>
);
}useContext(ThemeContext)는 호출한 컴포넌트 위쪽에서 가장 가까운 ThemeContext Provider를 찾습니다. 같은 컴포넌트가 반환하는 Provider는 그 컴포넌트 안의 useContext 호출에 적용되지 않으므로 Provider는 소비자보다 위에 있어야 합니다.
ThemeContext.Consumer도 남아 있지만 새 함수 컴포넌트에서는 useContext가 권장됩니다.
props 없이 통과하는 중간 컴포넌트
Context를 직접 읽지 않는 컴포넌트는 Context 갱신의 직접 구독자가 아닙니다.
import ThemeProvider from './contexts/ThemeProvider';
import Header from './components/Header';
import MainContent from './components/MainContent';
export default function App() {
return (
<ThemeProvider>
<Header />
<MainContent />
</ThemeProvider>
);
}import ContentSection from './ContentSection';
export default function MainContent() {
return (
<main>
<ContentSection />
</main>
);
}import { useTheme } from '../contexts/ThemeContext';
export default function ContentSection() {
const { theme } = useTheme();
return <section className={`content content--${theme}`}>본문</section>;
}MainContent는 ThemeContext를 직접 읽지 않지만 ContentSection은 읽습니다. 따라서 MainContent가 Context 전파를 위해 props를 중계할 필요는 없습니다.
다만 “Context를 읽지 않는다”가 “절대 다시 렌더되지 않는다”는 뜻은 아닙니다. 부모가 다시 렌더되면 일반적인 부모-자식 렌더 경로로 실행될 수 있고, element identity나 memoization에 따른 bailout이 일부 작업을 건너뛸 수도 있습니다. 직접 Context 구독과 부모 렌더 경로를 구분해서 관찰해야 합니다.
가장 가까운 Provider와 override
Provider를 중첩하면 소비자는 위쪽에서 가장 가까운 Provider의 값을 받습니다.
<ThemeContext value={darkTheme}>
<Page />
<ThemeContext value={lightTheme}>
<Footer />
</ThemeContext>
</ThemeContext>Page 아래 소비자는 darkTheme을, 안쪽 Provider 아래 Footer 소비자는 lightTheme을 읽습니다. Provider 범위는 가능한 한 값을 실제로 필요로 하는 가장 작은 하위 트리에 둡니다.
Context 갱신과 렌더 계약
React는 Provider의 이전 value와 다음 value를 Object.is로 비교합니다.
- 비교 결과가 다르면 그 Context를 읽는 소비자에게 새 값을 전달하고 다시 렌더링합니다.
- 객체와 함수는 내용이 같아 보여도 새 identity이면 다른 값입니다.
memo는 props에 대한 부모 렌더 최적화이므로, 컴포넌트가 직접 읽는 Context의 갱신을 막지 않습니다.- Context를 읽는 바깥 컴포넌트와 필요한 값만 props로 받는 memoized 자식을 분리하는 방식은 별도의 최적화 경계가 될 수 있습니다.
Context value와 소비자 갱신 판단 순서 — 상태 추적
같은 light 내용의 a/b 객체가 Object.is false이므로 직접 소비자 갱신과 같은 참조 a/a 보존이 다르다. Provider value identity/직접 구독/부모 렌더를 분리한 연속 렌더 사례가 유용하다.
REACT STATE · OWNERSHIP & TRANSITION
같은 내용의 새 객체도 Context value의 변경입니다
Provider 범위와 상태 소유자를 먼저 확인한 뒤 value identity와 직접 구독자를 추적합니다. memo는 직접 Context 구독의 새 값 전달을 막지 않습니다.
value identity와 직접 Context 구독
Object.is(A,A)는 true, 같은 내용의 새 객체 B는 false입니다. useContext를 직접 구독하는 일반 소비자와 memo 소비자는 새 value를 각각 전달받습니다. pass-through는 직접 구독자가 아니며 부모 렌더는 별도 경로입니다. Context split·참조 최적화 판단은 원설명에 보존합니다.
원래 설명과 상세 기준
value identity · 최적화가 필요한 원인부터 확인
a = { theme: "light" }
b = { theme: "light" }
Object.is(a, b) → false
Object.is(a, a) → true실제 theme 변경에는 새 값이 필요합니다. useMemo/useCallback은 불필요한 identity 변경의 측정된 비용을 줄이는 최적화입니다.
| 순서 | 확인할 질문 | 유지할 계약 |
|---|---|---|
| 1 · 범위 | 어느 하위 트리가 값을 읽는가? | Provider를 모든 페이지의 최상단이 아니라 소비자를 포함하는 가장 작은 충분한 범위에 둡니다. |
| 2 · 소유 | 누가 상태와 변경 규칙을 소유하는가? | 컴포넌트의 useState나 useReducer가 소유합니다. Context는 현재 값과 변경 명령을 전달합니다. |
| 3 · 비교 | 이전 값과 다음 값이 같은가? | React는 Provider의 value를 Object.is로 비교합니다. |
| 4 · 참조 | 객체나 함수가 이유 없이 새로 만들어지는가? | 측정된 비용이 있으면 useCallback과 useMemo로 불필요한 identity 변경을 줄입니다. |
| 5 · 구독 | 누가 이 Context를 직접 읽는가? | 값이 달라지면 useContext 소비자가 새 값을 받고 다시 렌더됩니다. pass-through는 직접 구독자가 아닙니다. |
| 6 · 메모 | memo가 Context 갱신을 막는가? |
막지 못합니다. 바깥에서 Context를 읽고 필요한 값을 memoized 자식의 props로 내리는 별도 경계는 가능합니다. |
| 7 · 분리 | 소비자 집합과 변경 주기가 다른가? | 관련 없는 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
Scope
값을 읽는 소비자를 포함하는 가장 작은 Provider 범위를 고릅니다.
Owner
상태와 변경 규칙은 컴포넌트가 소유하고 Context는 전달만 합니다.
Object.is이전
value와 다음value가 다른지 확인합니다.Value identity
객체·함수가 불필요하게 새로 만들어질 때만
useCallback과useMemo를 검토합니다.Consumer update
직접 Context를 읽는 소비자가 새 값을 받습니다. pass-through는 직접 구독하지 않습니다.
Memo limit
memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다.Context split
소비자 집합과 변경 주기가 다른 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다.
직접 구독과 부모 렌더는 다른 경로다
Context를 읽지 않는 중간 컴포넌트도 부모 렌더 때문에 실행될 수 있고, element identity나 memoization에 따른 bailout이 일부 작업을 건너뛸 수 있습니다.
Profiler로 원인을 확인한다
Provider 범위, value identity, 직접 소비자, 부모 렌더를 구분해 기록한 뒤 가장 작은 변경을 적용합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 표 행 | 순서 | 확인할 질문 | 유지할 계약 |
| 원도표 표 행 | 1 · 범위 | 어느 하위 트리가 값을 읽는가? | Provider를 모든 페이지의 최상단이 아니라 소비자를 포함하는 가장 작은 충분한 범위에 둡니다. |
| 원도표 표 행 | 2 · 소유 | 누가 상태와 변경 규칙을 소유하는가? | 컴포넌트의 useState나 useReducer가 소유합니다. Context는 현재 값과 변경 명령을 전달합니다. |
| 원도표 표 행 | 3 · 비교 | 이전 값과 다음 값이 같은가? | React는 Provider의 value를 Object.is로 비교합니다. |
| 원도표 표 행 | 4 · 참조 | 객체나 함수가 이유 없이 새로 만들어지는가? | 측정된 비용이 있으면 useCallback과 useMemo로 불필요한 identity 변경을 줄입니다. |
| 원도표 표 행 | 5 · 구독 | 누가 이 Context를 직접 읽는가? | 값이 달라지면 useContext 소비자가 새 값을 받고 다시 렌더됩니다. pass-through는 직접 구독자가 아닙니다. |
| 원도표 표 행 | 6 · 메모 | memo가 Context 갱신을 막는가? | 막지 못합니다. 바깥에서 Context를 읽고 필요한 값을 memoized 자식의 props로 내리는 별도 경계는 가능합니다. |
| 원도표 표 행 | 7 · 분리 | 소비자 집합과 변경 주기가 다른가? | 관련 없는 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
| 원도표 조건·주석 | Scope: 값을 읽는 소비자를 포함하는 가장 작은 Provider 범위를 고릅니다. |
| 원도표 조건·주석 | Owner: 상태와 변경 규칙은 컴포넌트가 소유하고 Context는 전달만 합니다. |
| 원도표 설명 | 이전 value와 다음 value가 다른지 확인합니다. |
| 원도표 조건·주석 | Object.is: 이전 value와 다음 value가 다른지 확인합니다. |
| 원도표 설명 | 객체·함수가 불필요하게 새로 만들어질 때만 useCallback과 useMemo를 검토합니다. |
| 원도표 조건·주석 | Value identity: 객체·함수가 불필요하게 새로 만들어질 때만 useCallback과 useMemo를 검토합니다. |
| 원도표 조건·주석 | Consumer update: 직접 Context를 읽는 소비자가 새 값을 받습니다. pass-through는 직접 구독하지 않습니다. |
| 원도표 설명 | memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다. |
| 원도표 조건·주석 | Memo limit: memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다. |
| 원도표 조건·주석 | Context split: 소비자 집합과 변경 주기가 다른 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 이전 value A | value: A light / consumer: A의 현재 값 | provider가 theme:light인 객체 A를 제공한다 | 값의 내용과 객체의 정체성을 나눈다 |
| 새 value B | 내용: light 유지 / identity: A≠B | 같은 light 내용을 가진 새 B는 Object.is(A,B)=false다 | 직접 소비자는 새로운 Context 값의 갱신을 받는다 |
| 같은 A 비교 | Object.is(A,A): true / memo: 직접 Context 갱신을 막지 못함 | A/A는 true이며 props memo와 별도의 Context·부모 경로를 구별한다 | 참조 최적화는 측정한 필요에 적용한다 |
Context value와 소비자 갱신 판단 순서 — 상태 추적
같은 light 내용의 a/b 객체가 Object.is false이므로 직접 소비자 갱신과 같은 참조 a/a 보존이 다르다. Provider value identity/직접 구독/부모 렌더를 분리한 연속 렌더 사례가 유용하다.
REACT STATE · OWNERSHIP & TRANSITION
같은 내용의 새 객체도 Context value의 변경입니다
Provider 범위와 상태 소유자를 먼저 확인한 뒤 value identity와 직접 구독자를 추적합니다. memo는 직접 Context 구독의 새 값 전달을 막지 않습니다.
value identity와 직접 Context 구독
Object.is(A,A)는 true, 같은 내용의 새 객체 B는 false입니다. useContext를 직접 구독하는 일반 소비자와 memo 소비자는 새 value를 각각 전달받습니다. pass-through는 직접 구독자가 아니며 부모 렌더는 별도 경로입니다. Context split·참조 최적화 판단은 원설명에 보존합니다.
원래 설명과 상세 기준
value identity · 최적화가 필요한 원인부터 확인
a = { theme: "light" }
b = { theme: "light" }
Object.is(a, b) → false
Object.is(a, a) → true실제 theme 변경에는 새 값이 필요합니다. useMemo/useCallback은 불필요한 identity 변경의 측정된 비용을 줄이는 최적화입니다.
| 순서 | 확인할 질문 | 유지할 계약 |
|---|---|---|
| 1 · 범위 | 어느 하위 트리가 값을 읽는가? | Provider를 모든 페이지의 최상단이 아니라 소비자를 포함하는 가장 작은 충분한 범위에 둡니다. |
| 2 · 소유 | 누가 상태와 변경 규칙을 소유하는가? | 컴포넌트의 useState나 useReducer가 소유합니다. Context는 현재 값과 변경 명령을 전달합니다. |
| 3 · 비교 | 이전 값과 다음 값이 같은가? | React는 Provider의 value를 Object.is로 비교합니다. |
| 4 · 참조 | 객체나 함수가 이유 없이 새로 만들어지는가? | 측정된 비용이 있으면 useCallback과 useMemo로 불필요한 identity 변경을 줄입니다. |
| 5 · 구독 | 누가 이 Context를 직접 읽는가? | 값이 달라지면 useContext 소비자가 새 값을 받고 다시 렌더됩니다. pass-through는 직접 구독자가 아닙니다. |
| 6 · 메모 | memo가 Context 갱신을 막는가? |
막지 못합니다. 바깥에서 Context를 읽고 필요한 값을 memoized 자식의 props로 내리는 별도 경계는 가능합니다. |
| 7 · 분리 | 소비자 집합과 변경 주기가 다른가? | 관련 없는 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
Scope
값을 읽는 소비자를 포함하는 가장 작은 Provider 범위를 고릅니다.
Owner
상태와 변경 규칙은 컴포넌트가 소유하고 Context는 전달만 합니다.
Object.is이전
value와 다음value가 다른지 확인합니다.Value identity
객체·함수가 불필요하게 새로 만들어질 때만
useCallback과useMemo를 검토합니다.Consumer update
직접 Context를 읽는 소비자가 새 값을 받습니다. pass-through는 직접 구독하지 않습니다.
Memo limit
memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다.Context split
소비자 집합과 변경 주기가 다른 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다.
직접 구독과 부모 렌더는 다른 경로다
Context를 읽지 않는 중간 컴포넌트도 부모 렌더 때문에 실행될 수 있고, element identity나 memoization에 따른 bailout이 일부 작업을 건너뛸 수 있습니다.
Profiler로 원인을 확인한다
Provider 범위, value identity, 직접 소비자, 부모 렌더를 구분해 기록한 뒤 가장 작은 변경을 적용합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 표 행 | 순서 | 확인할 질문 | 유지할 계약 |
| 원도표 표 행 | 1 · 범위 | 어느 하위 트리가 값을 읽는가? | Provider를 모든 페이지의 최상단이 아니라 소비자를 포함하는 가장 작은 충분한 범위에 둡니다. |
| 원도표 표 행 | 2 · 소유 | 누가 상태와 변경 규칙을 소유하는가? | 컴포넌트의 useState나 useReducer가 소유합니다. Context는 현재 값과 변경 명령을 전달합니다. |
| 원도표 표 행 | 3 · 비교 | 이전 값과 다음 값이 같은가? | React는 Provider의 value를 Object.is로 비교합니다. |
| 원도표 표 행 | 4 · 참조 | 객체나 함수가 이유 없이 새로 만들어지는가? | 측정된 비용이 있으면 useCallback과 useMemo로 불필요한 identity 변경을 줄입니다. |
| 원도표 표 행 | 5 · 구독 | 누가 이 Context를 직접 읽는가? | 값이 달라지면 useContext 소비자가 새 값을 받고 다시 렌더됩니다. pass-through는 직접 구독자가 아닙니다. |
| 원도표 표 행 | 6 · 메모 | memo가 Context 갱신을 막는가? | 막지 못합니다. 바깥에서 Context를 읽고 필요한 값을 memoized 자식의 props로 내리는 별도 경계는 가능합니다. |
| 원도표 표 행 | 7 · 분리 | 소비자 집합과 변경 주기가 다른가? | 관련 없는 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
| 원도표 조건·주석 | Scope: 값을 읽는 소비자를 포함하는 가장 작은 Provider 범위를 고릅니다. |
| 원도표 조건·주석 | Owner: 상태와 변경 규칙은 컴포넌트가 소유하고 Context는 전달만 합니다. |
| 원도표 설명 | 이전 value와 다음 value가 다른지 확인합니다. |
| 원도표 조건·주석 | Object.is: 이전 value와 다음 value가 다른지 확인합니다. |
| 원도표 설명 | 객체·함수가 불필요하게 새로 만들어질 때만 useCallback과 useMemo를 검토합니다. |
| 원도표 조건·주석 | Value identity: 객체·함수가 불필요하게 새로 만들어질 때만 useCallback과 useMemo를 검토합니다. |
| 원도표 조건·주석 | Consumer update: 직접 Context를 읽는 소비자가 새 값을 받습니다. pass-through는 직접 구독하지 않습니다. |
| 원도표 설명 | memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다. |
| 원도표 조건·주석 | Memo limit: memo는 직접 Context 갱신을 막지 않습니다. props 경계를 따로 만들 때만 역할이 달라집니다. |
| 원도표 조건·주석 | Context split: 소비자 집합과 변경 주기가 다른 값, 또는 state와 안정적인 dispatch를 별도 Context로 나눌 수 있습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 이전 value A | value: A light / consumer: A의 현재 값 | provider가 theme:light인 객체 A를 제공한다 | 값의 내용과 객체의 정체성을 나눈다 |
| 새 value B | 내용: light 유지 / identity: A≠B | 같은 light 내용을 가진 새 B는 Object.is(A,B)=false다 | 직접 소비자는 새로운 Context 값의 갱신을 받는다 |
| 같은 A 비교 | Object.is(A,A): true / memo: 직접 Context 갱신을 막지 못함 | A/A는 true이며 props memo와 별도의 Context·부모 경로를 구별한다 | 참조 최적화는 측정한 필요에 적용한다 |
value identity 안정화
Provider가 매번 value={{ theme, toggleTheme }}처럼 새 객체와 새 함수를 만들면, 상태가 그대로여도 Provider의 다른 렌더 때문에 소비자가 갱신될 수 있습니다.
측정 결과 이 비용이 의미 있을 때 useCallback으로 함수를, useMemo로 객체 identity를 안정화합니다. 메모이제이션을 무조건 추가하지 말고 React Profiler로 어떤 Provider 변경이 어떤 소비자 렌더를 만들었는지 먼저 확인합니다.
Context 분리
관련 없는 값을 한 객체에 넣으면 한 필드만 바뀌어도 그 Context를 읽는 모든 소비자가 새 값을 받습니다.
다음 기준이 다르면 Context를 나눌 수 있습니다.
- 값을 읽는 소비자 집합
- 변경 빈도와 변경 이유
- 상태를 읽는 컴포넌트와 안정적인 변경 명령만 필요한 컴포넌트
- Provider가 필요한 하위 트리 범위
state와 dispatch를 별도 Context로 나누는 것도 한 예입니다. 다만 작은 테마 객체처럼 비용이 문제가 되지 않는 경우에는 단순한 하나의 Context가 더 읽기 쉽습니다.
Context에 넣지 않을 상태
Context는 모든 상태의 기본 목적지가 아닙니다.
- 한 입력 필드나 작은 컴포넌트만 쓰는 값은 가까운 local state에 둡니다.
- URL 경로와 검색 조건은 router가 소유합니다.
- 서버 응답, 로딩, 재검증, 캐시 수명은 server/cache 데이터 계층에 둡니다.
- Context는 이 원본들을 UI store로 복제하는 수단이 아닙니다.
Provider 범위와 객체 비교를 따로 확인하기
위 테마 파일을 앱에 연결하면 첫 Header 문구는 현재 테마: light이고 전환 뒤 dark가 됩니다. MainContent에 테마 props를 추가하지 않아도 ContentSection은 같은 Context 값을 읽습니다. 원 예제에는 .header--dark와 .content--dark의 CSS 정의가 없으므로 클래스 이름이 바뀌는 것과 실제 색상이 바뀌는 것은 별개의 확인입니다. 색상까지 연결하는 완전한 실습은 테마 변경 기능 구현에서 진행합니다.
객체 비교는 React 화면 없이도 JavaScript 콘솔에서 먼저 확인할 수 있습니다.
const a = { theme: 'light' };
const b = { theme: 'light' };
const same = a;
console.log(Object.is(a, b)); // false: 새 객체
console.log(Object.is(a, same)); // true: 같은 객체내용과 identity를 구별한 다음에야 어떤 Provider 렌더가 새 value를 만들었는지 추적할 수 있습니다. 이 비교만으로 실제 소비자의 렌더 횟수를 측정한 것은 아닙니다.
같은 Context의 세 가지 경계 풀기
모든 소비자는 위의 useTheme을 사용하고, Provider에 주는 정상 값은 { theme, toggleTheme } 객체입니다. 다음 상황을 각각 독립적으로 읽습니다.
- 바깥 Provider의
theme은dark, 안쪽 Provider의theme은light입니다. 안쪽에 있는Footer와 바깥에만 있는Header는 무엇을 읽을까요? Header를 모든 Provider 밖으로 옮기면 위 custom Hook의 어떤 분기가 실행될까요?- Provider는 있지만
value={undefined}라면 기본값null이 대신 전달될까요? 위 Hook의value === null검사와Header의 구조 분해는 어떻게 될까요?
가까운 공급자·누락·잘못된 값을 구분하는 풀이
첫 경우 Footer는 가까운 안쪽의 light, Header는 바깥의 dark를 읽습니다. 이 문제에서 두 Provider는 같은 Context 객체를 사용한다는 조건이 중요합니다.
두 번째는 Provider가 없으므로 정적인 기본값 null을 읽고 useTheme must be used within ThemeProvider 오류를 냅니다. 세 번째는 존재하는 Provider가 제공한 undefined를 그대로 읽습니다. undefined === null은 거짓이어서 이 custom 오류 검사는 통과하지만, Header가 undefined에서 theme 등을 구조 분해할 때 오류가 납니다. 누락 감지용 null sentinel이 모든 잘못된 value의 타입 검증까지 구현한 것은 아닙니다.
정리
Context 설계의 핵심은 “얼마나 전역적인가”보다 다음 질문에 답하는 것입니다.
- 누가 상태와 변경 규칙을 소유하는가?
- 어느 하위 트리의 소비자가 값을 읽는가?
- Provider
value는 어떤 이유로 identity가 바뀌는가? - 직접 Context 구독과 부모 렌더 경로가 어떻게 다른가?
- 변경 주기와 소비자 집합이 다른 값은 분리해야 하는가?
다음 절에서는 useReducer가 복잡한 업데이트 규칙을 한 소유자에 모으고, Context가 state와 dispatch를 하위 트리에 전달하는 구조를 살펴봅니다.