2025-09-22 01:31
-
Props Drilling은 React에서 데이터를 여러 계층의 컴포넌트를 통해 전달하는 패턴으로, 코드 복잡성과 유지보수 문제를 야기한다.
-
이 문제를 해결하기 위해 컴포넌트 합성, Context API, 그리고 Redux나 Zustand 같은 외부 상태 관리 라이브러리가 사용된다.
-
프로젝트의 규모와 상태의 복잡성에 따라 적절한 해결책을 선택하는 것이 효율적인 애플리케이션 관리의 핵심이다.
Props Drilling 완벽 가이드 프론트엔드 개발자의 필수 교양
React 애플리케이션의 규모가 커지면서 많은 개발자가 ‘상태 관리’라는 거대한 산을 마주하게 된다. 그리고 그 산을 오르는 길목에서 반드시 마주치는 골치 아픈 골짜기가 있으니, 바로 Props Drilling이다. 이 용어는 마치 드릴로 땅을 파내려 가는 모습처럼, props를 여러 컴포넌트 계층을 통해 깊숙한 곳까지 전달하는 상황을 묘사한다.
Props Drilling은 React의 기본적인 데이터 흐름 방식 때문에 자연스럽게 발생하는 패턴이지만, 방치하면 코드의 가독성을 해치고 유지보수를 악몽으로 만드는 주범이 될 수 있다. 이 핸드북에서는 Props Drilling이 왜 발생하는지, 어떤 문제를 일으키는지, 그리고 이 지루한 드릴링 작업을 멈추고 더 나은 코드를 작성할 수 있는 강력한 해결책들은 무엇인지 심도 있게 탐구한다.
목차
-
Props Drilling이란 무엇인가? 괴물의 탄생
-
왜 Props Drilling은 개발자를 괴롭히는가? (문제점)
-
Props Drilling 지옥 탈출 가이드: 4가지 핵심 전략
-
어떤 무기를 선택할 것인가? 상황별 최적의 솔루션
-
결론: 현명하게 상태를 지휘하는 개발자로 거듭나기
1. Props Drilling이란 무엇인가? 괴물의 탄생
정의: 데이터를 물통 나르듯 전달하는 과정
Props Drilling은 상위 컴포넌트의 상태(state)를 하위 컴포넌트로 전달할 때, 해당 상태가 필요하지 않은 중간 컴포넌트들을 거쳐서 전달하는 과정을 말한다. 마치 산 정상의 약수터에서 물을 떠 와 마을 끝 집에 전달하기 위해, 중간에 있는 모든 집을 일일이 들러 물통을 건네주는 모습과 같다. 중간에 있는 집들은 물이 필요 없지만, 오직 전달만을 위해 물통을 받고 넘겨주는 역할을 수행한다.
React의 데이터 흐름은 부모에서 자식으로 흐르는 **단방향(One-way data flow)**이 기본 원칙이다. 이 원칙 덕분에 데이터의 흐름을 예측하기 쉽고 디버깅이 용이하다는 장점이 있다. Props는 이 데이터 흐름을 구현하는 가장 기본적인 수단이다. 하지만 컴포넌트 트리가 깊어지면 이 원칙이 오히려 개발의 발목을 잡는 족쇄가 되기도 하는데, 이것이 바로 Props Drilling이 발생하는 근본적인 원인이다.
한눈에 보는 예시
사용자의 로그인 상태와 테마(다크 모드/라이트 모드)를 관리하는 간단한 애플리케이션을 상상해 보자. 최상위 컴포넌트인 App이 이 상태들을 가지고 있고, 저 멀리 깊숙한 곳에 있는 Avatar 컴포넌트와 ThemeToggleButton 컴포넌트가 이 상태를 필요로 한다.
JavaScript
// 최상위 컴포넌트
function App() {
const [user, setUser] = useState({ name: 'Alice', isLoggedIn: true });
const [theme, setTheme] = useState('light');
return (
<PageLayout user={user} theme={theme} />
);
}
// 중간 컴포넌트 1: PageLayout
// 이 컴포넌트는 user와 theme가 직접 필요하지 않다. 오직 Header로 전달할 뿐.
function PageLayout({ user, theme }) {
return (
<div>
<Header user={user} theme={theme} />
<MainContent />
</div>
);
}
// 중간 컴포넌트 2: Header
// 이 컴포넌트도 user와 theme가 직접 필요하지 않다. 오직 UserProfile로 전달할 뿐.
function Header({ user, theme }) {
return (
<header>
<Logo />
<UserProfile user={user} theme={theme} />
</header>
);
}
// 최종 목적지 컴포넌트
function UserProfile({ user, theme }) {
return (
<div>
<Avatar user={user} />
<p>Welcome, {user.name}!</p>
<ThemeToggleButton theme={theme} />
</div>
);
}
위 코드에서 App 컴포넌트의 user와 theme 상태는 PageLayout과 Header를 거쳐 UserProfile까지 전달된다. PageLayout과 Header는 이 props들을 오직 ‘통과’시키는 역할만 할 뿐, 직접 사용하지 않는다. 이것이 바로 전형적인 Props Drilling이다. 컴포넌트가 3-4개만 되어도 코드가 복잡해지기 시작한다.
2. 왜 Props Drilling은 개발자를 괴롭히는가? (문제점)
Props Drilling은 단순히 코드가 길어지는 문제에서 그치지 않고, 여러 가지 심각한 문제를 야기한다.
가독성 저하와 불필요한 코드
중간 컴포넌트들은 자신과 관련 없는 props들로 인해 인터페이스가 복잡해진다. 다른 개발자가 Header 컴포넌트의 코드를 볼 때, user와 theme prop이 왜 필요한지 이해하기 위해 상위, 하위 컴포넌트의 구조를 모두 파악해야 한다. 이는 코드의 의도를 파악하기 어렵게 만들고, 불필요한 상념을 유발한다.
유지보수의 지옥
만약 user 객체의 구조를 name에서 username으로 변경해야 한다면 어떻게 될까? App 컴포넌트뿐만 아니라, PageLayout, Header, UserProfile 등 user prop을 전달하는 모든 컴포넌트의 코드를 수정해야 한다. props의 이름이 바뀌거나 새로운 prop을 추가해야 할 때도 마찬가지다. 이런 수정 작업은 애플리케이션이 복잡할수록 실수를 유발하고 엄청난 시간을 소모하게 만든다.
컴포넌트 재사용성의 발목을 잡다
Header 컴포넌트를 다른 프로젝트나 다른 페이지에서 재사용하고 싶다고 가정해 보자. 하지만 이 Header는 user와 theme prop을 반드시 받아야만 동작하도록 설계되어 있다. 이 컴포넌트를 재사용하려면 항상 불필요한 user와 theme 데이터를 만들어 넘겨주어야 하는, 소위 ‘강한 결합(tight coupling)’ 상태가 된다. 이로 인해 컴포넌트의 독립성과 재사용성이 현저히 떨어진다.
성능 저하의 주범, 불필요한 리렌더링
React에서 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 기본적으로 리렌더링된다. App 컴포넌트에서 전혀 다른 상태, 예를 들어 counter가 변경되어 리렌더링이 발생했다고 하자. user와 theme prop은 변경되지 않았음에도 불구하고 PageLayout과 Header는 새로운 props 객체를 받아 리렌더링될 수 있다. 물론 React.memo를 사용하여 이를 방지할 수 있지만, 이는 또 다른 코드를 추가하고 관리 포인트를 늘리는 일이다. Props Drilling은 불필요한 리렌더링을 유발할 잠재적인 위험을 내포하고 있다.
3. Props Drilling 지옥 탈출 가이드: 4가지 핵심 전략
다행히도 우리에게는 이 지루한 드릴링을 멈출 여러 가지 강력한 도구와 전략이 있다.
전략 1: 컴포넌트 합성 (Component Composition) - 가장 순수한 해결책
상태 관리 라이브러리를 도입하기 전에 가장 먼저 고려해야 할, React 본연의 철학에 가장 가까운 해결책이다. Props Drilling이 발생하는 근본적인 이유는 중간 컴포넌트가 자신이 무엇을 렌더링할지 너무 많이 알고 있기 때문이다. 컴포넌트 합성은 이 책임을 상위 컴포넌트로 위임하는 방식이다.
가장 대표적인 방법은 children prop을 활용하는 것이다.
JavaScript
// App 컴포넌트에서 UI 조각을 직접 만들어 전달
function App() {
const [user, setUser] = useState({ name: 'Alice', isLoggedIn: true });
const [theme, setTheme] = useState('light');
return (
<PageLayout>
<Header>
<UserProfile user={user} theme={theme} />
</Header>
</PageLayout>
);
}
// 중간 컴포넌트들은 이제 children만 렌더링한다.
function PageLayout({ children }) {
return (
<div>
{children}
<MainContent />
</div>
);
}
function Header({ children }) {
return (
<header>
<Logo />
{children} {/* 상위에서 받은 UserProfile이 이곳에 렌더링된다 */}
</header>
);
}
// 최종 목적지 컴포넌트는 그대로
function UserProfile({ user, theme }) {
// ...
}
이제 PageLayout과 Header는 user나 theme의 존재 자체를 알지 못한다. 그저 상위에서 전달해 준 UI 조각(children)을 지정된 위치에 렌더링할 뿐이다. 이로써 중간 컴포넌트들은 데이터와 완전히 분리되어 재사용성이 극대화된다. 이는 매우 강력하고 간단한 해결책이지만, 컴포넌트 구조가 매우 복잡해지면 App 컴포넌트가 비대해질 수 있다는 단점이 있다.
전략 2: Context API - React의 내장 무기
애플리케이션 전반에 걸쳐 사용되는 전역적인(global) 데이터(테마, 사용자 정보, 언어 설정 등)를 관리하기 위해 React가 공식적으로 제공하는 기능이다. Context는 컴포넌트 트리 상에서 데이터를 아래로 전달하기 위한 “웜홀” 또는 “터널”을 만든다고 생각할 수 있다.
작동 원리:
-
createContext: Context 객체를 생성한다. -
Provider: 생성된 Context 객체에 포함된 컴포넌트로,valueprop을 통해 하위 컴포넌트들에게 데이터를 전달한다. -
useContext:Provider가 제공한 데이터에 접근하기 위해 하위 컴포넌트에서 사용하는 훅(Hook)이다.
JavaScript
// 1. Context 생성 (별도의 파일로 분리하는 것이 일반적)
const UserContext = React.createContext(null);
const ThemeContext = React.createContext('light');
// 2. 최상위 컴포넌트에서 Provider로 감싸기
function App() {
const [user, setUser] = useState({ name: 'Alice', isLoggedIn: true });
const [theme, setTheme] = useState('light');
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<PageLayout />
</ThemeContext.Provider>
</UserContext.Provider>
);
}
// 중간 컴포넌트들은 이제 아무 props도 받지 않는다!
function PageLayout() { /* ... */ }
function Header() { /* ... */ }
// 3. 최종 목적지 컴포넌트에서 useContext로 데이터 사용
function UserProfile() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return (
<div>
<Avatar /> {/* Avatar도 내부에서 UserContext를 사용할 수 있다 */}
<p>Welcome, {user.name}!</p>
<ThemeToggleButton /> {/* 이 컴포넌트는 ThemeContext를 사용 */}
</div>
);
}
장점과 단점:
-
장점: React에 내장되어 있어 별도의 라이브러리 설치가 필요 없다. 사용법이 비교적 간단하다. 전역 상태를 관리하는 데 매우 효과적이다.
-
단점:
Provider의value가 변경되면 해당 Context를 구독하는 모든 컴포G넌트가 리렌더링된다. 따라서 자주 변경되는 상태를 Context로 관리하면 성능 문제가 발생할 수 있다. 이를 해결하기 위해 Context를 여러 개로 분리하거나useMemo등을 사용해야 하는 추가적인 최적화 작업이 필요할 수 있다.
전략 3: 상태 관리 라이브러리 - 대규모 전투를 위한 중화기
애플리케이션의 상태가 복잡해지고, 여러 컴포넌트에서 상태를 읽고 수정하며, 비동기 작업과 연계되는 등 요구사항이 복잡해지면 전문적인 상태 관리 라이브러리를 고려할 때이다.
Redux Toolkit: 예측 가능한 상태 관리의 제왕
Redux는 “단일 정보 소스(Single Source of Truth)” 원칙에 따라 모든 전역 상태를 ‘스토어(store)‘라는 한 곳에서 관리한다. 컴포넌트는 스토어에서 직접 상태를 구독하고, 상태를 변경하려면 ‘액션(action)‘을 ‘디스패치(dispatch)‘해야 한다. 이 과정은 ‘리듀서(reducer)‘라는 순수 함수를 통해 처리된다.
과거의 Redux는 보일러플레이트 코드가 많다는 비판을 받았지만, **Redux Toolkit(RTK)**이 등장하면서 코드가 매우 간결해지고 사용 편의성이 대폭 향상되었다.
-
핵심 개념:
createSlice를 통해 상태, 리듀서, 액션을 한 번에 정의.configureStore로 손쉬운 스토어 설정. -
장점: 상태 변화를 예측하고 추적하기 용이하다. 강력한 개발자 도구(Redux DevTools)를 통해 시간 여행 디버깅이 가능하다. 대규모 애플리케이션의 복잡한 상태를 체계적으로 관리하는 데 최적화되어 있다.
-
단점: Zustand 같은 최신 라이브러리에 비해 여전히 배워야 할 개념이 많다. 간단한 애플리케이션에는 과도한 설정(overkill)일 수 있다.
Zustand: 가볍고 빠른 신흥 강자
Zustand는 Redux보다 훨씬 간단하고 직관적인 API를 제공하는 경량 상태 관리 라이브러리다. 훅(Hook) 기반으로 동작하며, 최소한의 보일러플레이트로 상태를 설정하고 사용할 수 있다.
-
핵심 개념:
create함수 하나로 스토어를 생성. 컴포넌트에서는 생성된 훅을 호출하여 상태와 상태 변경 함수를 가져온다. -
장점: 배우기 매우 쉽고 코드가 간결하다. Redux와 달리
Provider로 앱을 감쌀 필요가 없다. 컴포넌트는 스토어의 특정 값만 구독할 수 있어 불필요한 리렌더링을 자동으로 최적화해준다. -
단점: Redux에 비해 생태계나 관련 자료가 상대적으로 적다. 미들웨어나 복잡한 로직 구현 시 Redux만큼 정형화된 패턴이 없어 개발팀의 컨벤션이 중요해진다.
전략 4: 상태 끌어올리기 (State Colocation) - 역발상
“상태를 가능한 한 그 상태를 사용하는 컴포넌트와 가장 가까운 곳에 두라”는 원칙이다. 모든 상태를 전역으로 관리하는 것이 항상 정답은 아니다. 특정 페이지나 특정 컴포넌트 그룹에서만 사용되는 상태라면, 그들의 가장 가까운 공통 조상 컴포넌트에 상태를 위치시키는 것이 좋다. 이를 **State Colocation (상태의 지역화)**이라 한다.
이 전략은 Props Drilling을 완전히 없애는 것이 아니라, 불필요하게 먼 거리를 이동하는 것을 막아 그 범위를 최소화하는 리팩토링 기법이다. 무분별한 전역 상태 관리는 오히려 코드 추적을 어렵게 만들 수 있으므로, 항상 상태의 ‘소유권’을 신중하게 결정해야 한다.
4. 어떤 무기를 선택할 것인가? 상황별 최적의 솔루션
네 가지 전략 중 무엇을 선택해야 할지는 정답이 없다. 프로젝트의 규모, 팀의 숙련도, 상태의 복잡성에 따라 최적의 선택이 달라진다.
| 상황 | 추천 전략 | 이유 |
|---|---|---|
| 2~3 레벨 깊이의 간단한 Props 전달 | Props Drilling (그대로 두기) | 이 정도는 자연스러운 React 패턴. 섣부른 추상화는 오히려 코드를 복잡하게 만든다. |
| 레이아웃과 컨텐츠 분리가 필요할 때 | 컴포넌트 합성 (Composition) | 중간 컴포넌트의 재사용성을 높이고 싶을 때 가장 먼저 고려해야 할 순수하고 강력한 방법. |
| 테마, 인증 정보 등 앱 전반의 저빈도 업데이트 상태 | Context API | 별도 라이브러리 없이 React 내에서 전역 상태를 우아하게 관리할 수 있다. |
| 다양한 컴포넌트에서 자주 수정/사용되는 복잡한 상태 | Zustand, Redux Toolkit | 상태가 비동기 로직과 얽히고, 여러 곳에서 상호작용하며, 예측 가능성이 중요할 때 사용. |
| 특정 페이지나 기능 내에서만 공유되는 상태 | 상태 끌어올리기 (Colocation) | 상태를 전역으로 보내기 전에, 필요한 컴포넌트들의 가장 가까운 공통 조상에 두는 것을 고려. |
일반적으로는 다음의 순서로 접근하는 것이 좋다.
-
일단 Props Drilling을 사용한다. 2~3단계까지는 괜찮다.
-
더 깊어져서 불편해지면, 컴포넌트 합성을 통해 해결할 수 있는지 검토한다.
-
전역적으로 필요한 상태라면 Context API를 도입한다.
-
Context만으로 관리가 어렵고, 자주 변하는 상태가 많아지면 Zustand나 Redux Toolkit 같은 상태 관리 라이브러리를 도입한다.
5. 결론: 현명하게 상태를 지휘하는 개발자로 거듭나기
Props Drilling은 React 개발자라면 누구나 겪게 되는 성장통과 같다. 그것은 ‘악’이 아니라, 애플리케이션의 구조를 더 깊이 고민하게 만드는 ‘신호’다.
이 핸드북에서 다룬 컴포넌트 합성, Context API, 상태 관리 라이브러리, 상태 지역화 등의 전략들은 단순히 드릴링을 피하기 위한 기술이 아니다. 이것들은 더 나아가 컴포넌트 간의 결합도를 낮추고, 재사용성을 높이며, 유지보수가 용이하고, 성능이 뛰어난 애플리케이션을 만들기 위한 핵심 원칙과 맞닿아 있다.
하나의 해결책을 맹신하기보다는 각 도구의 장단점을 명확히 이해하고, 현재 마주한 문제의 성격과 프로젝트의 규모에 맞춰 가장 적절한 무기를 선택하는 지혜가 필요하다. 현명한 상태 관리 전략을 통해 복잡성을 제어하고, 우아한 데이터 흐름을 설계하는 개발자로 거듭나길 바란다.