1. useContext?
useContext는 컴포넌트에서 Context를 읽고 구독할 수 있는 React Hook.
createContext로 생성한 Context 객체를 매개변수로 받으며, 이 Hook은 호출하는 컴포넌트 트리에서 상위 방향으로 가장 가까운 SomeContext.Provider에 전달된 value 값을 반환한다.
- 이때,
useContext()를 호출하는 컴포넌트 내부의 Provider는 고려되지 않는다. 상위에 Provider가 전혀 없을 경우, createContext에 지정한 기본값이 사용된다. Context 값이 변경되면 React는 해당 Context를 읽는 컴포넌트를 자동으로 다시 렌더링한다.
2. useContext 사용법
useContext는 컴포넌트의 최상위 수준에서 호출하여 Context 값을 읽고 구독할 수 있다. 이는 Context 값이 변경되면 해당 컴포넌트가 자동으로 다시 렌더링됨을 의미한다.
useContext를 사용하면 createContext로 생성된 특정 Context 객체를 매개변수로 전달하여, 컴포넌트 트리에서 상위 방향으로 가장 가까운 SomeContext.Provider에 전달된 value 값을 반환받는다.
- 이 방식을 통해 트리의 깊은 곳에 있는 컴포넌트에서도 상위 컴포넌트에서 제공된 데이터를 props 드릴링 없이 쉽게 접근하고 전달받을 수 있다. Provider와
useContext를 호출하는 컴포넌트 사이에 얼마나 많은 컴포넌트 레이어가 있는지는 중요하지 않다.
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
토글 테마
</button>
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<div>
{}
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'light' ? '#eee' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}}
>
현재 테마: {theme}
</button>
);
}
export default App;
3. Context 값 업데이트
- Context 값이 시간이 지남에 따라 변경되도록 하려면 React의 State와 Context를 결합해야 한다. 부모 컴포넌트에서 State 변수를 선언하고, 이 현재 State 값을 Context Provider의 value prop으로 전달한다.
- 이렇게 하면 Provider에 전달된 State 변수 값이 업데이트될 때 (State setter 함수 호출 등을 통해), 해당 Provider는 새로운 Context 값을 받게 된다. React는 이 변화를 감지하여 해당 Context를 읽는 모든 컴포넌트를 자동으로 다시 렌더링한다. 이를 통해 트리의 깊은 곳에 있는 컴포넌트들이 최신 Context 값을 받도록 할 수 있다.
createContext 호출 시 지정하는 기본값(defaultValue)은 변경되지 않는다. Context 값을 업데이트하려면 반드시 State와 함께 사용해야 한다.
import React, { createContext, useState, useContext } from 'react';
const CountContext = createContext([0, () => {}]);
function CountProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={[count, setCount]}>
{children}
</CountContext.Provider>
);
}
function CounterDisplay() {
const [count] = useContext(CountContext);
return <p>현재 카운트: {count}</p>;
}
function CounterButton() {
const [, setCount] = useContext(CountContext);
return <button onClick={() => setCount(c => c + 1)}>증가</button>;
}
export default function App() {
return (
<CountProvider>
<CounterDisplay />
<CounterButton />
</CountProvider>
);
}
4. 기본값(Fallback) 지정
- React는 컴포넌트 트리에서 상위 방향으로 특정 Context Provider를 찾을 수 없는 경우가 있다. 이럴 때
useContext()가 반환하는 Context 값은 해당 Context를 createContext 호출 시 지정한 기본값과 동일하게 된다.
- 예를 들어,
const ThemeContext = createContext('light');와 같이 Context를 생성할 때 'light'라는 기본값을 지정할 수 있다. 이 기본값은 Provider가 없을 때 사용되며, 실수로 Provider 없이 컴포넌트가 렌더링되어도 깨지지 않게 하거나 테스트에 유용할 수 있다.
- 중요한 점은
createContext에 지정된 기본값은 변경되지 않는다. Context 값이 업데이트되도록 하려면 반드시 Provider를 사용하고 State와 결합해야 한다. 부모 트리에 <SomeContext.Provider value={undefined}>와 같이 value가 undefined인 Provider가 있다면, useContext는 기본값 대신 undefined를 반환한다. 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function DisplayTheme({ label }: { label: string }) {
const theme = useContext(ThemeContext);
return (
<p>
{label}: 현재 테마는 <strong>{theme}</strong>입니다.
</p>
);
}
export default function App() {
return (
<div>
{}
<DisplayTheme label="Fallback 예시" />
{}
<ThemeContext.Provider value={undefined as any}>
<DisplayTheme label="Undefined Provider 예시" />
</ThemeContext.Provider>
{}
<ThemeContext.Provider value="dark">
<DisplayTheme label="정상 Provider 예시" />
</ThemeContext.Provider>
</div>
);
}
5. Context 오버라이딩
- Context 오버라이딩은 트리의 특정 부분을 다른 값의 Provider로 감싸서 해당 부분에 대한 Context 값을 변경하는 것을 의미한다. 이렇게 하면 부모 트리에서 제공되는 Context 값과 다른 값을 해당 하위 트리 컴포넌트들이 사용하게 된다.
- React에서는 필요한 만큼 Provider를 중첩하여 Context를 오버라이딩 할 수 있다. 가장 가까운 상위 Provider의 value가
useContext 호출 컴포넌트에게 전달되기 때문에, 중첩된 Provider는 해당 Provider 내부에 있는 컴포넌트들에게 새로운 Context 값을 제공하게 된다.
import React, { createContext, useContext } from 'react';
const ThemeContext = createContext<'light' | 'dark'>('light');
function DisplayTheme({ label }: { label: string }) {
const theme = useContext(ThemeContext);
return (
<p>
{label}: 현재 테마는 <strong>{theme}</strong>입니다.
</p>
);
}
export default function App() {
return (
<div>
{}
<ThemeContext.Provider value="light">
<DisplayTheme label="전체 트리(최상위) 예시" />
{}
<ThemeContext.Provider value="dark">
<DisplayTheme label="오버라이딩된 서브트리 예시" />
</ThemeContext.Provider>
{}
<DisplayTheme label="다시 최상위 테마 예시" />
</ThemeContext.Provider>
</div>
);
}
6. 리렌더링 최적화 (객체와 함수 전달 시)
- Context를 통해 객체나 함수를 포함한 모든 종류의 값을 전달할 수 있다. 하지만 Context Provider에 객체나 함수를 value로 전달할 때 주의할 점이 있다. 부모 컴포넌트가 리렌더링될 때, 새로운 객체나 함수가 매번 생성되어 Provider의 value prop으로 전달될 수 있다.
- React는 Context 값이 변경되었는지
Object.is를 통해 비교하고, 새로운 객체나 함수는 이전 것과 다르다고 판단한다. 이 경우, 해당 Context를 읽는 모든 하위 컴포넌트가 자동으로 다시 렌더링된다. 작은 앱에서는 문제가 되지 않을 수 있지만, 불필요한 리렌더링을 막기 위한 성능 최적화가 필요할 수 있다.
- 이 문제를 최적화하려면 React의 다른 훅인 useCallback과 useMemo를 함께 사용해야 한다.
useCallback: Context를 통해 전달하는 함수를 useCallback으로 감싸면, 함수의 의존성이 변경되지 않는 한 동일한 함수 참조를 유지할 수 있다.
useMemo: Context value로 전달하는 객체 생성을 useMemo로 감싸면, 객체의 의존성(예: 객체 내부의 상태 값이나 함수)이 변경되지 않는 한 동일한 객체 참조를 유지할 수 있다.
- 이처럼
useCallback과 useMemo를 사용하여 Provider의 value prop으로 전달되는 객체와 함수의 참조를 안정화하면, 부모 컴포넌트가 리렌더링되더라도 실제 Context 값이 변경되지 않았다면 Context를 사용하는 하위 컴포넌트의 불필요한 리렌더링을 방지할 수 있다.
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
} from 'react';
type CounterContextType = {
count: number;
increment: () => void;
};
const CounterContext = createContext<CounterContextType>({
count: 0,
increment: () => {},
});
function CounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
const value = useMemo(
() => ({ count, increment }),
[count, increment]
);
return (
<CounterContext.Provider value={value}>
{children}
</CounterContext.Provider>
);
}
function CounterDisplay() {
const { count, increment } = useContext(CounterContext);
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={increment}>증가</button>
</div>
);
}
export default function App() {
return (
<CounterProvider>
<CounterDisplay />
</CounterProvider>
);
}
7. 문제 해결
- 컴포넌트가 Provider의 값을 인식하지 못하는 경우:
useContext()를 호출하는 컴포넌트와 동일한 컴포넌트 내부 또는 그 아래에 <SomeContext.Provider>를 렌더링했을 수 있다. Provider는 useContext()를 호출하는 컴포넌트 상위에 배치되어야 한다.
- 컴포넌트를
<SomeContext.Provider>로 감싸는 것을 잊었거나 예상과 다른 위치에 배치했을 수 있다. React 개발자 도구를 사용하여 컴포넌트 계층 구조를 확인해 보자.
- 빌드 시스템 문제(예: 심볼릭 링크)로 인해 Context 객체 자체가 서로 다르게 처리될 수 있다. Provider에서 사용하는 SomeContext와 값을 읽는 곳에서 사용하는 SomeContext가 정확히 동일한 객체인지 확인해야 한다.
import React, { createContext, useContext } from 'react';
const MyContext = createContext<string>('default');
function Problem1() {
return (
<div>
{}
<Consumer label="문제1 – Provider 이전" />
{}
<MyContext.Provider value="provided">
<Consumer label="Provider 내부" />
</MyContext.Provider>
</div>
);
}
function Consumer({ label }: { label: string }) {
const value = useContext(MyContext);
return <p>{label}: {value}</p>;
}
export default Problem1;
- 기본값이 있는데 Context가
undefined를 반환하는 경우:
- 트리 어딘가에
<SomeContext.Provider> 컴포넌트가 있지만, value prop을 지정하는 것을 잊었을 수 있다. value prop이 없으면 value={undefined}를 전달하는 것과 동일하게 작동한다. 실수로 다른 Prop의 이름을 사용했을 수도 있다. React는 이 경우 콘솔에 경고를 표시한다.
createContext(defaultValue) 호출의 기본값은 위에 일치하는 Provider가 전혀 없는 경우에만 사용된다. 부모 트리 어딘가에 <SomeContext.Provider value={undefined}> 컴포넌트가 있는 경우, useContext(SomeContext)는 기본값 대신 undefined를 Context 값으로 반환한다.
import React, { createContext, useContext } from 'react';
const MyContext = createContext<string>('default');
function Problem2() {
return (
<MyContext.Provider>
<Consumer label="문제2 – value 누락" />
</MyContext.Provider>
);
}
function Consumer({ label }: { label: string }) {
const value = useContext(MyContext);
return <p>{label}: {String(value)}</p>;
}
export default Problem2;
8. 요약 및 정리
useContext는 컴포넌트에서 가장 가까운 Provider가 전달하는 값을 읽고, 그 값이 변경되면 자동으로 리렌더링을 트리거하는 Hook.
- Provider가 없으면
createContext의 기본값(defaultValue)이 반환되고, value를 undefined로 넘기면 기본값 대신 undefined가 사용된다.
- Context로 객체나 함수를 전달할 땐 매 렌더링마다 새로운 참조가 생겨 불필요한 리렌더링이 발생하므로
useMemo/useCallback으로 값과 함수를 메모이제이션해야 한다.