리액트(React) 프로젝트의 규모가 커지면 필연적으로 마주하게 되는 현상이 있습니다. 바로 Props Drilling입니다. 최상위 컴포넌트에 있는 데이터를 5단계 아래에 있는 자식 컴포넌트로 전달하기 위해, 중간에 있는 컴포넌트들이 그 데이터를 사용하지 않음에도 불구하고 단순히 '전달'만 하는 고통스러운 상황을 말합니다.
Props Drilling 그 자체는 반드시 나쁜 것은 아니지만, 깊이가 깊어질수록 코드를 추적하기 어렵게 만들고 리팩토링을 불가능하게 만드는 주범이 됩니다. 오늘은 이 문제를 해결하기 위한 세 가지 전략을 단계별로 알아보겠습니다.
1. 첫 번째 단계: 컴포넌트 합성 (Component Composition)
많은 개발자가 Props Drilling을 발견하면 즉시 Context API나 상태 관리 라이브러리를 떠올립니다. 하지만 리액트 팀에서 권장하는 가장 첫 번째 해결책은 컴포넌트 합성입니다.
1.1 children을 활용한 구조 개선
데이터를 하위로 계속 내려보내는 대신, 컴포넌트를 조립하는 방식으로 구조를 변경해 보세요.
// 기존 방식 (Drilling 발생)
<Parent data={data}>
<Intermediate data={data}>
<Child data={data} />
</Intermediate>
</Parent>
// 컴포넌트 합성 방식
<Parent>
<Intermediate>
<Child data={data} />
</Intermediate>
</Parent>
이렇게 하면 Intermediate 컴포넌트는 data를 알 필요가 없습니다. 단순히 children으로 받은 내용을 렌더링 하기만 하면 됩니다. 데이터를 직접 사용하는 컴포넌트를 부모 수준으로 끌어올림으로써 의존성을 끊어내는 것이 핵심입니다.
2. 두 번째 단계: Context API (의존성 주입)
합성만으로 해결하기에 구조가 너무 복잡하거나, 앱 전체에서 공유되어야 하는 데이터(테마, 로그인 유저 정보 등)가 있다면 리액트 내장 기능인 Context API를 사용합니다.
2.1 Context API의 목적
Context는 '전역 상태 관리' 도구라기보다 '의존성 주입(Dependency Injection)' 도구에 가깝습니다. 특정 범위(Provider) 내에 있는 모든 컴포넌트가 계층 구조와 상관없이 데이터에 접근할 수 있게 해줍니다.
2.2 주의할 점: 리렌더링 최적화
Context의 값이 바뀌면 해당 Provider를 구독하고 있는 모든 하위 컴포넌트가 리렌더링 됩니다. 따라서 자주 바뀌는 동적인 데이터보다는 변경 빈도가 낮은 설정 데이터를 관리하는 데 적합합니다.
const UserContext = createContext();
function App() {
return (
<UserContext.Provider value={user}>
<Layout />
</UserContext.Provider>
);
}
function UserProfile() {
const user = useContext(UserContext); // 중간 단계 생략하고 바로 사용
return <div>{user.name}</div>;
}
3. 세 번째 단계: 외부 상태 관리 라이브러리 (Zustand)
복잡하고 빈번하게 변하는 상태(장바구니, 필터링 조건 등)를 관리해야 한다면 전용 라이브러리의 힘을 빌려야 합니다. 과거에는 Redux가 대세였지만, 최근에는 설정이 간편하고 성능이 뛰어난 Zustand가 각광받고 있습니다.
3.1 왜 Zustand인가?
- 보일러플레이트 제로: Redux처럼 복잡한 초기 설정이 필요 없습니다.
- 선택적 구독 (Selector): 컴포넌트가 필요한 상태만 선택해서 구독할 수 있어, 불필요한 리렌더링이 발생하지 않습니다.
- 리액트 외부에서도 접근 가능: 훅(Hook) 형태뿐만 아니라 일반 JS 로직에서도 상태를 읽고 쓸 수 있습니다.
3.2 실전 예시
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
}));
function AddButton({ product }) {
const addItem = useCartStore((state) => state.addItem);
return <button onClick={() => addItem(product)}>담기</button>;
}
4. 어떤 상황에서 무엇을 선택해야 할까?
가장 중요한 것은 상황에 맞는 도구를 선택하는 선구안입니다.
- 컴포넌트 합성: Props 전달 단계가 2~3단계 내외이며, UI 구조를 변경함으로써 해결 가능한 경우. (가장 깔끔한 코드 유지 가능)
- Context API: 다크 모드, 현재 언어(i18n), 로그인 정보처럼 앱 전체가 알아야 하지만 자주 바뀌지 않는 정보를 다룰 때.
- Zustand: 여러 컴포넌트가 복잡한 상태를 서로 공유하고, 업데이트가 매우 빈번하며, 성능 최적화가 필수적인 경우.
'개발' 카테고리의 다른 글
| [Architecture] 프론트엔드 클린 아키텍처: 도메인 로직과 UI 컴포넌트 분리하기 (0) | 2026.04.27 |
|---|---|
| [React] 선언적 프로그래밍의 정수: Suspense와 ErrorBoundary로 우아한 UI 만들기 (0) | 2026.04.27 |
| [JS] 메인 스레드를 비워라! Web Worker를 활용한 무거운 연산 분산 처리 (0) | 2026.04.27 |
| [Web] 웹 폰트로 인한 레이아웃 시프트(CLS) 해결하기: font-display와 가상 폰트 (0) | 2026.04.27 |
| [Next.js] LCP 점수를 높이는 이미지 최적화 전략: next/image 완벽 활용법 (0) | 2026.04.27 |