본문 바로가기
3주차

Prop Drilling? 해결 방법?

by soyybeann 2025. 5. 2.

안녕하세요, YB 박소이입니다.

리액트로 개발하다 보면 state나 데이터를 여러 컴포넌트에 전달해야 할 때가 많습니다.

저는 이럴 때마다 하위 컴포넌트로 데이터를 전달 하기만 했었는데요…

이번 아티클에선 컴포넌트가 깊어지고 데이터를 전달해야할 때 발생할 수 있는 문제인 props drilling에 대해 알아 보겠습니다.

 


🔎 prop drilling 이란?

 

Prop Drilling(프롭 드릴링)은 상위 컴포넌트에서 하위 컴포넌트로 props를 전달하는 과정에서 중간 컴포넌트들을 거쳐야하는 문제를 말합니다. 이 과정이 “드릴로 뚫듯이 전달한다”고 해서 다음과 같은 이름이 붙여졌습니다.

 

리액트의 기존 방식으로 부모 → 자식 → 손자 컴포넌트로 데이터를 전달하려면, props를 계속 내려줘야 합니다.

function App() {
  return <Parent name="스웹" />;
}

function Parent({ name }) {
  return <Child name={name} />;
}

function Child({ name }) {
  return <GrandChild name={name} />;
}

function GrandChild({ name }) {
  return <p>안녕하세요, {name}님!</p>;
}

 

위 예시에서 name은 GrandChild에서만 사용되는데, App에서 GrandChild까지 여러 단계에 걸쳐 props가 전달되는걸 확인할 수 있습니다. 중간 컴포넌트인 Parent와 Child는 실제로 name을 사용하지 않지만, props를 전달해야만 하는 상황인거죠.

 

⚠️prop drilling 문제점 

이렇게 트리 구조가 깊어지는 방식은 다음과 같은 문제를 일으킬 수 있습니다.

1. 컴포넌트가 많아질수록 관리가 어려움

 여러 단계의 컴포넌트를 거쳐야 하므로, 프로젝트 규모가 커질 수록 어떤 컴포넌트가 어떤 데이터를 넘기고 있는지 파악하기 어려울 수 있습니다.

 

2. 코드 가독성 저하

 실질적으로 사용하지 않는 데이터인데도, “전달”만을 위해 props를 받아야 해서 불필요한 코드가 생기게 됩니다.

 

3. 재사용성이 낮아짐

 컴포넌트가 특정 props를 받아야만 작동하게 되면, 특정 props에 강하게 의존하게 되어 다른 곳에서 재사용하기 어렵습니다.

 

4. 성능 저하

 상위 컴포넌트에서 props가 변경되면 하위 컴포넌트들이 리렌더링되는데, 중간 컴포넌트들 역시 리렌더링될 수 있습니다.

 

💡해결 방법

1. React Context API

Context API는 리액트에서 전역 상태를 관리하는 기능을 제공하며, 이를 통해 컴포넌트 트리의 어떤 위치에서도 데이터를 전달할 수 있습니다.

즉, props를 중간 컴포넌트들을 거쳐 전달할 필요 없이, Context ProvideruseContext 훅을 사용해 데이터를 직접 접근할 수 있게 됩니다.

 

 

예시 코드로 사용 방법을 알아 보겠습니다.

import { createContext, useContext } from 'react';

// 1. Context 생성
const NameContext = createContext();

function App() {
  return (
	// 2. 최상단에서 Provider로 값 전달
    <NameContext.Provider value="sweb">
      <Parent />
    </NameContext.Provider>
  );
}

function Parent() {
  return <Child />;
}

function Child() {
  return <GrandChild />;
}

function GrandChild() {
  // 3. 하위 컴포넌트에서 Context 값 사용
  const name = useContext(NameContext); 
  return <p>안녕하세요, {name}님!</p>;
}

 

먼저, createContext()로 Context를 생성합니다.

최상위 컴포넌트에서 <NameContext.Provider>로 값을 전달합니다. 하위 컴포넌트에서는 useContext()를 사용하여 바로 해당 값을 가져올 수 있습니다.

 

 

장/단점

이 방법은 중첩 구조에서도 데이터에 직접 접근 가능하다는 장점이 있지만, 자주 바뀌는 값을 Context로 관리하면 불필요한 리렌더링이 발생할 수 있다는 단점도 있습니다.

또한, 너무 많은 Context를 사용하면 관리가 어려워지니 적절한 때에 사용해야 합니다.

 

 

2. Custom Hooks 이용하기

Custom Hook은 useState, useEffect, useContext 등 React의 내장 Hook을 사용하여 만든 재사용 가능한 함수형 로직 단위입니다. 이를 통해 특정 상태와 그 상태를 다루는 로직을 한 곳에서 정의하고, 여러 컴포넌트에서 동일한 방식으로 사용할 수 있습니다.

 

커스텀 훅은 어떻게 prop drilling을 해결할 수 있는 걸까요?

앞서 상위 컴포넌트에서 상태를 만들어 하위로 전달하는 구조가 드릴링을 유발한다고 했습니다. 커스텀 훅을 사용하면 상태를 별도 로직으로 추출하고, 각 컴포넌트에서 독립적으로 이 훅을 호출하여 필요한 데이터를 가져올 수 있습니다.

이를 통해 드릴링을 해소하고 데이터 전달을 보다 간편하게 할 수 있습니다.

 

  • useName.jsx
import { useState } from 'react';

export function useName() {
  const [name, setName] = useState('sweb');
  return { name, setName };
}

 

  • GrandChild.js
import { useName } from './useName';

function GrandChild() {
  const { name } = useName();
  return <p>안녕하세요, {name}님!</p>;
}

 

 

장/단점

Custom Hook은 상태 및 로직을 분리하여 여러 컴포넌트에서 공유가 가능하므로 가독성과 재사용성이 좋지만,

같은 훅을 여러 컴포넌트에서 사용해도 상태는 각각 독립적이다 보니 만약 여러 컴포넌트 간 동일한 상태 공유가 필요하다면 Context API 등의 방법이 추가로 필요할 수 있습니다.

 

 

3. Children props

children prop은 React의 기본 기능으로, 부모 컴포넌트가 자식 컴포넌트에게 전달할 수 있는 특수한 props 입니다.

이 props를 통해 부모 컴포넌트는 자식 컴포넌트가 렌더링할 내용(HTML, JSX 등)을 자식 컴포넌트의 childern 프로퍼티로 넘길 수 있습니다.

children prop을 사용하면, 특정 컴포넌트가 어떤 자식 컴포넌트들을 갖게 될지 미리 정의하지 않고도, 재사용 가능한 컴포넌트를 만들 수 있습니다.

 

 

기본 사용 법은 다음과 같습니다. 

function Wrapper({ children }) {
  return <div className="wrapper">{children}</div>;  // 부모가 받은 children을 출력
}

function App() {
  return (
    <Wrapper>
      <p>안녕하세요, sweb님!</p>  {/* 부모 컴포넌트에서 자식으로 전달 */}
    </Wrapper>
  );
}

 

App에서 <p> 태그를 Wrapper의 children으로 전달하는 방식입니다.

이때, Wrapper는 내용을 전달받고 그곳에 표시만 하면 되기 때문에, 해당 내용이 무엇인지는 Wrapper가 알 필요 없이, 단순히 렌더링하는 역할만 합니다.

 

 

children props는 어떻게 prop drilling을 해결할 수 있는 걸까요?

다시 한 번, prop drilling 문제는 주로 중간 컴포넌트들이 직접적으로 데이터를 전달하지 않아도 되는 상황에서 발생한다고 언급했습니다.

children을 활용하면 중간에 불필요한 컴포넌트를 거치지 않고, 데이터를 부모 컴포넌트에서 직접 자식에게 전달하기 때문에 prop drilling 문제를 해결할 수 있습니다.

function Parent() {
  const name = 'sweb';

  return (
    <Child>
      <GrandChild name={name} />  {/* 부모에서 자식에게 직접 전달 */}
    </Child>
  );
}

function Child({ children }) {
  return <div className="child">{children}</div>;  // 자식 컴포넌트를 그냥 렌더링
}

function GrandChild({ name }) {
  return <p>안녕하세요, {name}님!</p>;
}

 

children을 활용하여 GrandChild 컴포넌트로 직접 데이터를 전달할 수 있습니다. 즉, Parent가 직접 GrandChild에 데이터를 전달하고, Child는 단순히 레이아웃을 정의하는 역할을 합니다. name은 더 이상 ParentChildGrandChild 방식으로 전달되지 않으며, 대신 GrandChild부모로부터 직접 전달받는 구조로 변경되었습니다.

 

 

장/단점

children을 통해 컴포넌트 내부에서 어떤 내용을 렌더링할지 자유롭게 구성할 수 있고, 중간 컴포넌트에서 데이터를 전달하지 않으르므로 코드가 간결해진다는 장점이 있습니다.

하지만, childre은 단순히 컴포넌트를 렌더링할 때 구조 안에 어던 내용을 표시할지 정하는 내용 전달 수단이기에, 만약 상태 변경이 필요하다면 context나 state 관리가 필요합니다. 또한, Parent component의 불필요한 렌더링을 유발할 수 있다는 단점이 있습니다.

 

 

4. 상태 관리 라이브러리 이용

애플리케이션 상태가 복잡해지면, Redux같은 전역 상태 관리 라이브러리를 사용하는 것이 좋습니다. 이는 전역 상태를 만들고 상태를 필요할 때 바로 접근할 수 있습니다.

 

  • store.jsx

 먼저, createSlice로 상태와 reducer를 정의합니다.

import { configureStore, createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: { name: 'sweb' },
  reducers: {
    setName: (state, action) => {
      state.name = action.payload;
    },
  },
});

export const { setName } = userSlice.actions;

const store = configureStore({
  reducer: { user: userSlice.reducer },
});

export default store;

 

 

  • App.jsx

useSelector로 필요한 state를 직접 불러와 Provider를 통해 전역 상태에 접근 가능합니다.

import { Provider, useSelector } from 'react-redux';
import store from './store';

function GrandChild() {
  const name = useSelector((state) => state.user.name);
  return <p>안녕하세요, {name}님!</p>;
}

function App() {
  return (
    <Provider store={store}>
      <GrandChild />
    </Provider>
  );
}

 

  • GrandChild.jsx

여기서 GrandChild는 부모 컴포넌트가 어떤 데이터도 props로 주지 않았는데도, 전역 상태인 name을 직접 사용할 수 있게 됩니다.

import { useSelector } from 'react-redux';

function GrandChild() {
  const name = useSelector((state) => state.user.name);
  return <p>안녕하세요, {name}님!</p>;
}

 

 

Redux 라이브러리에 대해 더 궁금하시다면 아래 공식 문서를 참고해주시면 좋을 것 같습니다!

https://redux-toolkit.js.org/ 

 

5. 컴포넌트 최적화

성능 문제를 해결하기 위한 또 다른 방법은 메모이제이션입니다.

 

🔎 메모이제이션(Memoization)

이전 계산 결과를 기억해두고, 같은 입력이 들어오면 다시 계산하지 않고 결과를 재사용하는 최적화 기법입니다.

 

React에서는 다음과 같은 불필요한 컴포넌트 리렌더링을 막는 도구들을 제공합니다.

React.memo(Component) props가 바뀌지 않으면 컴포넌트를 다시 렌더링하지 않음
useMemo(() => ..., [deps]) 값 계산 결과를 의존성(deps)이 바뀔 때만 다시 계산
useCallback(() => ..., [deps]) 함수를 메모이제이션해 불필요한 함수 재생성 방지

 

이 도구들을 활용하면 Prop Drilling으로 인한 성능 저하를 최소화할 수 있습니다.

 

const Child = React.memo(({ name }) => {
  return <p>안녕하세요, {name}님!</p>;
});

function App() {
	const [count, setCount] = useState(0);
	
  return <Child name="스웹" />;
  <button onClick={() => setCount(count + 1)}>+1</button>
}

Child는 React.memo로 감싸져 있으므로, props.name이 바뀌지 않으면 버튼 클릭으로 App이 리렌더링되어도 Child는 리렌더링되지 않습니다.

 

이러한 방법은 불필요한 렌더링을 방지해 성능 최적화 기능이 있지만, 오용한다면 비용이 더 크게 발생할 수 있습니다.

따라서, 자주 렌더링되는 큰 컴포넌트나 계산량이 많은 로직에 사용하는 걸 권장합니다.

 


🪛 마치며

Prop Drilling은 애플리케이션의 규모가 커질수록 유지보수와 성능에 큰 영향을 미칠 수 있습니다.

상황에 맞는 해결 방법을 선택하는 것이 중요하며, 적절히 활용하면 더 깔끔하고 효율적인 코드로 애플리케이션을 구축할 수 있을 것입니다.

읽어 주셔서 감사합니다! ☺️

 

 

https://ko.react.dev/learn/passing-data-deeply-with-context

https://www.geeksforgeeks.org/what-is-prop-drilling-and-how-to-avoid-it/

https://velog.io/@rachel28/Prop-Drilling

https://velog.io/@ahsy92/기술면접-상태관리와-Props-Drilling