리액트(React)로 컴포넌트를 분리하며 프로젝트를 개발하다 보면 누구나 한 번쯤 마주치는 악몽이 있습니다. 바로 '프롭스 드릴링(Props Drilling)'입니다.
로그인한 유저 정보나 다크모드 설정값처럼 앱 전체에서 쓰이는 데이터를 단 하나의 하위 컴포넌트에 전달하기 위해, 부모 ➔ 자식 ➔ 손자 ➔ 증손자 컴포넌트를 거쳐 불필요하게 props를 계속 넘겨주다 보면 코드가 지저분해지고 유지보수가 불가능해집니다.
처음에는 Redux(리덕스)를 도입해 보려 했지만, 단순한 상태 하나 만드는데 Action, Reducer, Store 등 보일러플레이트 코드가 너무 많아 배보다 배꼽이 더 큰 느낌을 받았습니다.
오늘은 보일러플레이트 없이 가볍고 직관적인 Zustand(주스탠드)를 도입하여 프롭스 드릴링을 단 10분 만에 해결했던 경험과 실무 활용 꿀팁을 공유해 보려고 합니다.
1. 내가 마주친 문제: "증손자 컴포넌트 하나 때문에 부모들이 고통받다"
헤더에 있는 장바구니 카운트와, 메인 상품 목록 페이지에 있는 '장바구니 담기' 버튼을 동기화해야 하는 상황이었습니다.
// ❌ 프롭스 드릴링으로 고통받던 과거 컴포넌트 구조
<App>
<Header cartCount={cartCount} /> {/* 헤더에 전달 */}
<MainPage>
<ProductList>
<ProductCard onAddToCart={handleAddToCart} /> {/* 3단계를 거쳐 함수 전달 */}
</ProductList>
</MainPage>
</App>
중간에 있는 MainPage나 ProductList는 onAddToCart 함수를 직접 쓰지도 않으면서 단순히 자식에게 넘겨주기 위해 props를 억지로 받아야 했습니다. 이 구조는 나중에 컴포넌트 위치가 바뀌거나 기능이 추가될 때마다 치명적인 리팩토링 비용을 발생시켰습니다.
2. 왜 Redux 대신 Zustand를 선택했는가?
전역 상태 라이브러리를 고민할 때 가장 먼저 떠오른 것은 Redux였지만, 작은 규모의 기능 하나를 추가하기 위해 작성해야 할 보일러플레이트 코드가 너무 많았습니다. 반면 Zustand는 다음과 같은 압도적인 장점이 있었습니다.
- 단순한 훅(Hook) 기반: 별도의 Context Provider로 앱 전체를 감쌀 필요가 없음.
- 극도로 가벼운 번들 크기: 수십 KB에 달하는 다른 라이브러리에 비해 약 1KB 수준으로 초경량.
- 타입스크립트(TypeScript) 완벽 지원: 복잡한 제네릭 설정 없이 직관적인 타입 추론 가능.
3. 실무 적용: 10줄로 만드는 Zustand 전역 스토어
Zustand의 핵심은 create 함수를 사용해 스토어(Store)를 정의하고, 필요한 컴포넌트에서 일반 커스텀 훅처럼 가져다 쓰는 것입니다.
// store/useCartStore.ts (장바구니 전역 스토어)
import { create } from 'zustand';
interface CartState {
cartCount: number;
addToCart: () => void;
clearCart: () => void;
}
export const useCartStore = create<CartState>((set) => ({
cartCount: 0,
// set 함수를 사용해 상태를 불변성 유지하며 업데이트
addToCart: () => set((state) => ({ cartCount: state.cartCount + 1 })),
clearCart: () => set({ cartCount: 0 }),
}));
Provider로 최상단 App 컴포넌트를 감싸는 과정조차 필요 없이, 이 파일 하나로 전역 상태 세팅이 끝납니다.

4. 컴포넌트에서 사용하기: "필요한 곳에서 바로 꺼내 쓴다"
이제 프롭스 드릴링 없이, 상태가 필
요한 컴포넌트에서 훅을 호출하기만 하면 됩니다.
// 1. 헤더 컴포넌트: cartCount 값만 구독
import { useCartStore } from '../store/useCartStore';
export function Header() {
const cartCount = useCartStore((state) => state.cartCount);
return <header>장바구니: 🛒 {cartCount}개</header>;
}
// 2. 상품 카드 컴포넌트: addToCart 함수만 호출
export function ProductCard() {
const addToCart = useCartStore((state) => state.addToCart);
return <button onClick={addToCart}>장바구니 담기</button>;
}
중간 컴포넌트들을 거칠 필요 없이 각자 필요한 데이터와 함수만 직접 구독하므로, 불필요한 리렌더링도 방지되고 코드가 엄청나게 깔끔해졌습니다.

5. 결론 및 느낀 점
Zustand를 도입하고 나서 리액트 상태 관리의 피로도가 절반 이하로 줄어들었습니다.
- 상태 하나 만드는데 1분도 채 걸리지 않는 압도적인 생산성
- 불필요한 props 전달이 사라져 깔끔해진 컴포넌트 트리 구조
컴포넌트 깊이가 깊어지면서 props를 넘겨주는 데 스트레스를 받고 계신다면, 무거운 라이브러리 대신 가볍고 직관적인 Zustand로 전역 상태 관리를 시작해 보시길 강력히 추천합니다!
'개발' 카테고리의 다른 글
| [Web] 스크롤 한 번에 함수가 500번 실행된다고? 디바운스와 스로틀로 브라우저 렉 잡은 후기 (0) | 2026.08.19 |
|---|---|
| [Network] "Access to fetch at... blocked by CORS policy" 프론트엔드 개발자의 최대 난적 CORS 에러 해결기 (0) | 2026.08.17 |
| [Git/GitHub] 커밋했는데 잔디가 안 심어진다? 5분 만에 해결한 Git 이메일 연동 삽질기 (0) | 2026.08.15 |
| [React] useEffect의 올바른 사용법과 무한 리렌더링(Infinite Loop) 탈출기 (0) | 2026.08.13 |
| [VS Code] 프론트엔드 개발 생산성을 2배로 올려주는 필수 확장 프로그램 5선 (0) | 2026.08.11 |