Context — 멀리 떨어진 컴포넌트에 값을 건네는 길
Props를 계단처럼 넘기는 prop drilling의 피로를 Context로 푸는 법을 정리합니다. createContext·Provider·useContext 3단계, 그리고 언제 쓰고 언제 아껴야 하는지까지 짚습니다.
지난 글까지는 컴포넌트 하나가 스스로 다루는 값(State·Ref)을 봤습니다. 이제 시야를 넓혀, 여러 컴포넌트가 값을 나눠 갖는 이야기로 넘어갑니다. Props가 부모에서 자식으로 값을 건넨다고 했는데, 이걸로 충분하지 않은 순간이 있습니다. 그 빈틈을 메우는 것이 Context입니다.
Prop drilling — 값을 계단처럼 나르는 피로
Props는 한 칸씩 전달됩니다. 부모 → 자식 → 손자로, 한 단계씩요. 그런데 최상위에서 정한 값(예: 다크/라이트 테마)을 다섯 단계 아래의 버튼이 써야 한다면 어떻게 될까요?
<App theme={theme}>
<Layout theme={theme}> {/* Layout은 theme을 쓰지도 않는데… */}
<Sidebar theme={theme}> {/* Sidebar도 그냥 넘기기만… */}
<Button theme={theme} /> {/* 진짜 필요한 건 여기 하나 */}
중간의 Layout, Sidebar는 theme을 쓰지도 않으면서 그저 아래로 전달하기 위해 계속 받아 넘겨야 합니다. 이렇게 값을 계단처럼 층층이 실어 나르는 것을 prop drilling이라고 부릅니다. 층이 깊어질수록 코드가 지저분해지고, 중간 컴포넌트가 무슨 값을 나르는지도 파악하기 어려워집니다.
Context는 이 계단을 건너뛰게 해 줍니다. 위에서 값을 한 번 뿌려 두면, 아래 어느 깊이의 컴포넌트든 중간을 거치지 않고 직접 꺼내 쓸 수 있습니다.
Context의 3단계
Context는 딱 세 단계로 씁니다.
① 만든다 — createContext
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // 기본값
createContext가 돌려주는 것은 컴포넌트가 아니라 하나의 “통로 객체”입니다. 이 객체가 값을 뿌리는 쪽과 꺼내는 쪽을 이어 줍니다.
② 뿌린다 — <Context>로 감싼다
function MyPage() {
return (
<ThemeContext value="dark">
<Form /> {/* 이 안의 어떤 깊이든 'dark'를 꺼낼 수 있음 */}
</ThemeContext>
);
}
ThemeContext로 감싼 영역 안에서는, 레벨에 상관없이 그 값을 구독할 수 있습니다.
예전 코드에서는
<ThemeContext.Provider value="dark">형태를 씁니다. React 19부터는 위처럼<ThemeContext value=...>로 바로 쓸 수 있게 짧아졌습니다. 둘은 같은 것입니다.
③ 꺼낸다 — useContext
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext); // 중간을 거치지 않고 바로 꺼냄
return <button className={theme}>버튼</button>;
}
Button이 아무리 깊은 곳에 있어도, useContext(ThemeContext) 한 줄이면 위에서 뿌린 값을 곧바로 받습니다. 계단을 오르내리던 theme prop들이 전부 사라지는 거죠.
상수만이 아니라 상태도, 함수도
뿌리는 값은 고정된 상수일 필요가 없습니다. State와 결합하면, 아래에서 값을 바꾸는 것까지 가능합니다.
function MyPage() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext value={{ theme, setTheme }}>
<Form />
</ThemeContext>
);
}
theme과 setTheme을 함께 내려보내면, 하위 컴포넌트는 값을 읽는 것뿐 아니라 바꾸는 것도 할 수 있습니다. 로그인 정보와 login() 함수를 함께 뿌리는 인증 컨텍스트가 대표적인 예입니다.
Provider는 중첩할 수도 있는데, 이 경우 가장 가까운 것이 이깁니다.
<ThemeContext value="dark">
<ThemeContext value="light">
<Footer /> {/* 가장 가까운 'light'를 구독 */}
</ThemeContext>
</ThemeContext>
언제 쓰고, 언제 아껴야 하나
Context는 강력하지만 만능 도구가 아닙니다. 테마, 로그인한 사용자, 언어 설정처럼 앱 전반이 두루 쓰는 값을 뿌릴 때 빛납니다. 반대로 두세 단계만 내려가면 되는 값까지 전부 Context로 빼면, 오히려 값의 출처가 흐려져 추적이 어려워집니다. prop drilling이 실제로 불편해질 때 꺼내 드는 도구로 생각하는 게 좋습니다.
한 가지 더. Context의 값이 바뀌면, 그 값을 구독하는 하위 컴포넌트들이 다시 렌더링됩니다. 넓은 범위를 감싼 Context일수록 이 리렌더의 파장이 커질 수 있어, 성능을 신경 써야 하는 순간이 옵니다. 이렇게 “필요 이상의 리렌더를 어떻게 줄일 것인가”가 다음 글의 주제입니다. 마지막으로 렌더링 최적화 — useMemo, useCallback, memo를 살펴보겠습니다.