useContext는 부모 컴포넌트가 제공하는 값을 읽으므로, 트리 깊숙한 곳의 컴포넌트가 중간의 모든 컴포넌트가 prop으로 넘겨 주지 않아도 그 값을 받을 수 있습니다. createContext로 컨텍스트를 만들고, 트리의 일부를 value를 가진 Provider로 감싸고, 그 값이 필요한 곳에서 useContext(TheContext)를 호출합니다.
Toolbar는 테마를 전혀 언급하지 않는데도, 체크박스를 체크하면 두 버튼이 모두 바뀝니다. 이것이 컨텍스트의 핵심입니다. 중간 컴포넌트들은 쓰지도 않는 prop을 나르지 않으며, 이것이 컨텍스트가 해결하는 prop drilling 문제입니다.
세 단계
// 1. Create it once, outside any component, usually in its own file
export const ThemeContext = createContext('light');
// 2. Provide a value to a part of the tree
<ThemeContext value={theme}>
<Page />
</ThemeContext>
// 3. Read it in any component inside that part
const theme = useContext(ThemeContext);
React 19에서는 컨텍스트 객체 자체가 Provider입니다. 오래된 코드는 <ThemeContext.Provider value={theme}>라고 쓰며, 이것도 여전히 동작하므로 두 가지를 모두 보게 됩니다.
useContext는 트리를 거슬러 올라가며 그 컨텍스트의 가장 가까운 Provider를 찾습니다. 컴포넌트는 가장 가까운 것을 읽으므로, Provider를 중첩해서 한 구역의 값을 덮어쓸 수 있습니다.
기본값
createContext의 인자는 컴포넌트 위에 Provider가 없을 때 useContext가 반환하는 값입니다. state가 바뀌어도 바뀌지 않는 대체 값입니다.
첫 번째 Greeting은 위에 Provider가 없으므로 기본값 'en'을 받습니다. 두 번째는 'es'를 읽고, 세 번째는 더 가까운 'ja'를 읽습니다. 바깥쪽 value="es"를 value="ja"로 바꾸면 두 번째 줄만 바뀝니다.
자식에서 컨텍스트 업데이트하기
컨텍스트는 값을 아래로 전달할 뿐입니다. 깊은 자식이 값을 바꿀 수 있게 하려면, Provider가 있는 곳에서 값을 state로 보관하고 그 옆에 setter를 컨텍스트에 넣으세요.
LoginButton이 setUser를 호출하면 App의 state가 바뀌고, Provider가 새 값을 받고, Header도 업데이트됩니다. 어느 컴포넌트도 prop을 받지 않았습니다.
값이 바뀌면 모든 소비자가 다시 렌더링됩니다
Provider의 value가 바뀌면 React는 그 컨텍스트에 대해 useContext를 호출하는 모든 컴포넌트를 렌더링합니다. 그 컴포넌트가 memo 안에 있고 props가 그대로여도 마찬가지입니다. React는 Object.is로 이전 값과 새 값을 비교합니다. 그래서 value={{ user, setUser }}는 App이 렌더링될 때마다 새 객체이고, user가 바뀌지 않았어도 모든 소비자가 다시 렌더링됩니다.
두 리더 모두 첫 렌더링에서 한 번씩 로그를 남깁니다. 버튼을 클릭하면 PlainReader만 다시 로그를 남깁니다. 그 컨텍스트가 새 객체를 받았기 때문입니다. useMemo는 name이 바뀔 때까지 MemoContext에 같은 객체를 넘기므로 MemoReader는 그대로입니다. 이것은 렌더링이 느려졌을 때만 중요합니다. 작은 앱이라면 렌더링마다 새 객체가 생겨도 괜찮습니다.
컨텍스트 나누기
자주 바뀌는 값과 절대 바뀌지 않는 값은 컨텍스트를 함께 쓰면 안 됩니다. setter가 state와 같이 있으면 setUser만 호출하면 되는 컴포넌트도 user가 바뀔 때마다 렌더링됩니다. 두 컨텍스트로 나누면 각 소비자는 자기가 읽는 것만 구독합니다.
const UserContext = createContext(null);
const SetUserContext = createContext(() => {});
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<SetUserContext value={setUser}>
<UserContext value={user}>{children}</UserContext>
</SetUserContext>
);
}
useState의 setUser는 영원히 같은 동일성을 유지하므로, SetUserContext만 읽는 컴포넌트는 로그인 때문에 렌더링되는 일이 없습니다. 관련 없는 데이터에도 같은 원리가 적용됩니다. 테마와 장바구니는 하나의 큰 AppContext가 아니라 별도의 컨텍스트에 두어야 합니다.
흔한 마무리는 useContext(UserContext)를 호출하고 Provider가 없으면 명확한 오류를 던지는 useUser() 같은 커스텀 훅입니다. 그러면 호출하는 쪽은 컨텍스트 객체를 직접 import할 일이 없습니다.
컨텍스트, props, 상태 관리 라이브러리
- props가 기본입니다. 값을 두세 단계 아래로 넘기는 것은 명확하고 추적하기 쉽습니다.
- 컨텍스트는 여러 깊이의 많은 컴포넌트가 필요로 하고 거의 바뀌지 않는 값에 적합합니다. 테마, 언어, 로그인한 사용자, 기능 플래그 객체 같은 것들입니다.
- 상태 관리 라이브러리(Redux Toolkit, Zustand, Jotai 등)는 자주 업데이트되는 큰 state에 적합하며, 컴포넌트가 한 조각만 구독하고 나머지 때문에 생기는 렌더링을 건너뛰게 할 수 있습니다. 컨텍스트에는 그런 선택자가 없어서, 소비자는 값이 조금이라도 바뀌면 렌더링됩니다.
컨텍스트나 라이브러리를 꺼내기 전에, 컴포넌트를 children으로 넘기는 것만으로 drilling이 사라지는지 확인해 보세요. 새로운 API 없이도 해결되는 경우가 많습니다.
자주 묻는 질문
React에서 useContext는 무엇을 하나요?
트리에서 컴포넌트 위에 있는 가장 가까운 Provider에서 컨텍스트의 현재 값을 가져와 반환합니다. 그 Provider의 값이 바뀌면 그것을 읽는 모든 컴포넌트가 다시 렌더링됩니다.
createContext의 기본값은 무엇을 위한 것인가요?
컴포넌트 위에 Provider가 없을 때 useContext가 반환하는 값입니다. 테스트나 단독으로 렌더링하는 컴포넌트에 유용하며, 절대 바뀌지 않습니다.
React 19에서도 Context.Provider가 필요한가요?
아니요. React 19에서는 컨텍스트 자체를 Provider로 렌더링할 수 있습니다: <ThemeContext value="dark">. <ThemeContext.Provider value="dark">도 여전히 동작하므로 오래된 코드도 계속 실행됩니다.
자식 컴포넌트에서 컨텍스트 값을 업데이트하려면 어떻게 하나요?
Provider를 렌더링하는 컴포넌트에서 값을 state로 보관하고, 값 옆에 setter를 컨텍스트에 넣으세요: value={{ theme, setTheme }}. 자식은 useContext로 setTheme을 읽어서 호출합니다.
React 컨텍스트는 Redux를 대체하나요?
컨텍스트는 트리 아래로 값을 전달할 뿐, 스스로 state를 관리하지 않습니다. useState나 useReducer와 함께 쓰면 많은 앱을 감당할 수 있습니다. 상태 관리 라이브러리는 컨텍스트에 없는 기능을 더합니다. 한 조각만 구독해서 관련 없는 업데이트로 컴포넌트가 다시 렌더링되지 않게 하는 것, 그리고 개발자 도구와 미들웨어 같은 것들입니다.