동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 때가 있다.
이럴 때는 가장 가까운 공통 부모로 state를 끌어올려야 한다.
하지만 컴포넌트들이 복잡해지고 자식 요소, 자식의 자식 등 겹겹이 쌓이면 상태를 관리하기가 어려워진다.
이 때 발생하는 문제점이 Prop Drilling이다.
위의 State 를 끌어오는 방식을 사용한다면 아래와 같이 복잡한 컴포넌트가 생긴다면, 7-child의 데이터를 사용하려면 7-child → 5-child → 4-child → 1-child 이렇게 위에 있는 모든 컴포넌트들을 거치면서 끌어올려야 한다.
이때 Prop Drilling 은 props 를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 데이터를 전달하는 과정이다.
아래 사진으로 예를 들면 1-child에서 7-child 의 데이터를 사용하고 싶다면, 5-child , 4-child 에서는 데이터를 사용안해도 전달을 해야 한다는 소리이다.
( 7-child → 5-child → 4-child → 1-child )

prop 전달이 많은 컴포넌트들을 거치면 해당 prop을 추적하기가 어려워지고, 유지보수가 더욱 어려워진다.
👀 그러면 어떤 해결 방법이 있을까?
- 컴포넌트 합성
- Context API

특정 컴포넌트가 다른 컴포넌트를 포함하거나 조합하여 화면을 구성하는 방식이다.
Prop Drilling 문제를 완화하기 위해 사용하는 접근법 중 하나로, 컴포넌트 합성을 통해 데이터를 props로 직접 필요한 컴포넌트에 전달하거나, 공통의 Wrapper 컴포넌트를 활용하여 복잡도를 줄일 수 있다.
✔️ Prop Drilling 문제 상황
data 는 Parent에서 필요하지 않지만 Child로 전달하기 위해 거쳐야 한다.
function Grandparent() {
const data = "Hello, World!";
return <Parent data={data} />;
}
function Parent({ data }) {
return <Child data={data} />;
}
function Child({ data }) {
return <div>{data}</div>;
}
✔️ 컴포넌트 합성을 활용한 해결
parent가 불필요하게 props를 전달하지 않아도 된다.
function Grandparent() { // data를 직접 전달하는 책임은 Grandparent가 가짐
const data = "Hello, World!";
return ( // 이 부분에서 두 컴포넌트를 합성함
<Parent>
<Child data={data} />
</Parent>
);
}
function Parent({ children }) { // 단순히 레이아웃만 제공
return <div className="parent-container">{children}</div>;
}
function Child({ data }) {
return <div>{data}</div>;
}
여전히 데이터가 여러단계의 컴포넌트를 거쳐야 한다.
( Grandparent → Parent → Child )
구조가 복잡해질 수 있다.
컴포넌트를 감싸는 식으로 사용을 하기 때문에 각 컴포넌트가 어떤 역할을 하는지 이해하기 어렵다.
이때 context API 를 사용하게 되면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있다.
일반적인 리액트 컴포넌트 트리 안에서 전역적이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법이다.
Context API에서 State값을 변경하면, Provider로 감싼 모든 자식 컴포넌트들이 리렌더링되므로 전역 상태 관리를 위한 도구가 아닌, 데이터를 쉽게 전달하고 공유하기 위한 목적으로 사용하는 것이 적합하다.createContext 함수를 사용하여 Context 객체를 생성한다.
Context 객체는 데이터의 기본 값을 가질 수 있다.
📁 Theme.jsx
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // 기본값으로 'light' 설정
Context의 데이터를 제공하기 위해 Provider 컴포넌트를 사용한다.
Provider는 데이터를 받아서 하위 트리의 모든 컴포넌트에 전달한다.
value 속성을 통해 하위 컴포넌트에 공유할 데이터를 전달한다.📁 App.js
import React, { useState } from 'react';
import { ThemeContext } from './ThemeContext';
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
export default App;
useContext 훅 사용 (권장!!)import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<div>
<p>현재 테마: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경
</button>
</div>
);
}
export default Toolbar;
Context.Consumer 사용import React from 'react';
import { ThemeContext } from './ThemeContext';
function Toolbar() {
return (
<ThemeContext.Consumer> // Consumer 로 감싸서 함수형 컴포넌트로 렌더링
{({ theme, setTheme }) => (
<div>
<p>현재 테마: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
테마 변경
</button>
</div>
)}
</ThemeContext.Consumer>
);
}
export default Toolbar;
여러 Context를 사용하는 경우 Provider를 중첩해 사용할 수 있다.
import { UserContext } from './UserContext';
import { ThemeContext } from './ThemeContext';
function App() {
return (
<UserContext.Provider value={{ user: 'John Doe' }}>
<ThemeContext.Provider value={{ theme: 'dark' }}>
<Dashboard />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
하위 컴포넌트에서 useContext를 사용해 각각의 데이터를 가져올 수 있다.
const user = useContext(UserContext);
const theme = useContext(ThemeContext);