컴포넌트 트리가 깊어질수록 부모에서 자식으로 데이터를 전달하기 위해 중간에 있는 모든 컴포넌트가 props를 받아서 아래로 넘겨줘야 하는 상황이 생깁니다. Context API는 이 문제를 해결하여 컴포넌트 트리 어느 깊이에서든 데이터를 직접 읽을 수 있도록 합니다.
다음과 같이 user 정보가 App → Layout → Sidebar → UserProfile 순서로 필요하다고 가정합니다.
App (user 보유)
└─ Layout (user 전달만 함)
└─ Sidebar (user 전달만 함)
└─ UserProfile (user 실제 사용)
Layout과 Sidebar는 user를 직접 사용하지 않지만 전달을 위해 반드시 props로 받아야 합니다. 이처럼 데이터를 필요로 하지 않는 중간 컴포넌트들이 props를 단순히 전달만 하는 현상을 Props Drilling이라고 합니다.
Context는 세 가지 구성 요소로 이루어집니다.
1. Context 생성
const UserContext = createContext(null);
2. Provider로 값 공급
Provider는 자신의 하위 트리에 Context 값을 제공합니다. value prop으로 전달한 값이 하위 컴포넌트에서 읽힙니다.
function App() {
const [user, setUser] = useState({ name: "김철수" });
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
3. useContext로 값 읽기
useContext를 사용하면 트리의 어느 깊이에서든 가장 가까운 Provider의 값을 직접 읽을 수 있습니다. 중간 컴포넌트들은 아무것도 전달할 필요가 없습니다.
function UserProfile() {
const user = useContext(UserContext);
return <p>{user.name}</p>;
}
Provider 외부에서 useContext를 호출하면 createContext에 전달한 기본값을 반환합니다.
Context는 편리하지만 무분별하게 사용하면 성능 문제가 발생할 수 있습니다. Provider의 value가 변경되면 해당 Context를 구독하는 모든 컴포넌트가 리렌더링됩니다. 자주 바뀌는 값을 하나의 Context에 모두 담으면 관련 없는 컴포넌트까지 리렌더링되는 문제가 생깁니다.
// 주의: 자주 변경되는 값과 거의 변경되지 않는 값을 한 Context에 묶지 않음
const AppContext = createContext({ user, theme, cartItems }); // 나쁜 예
// 관심사별로 Context를 분리
const UserContext = createContext(user);
const ThemeContext = createContext(theme);
Context는 다음과 같은 상황에 적합합니다.
반면 단순히 컴포넌트 몇 개 사이에서만 공유하는 데이터라면 컴포넌트 합성(Component Composition)으로 해결하는 것이 더 적합할 수 있습니다. 상태 관리 라이브러리가 필요할 만큼 복잡한 상태라면 Redux, Zustand 같은 도구를 검토하는 것이 나을 수 있습니다.