July 14, 2025

Props Drilling (프롭스 드릴링) 🕳️: 컴포넌트 간 불필요한 의존성 심화와 유지보수의 악몽

React
컴포넌트
상태관리

Summary

컴포넌트 트리의 여러 단계에 걸쳐 Props를 전달해야 하는 'Props Drilling'은 코드의 가독성을 해치고, 불필요한 재렌더링을 유발하며, 컴포넌트 간의 결합도를 높여 유지보수를 어렵게 만듭니다. React Context API, 상태 관리 라이브러리, 또는 컴포넌트 합성(Composition)을 통해 데이터를 필요한 컴포넌트에 직접 전달하고, 코드의 응집도를 높여야 합니다.

Why Wrong?

컴포넌트 트리의 여러 단계에 걸쳐 props를 전달해야 하는 'Props Drilling'은 코드의 가독성을 해치고, 불필요한 재렌더링을 유발하며, 컴포넌트 간의 결합도를 높여 유지보수를 어렵게 만듭니다.

  • 가독성 및 유지보수성 저하: 컴포넌트가 자신에게 필요 없는 props를 중간에서 단순히 전달(relay)만 하는 경우, 코드를 읽고 이해하기 어려워집니다. 어떤 데이터가 어디서 왔는지 추적하기 힘들어지고, 나중에 prop의 이름이나 타입이 변경되면 관련 없는 모든 중간 컴포넌트를 수정해야 합니다.
  • 불필요한 재렌더링 유발: 중간 컴포넌트가 자신은 사용하지 않는 props가 변경될 때마다 재렌더링될 수 있습니다. 이는 특히 큰 애플리케이션에서 성능 저하의 원인이 될 수 있습니다. React.memo와 같은 최적화 기법을 사용하더라도, props 자체가 변경되면 재렌더링이 발생합니다.
  • 컴포넌트 결합도 증가: 중간 컴포넌트들이 실제 비즈니스 로직과 관련 없이 오로지 데이터를 하위 컴포넌트에 전달하는 역할만을 수행하게 되면서, 컴포넌트 간의 결합도가 불필요하게 높아집니다. 이는 컴포넌트의 재사용성을 떨어뜨리고 테스트를 어렵게 만듭니다.

How to Fix?

Props Drilling 문제를 해결하고 더 효율적이며 유지보수하기 쉬운 애플리케이션을 구축하기 위해 다음과 같은 방법들을 고려할 수 있습니다.

  • Context API 사용: 전역적으로 또는 특정 컴포넌트 서브트리에서 공유되어야 할 데이터를 React Context에 담아 제공합니다. 이 방법을 사용하면 컴포넌트 트리의 깊이에 상관없이 필요한 하위 컴포넌트에서 useContext 훅을 사용하여 데이터에 직접 접근하게 됩니다. 이는 중간 컴포넌트들이 불필요하게 props를 전달하는 역할을 하지 않도록 해줍니다. Context는 UI 테마, 사용자 인증 정보 등 여러 컴포넌트가 접근해야 하는 '전역적인' 데이터에 적합합니다.
  • 상태 관리 라이브러리 활용: Redux, Zustand, Jotai, Recoil 등과 같은 전역 상태 관리 라이브러리를 사용하여 애플리케이션의 상태를 중앙 집중식으로 관리합니다. 이러한 라이브러리들은 더 복잡한 상태 로직, 미들웨어, 개발자 도구 등을 제공하여 대규모 애플리케이션의 상태 관리에 적합할 수 있습니다. 필요한 컴포넌트에서 특정 상태를 선택적으로 구독하여 가져올 수 있습니다.
  • 컴포넌트 합성 (Composition): 부모 컴포넌트에서 필요한 데이터를 가진 특정 컴포넌트를 직접 렌더링하고, 이 컴포넌트가 데이터를 필요한 자식 컴포넌트에 전달하도록 구조를 변경합니다. props.children을 활용하거나, 렌더 프롭스(render props) 패턴을 사용하여 데이터를 직접 필요한 하위 컴포넌트에 전달할 수 있습니다. 이는 중간 컴포넌트의 책임을 줄여주고, 재사용 가능한 컴포넌트의 생성을 돕습니다.

Before Code (Bad)

```jsx
// App.jsx
function App() {
  const user = { name: 'Alice', theme: 'dark', notifications: 5 };
  return <Page user={user} />;
}

// Page.jsx
function Page({ user }) {
  // Page는 user prop을 사용하지 않지만 Layout에 전달하기 위해 받음
  return <Layout user={user} />;
}

// Layout.jsx
function Layout({ user }) {
  // Layout도 user prop을 사용하지 않지만 Header에 전달하기 위해 받음
  return (
    <div>
      <Header user={user} />
      <Sidebar />
      <MainContent />
    </div>
  );
}

// Header.jsx - user prop은 Header에서만 필요하지만, Page와 Layout을 거쳐 전달됨
function Header({ user }) {
  return (
    <header>
      <h1>Welcome, {user.name}</h1>
      <span>Notifications: {user.notifications}</span>
    </header>
  );
}

// Sidebar.jsx, MainContent.jsx 등은 user prop이 필요 없음
```

**문제점:**
`user` 객체는 `Header` 컴포넌트에서만 실제로 사용되지만, `App`에서 `Page`를 거쳐 `Layout`을 거쳐 `Header`까지 여러 단계를 걸쳐 전달됩니다. 이는 `Page``Layout` 컴포넌트가 자신에게는 필요 없는 `user` prop에 의존하게 만들고, 코드의 가독성을 떨어뜨리며, `user` prop이 변경될 때 `Page``Layout`이 불필요하게 재렌더링될 가능성을 높입니다. 만약 `user` 객체에 새로운 속성을 추가하거나 기존 속성 이름을 변경해야 한다면, 중간의 모든 컴포넌트의 prop 정의를 수정해야 하는 번거로움이 있습니다.

After Code (Good)

```jsx
import React, { createContext, useContext } from 'react';

// 1. Context 생성: 공유할 데이터의 기본값(또는 null)으로 초기화
const UserContext = createContext(null);

// App.jsx
function App() {
  const user = { name: 'Alice', theme: 'dark', notifications: 5 };
  return (
    // 2. Context Provider를 사용하여 하위 컴포넌트에 값 제공
    <UserContext.Provider value={user}>
      <Page />
    </UserContext.Provider>
  );
}

// Page.jsx - user prop이 필요 없으므로, 더 이상 전달하지 않음
function Page() {
  return <Layout />;
}

// Layout.jsx - user prop이 필요 없으므로, 더 이상 전달하지 않음
function Layout() {
  return (
    <div>
      <Header />
      <Sidebar />
      <MainContent />
    </div>
  );
}

// Header.jsx - user prop을 직접 Context에서 소비
function Header() {
  // 3. useContext 훅을 사용하여 Provider로부터 값 소비
  const user = useContext(UserContext);
  if (!user) return null; // Context 값이 없을 경우 (예: Provider가 누락된 경우) 처리

  return (
    <header>
      <h1>Welcome, {user.name}</h1>
      <span>Notifications: {user.notifications}</span>
    </header>
  );
}

// Sidebar.jsx, MainContent.jsx 등은 여전히 user prop이 필요 없음
```

**개선된 점:**
`UserContext`를 사용하여 `user` 객체를 필요로 하는 `Header` 컴포넌트가 직접 데이터에 접근할 수 있게 되었습니다. 이제 `Page``Layout` 컴포넌트는 `user` prop을 받을 필요가 없으므로, `user` prop에 대한 불필요한 의존성에서 벗어나 더욱 간결하고 재사용성이 높아졌습니다. 또한, `user` 객체가 변경되더라도 `Header` 컴포넌트와 `UserContext.Provider`를 감싸는 `App` 컴포넌트 외에는 다른 중간 컴포넌트가 재렌더링될 가능성이 줄어들어 성능 측면에서도 이점을 가질 수 있습니다.