데이터 흐름 탐색: Prop Drilling 이해 및 해결 방법
Emily Parker
Product Engineer · Leapcell

소개
프론트엔드 개발의 역동적인 세계에서 복잡한 사용자 인터페이스를 구축하는 것은 종종 상호 연결된 컴포넌트들의 그물을 관리하는 것을 의미합니다. 애플리케이션이 성장함에 따라 이러한 컴포넌트 간에 데이터를 전달하는 복잡성도 증가합니다. 부모에서 자식으로의 데이터 흐름은 때때로 프론트엔드 개발자들이 자주 마주치는 일반적인 함정에 부딪힐 수 있습니다: Prop Drilling(속성 드릴링). 이 프레임워크들을 작업할 때 유지 관리 가능하고, 읽기 쉬우며, 효율적인 코드를 작성하려면 이 어려움을 이해하는 것이 중요합니다. 이 글에서는 Prop Drilling이 무엇인지, 잠재적인 단점은 무엇인지 살펴본 후, React Context 및 Vue Provide/Inject와 같은 우아한 해결책이 데이터 흐름을 간소화하고 애플리케이션의 전반적인 아키텍처를 개선하는 강력한 방법을 어떻게 제공하는지 보여줄 것입니다.
Prop Drilling과 그 대안 파헤치기
해결책으로 들어가기 전에, 관련된 핵심 개념에 대한 명확한 이해를 확립해 봅시다.
Prop Drilling이란 무엇인가?
Prop Drilling은 데이터가 실제로 필요하지 않은 중간 컴포넌트들을 여러 개 통과하여 부모 컴포넌트에서 깊게 중첩된 자식 컴포넌트로 데이터를 전달하는 프로세스를 의미합니다. 조부모가 손주에게 전달할 메시지가 있지만, 메시지가 최종 목적지에 도달하기 위해 부모를 거쳐 자식을 통해 물리적으로 전달해야 하는 부모-자식 관계의 체인을 상상해 보세요. 각 중간 컴포넌트는 실제로 데이터를 소비하거나 수정하지 않고 단순히 속성을 전달하는 릴레이 역할을 합니다.
예시: React에서의 Prop Drilling
다음 React 구조를 고려해 보세요:
// GrandparentComponent.js function GrandparentComponent() { const user = { name: "Alice", theme: "dark" }; return <ParentComponent user={user} />; } // ParentComponent.js function ParentComponent({ user }) { // ParentComponent는 'user'를 직접 사용하지 않지만, 아래로 전달합니다. return <ChildComponent user={user} />; } // ChildComponent.js function ChildComponent({ user }) { // ChildComponent는 'user'를 직접 사용하지 않지만, 아래로 전달합니다. return <GrandchildComponent user={user} />; } // GrandchildComponent.js function GrandchildComponent({ user }) { return ( <div> Hello, {user.name}! Your theme is {user.theme}. </div> ); }
이 예시에서 user 속성은 ParentComponent와 ChildComponent를 통과하여 GrandchildComponent에 도달하도록 "드릴링"되었습니다.
Prop Drilling의 단점
작은 애플리케이션에서는 간단해 보이지만, Prop Drilling은 컴포넌트 트리가 성장함에 따라 여러 가지 문제를 야기할 수 있습니다:
- 가독성 저하: 특히 대규모 코드베이스에서 속성이 어디에서 시작되고 최종 목적지가 어디인지 추적하기가 더 어려워집니다.
- 유지 관리 오버헤드 증가: 속성 이름이 변경되거나 구조가 수정되면 단순히 전달만 하는 여러 중간 컴포넌트를 업데이트해야 할 수 있습니다. 이는 취약한 코드로 이어질 수 있습니다.
- 캡슐화 위반: 중간 컴포넌트는 필요하지 않은 데이터에 대해 알게 되어 캡슐화 원칙을 위반하고 재사용성을 떨어뜨립니다.
- 성능 우려 (드물지만 가능): React와 Vue는 최적화되어 있지만, 매우 크고 깊게 중첩된 트리에서는 불필요하게 중간 컴포넌트를 다시 렌더링하면 성능에 미묘한 영향을 줄 수 있습니다.
드릴링 탈출: React Context와 Vue Provide/Inject
React와 Vue는 각 레벨에서 명시적으로 속성을 전달하지 않고도 컴포넌트 트리 전체에서 데이터를 공유할 수 있는 방법을 제공하여 Prop Drilling을 해결하는 강력한 메커니즘을 제공합니다.
React Context API
React Context는 속성을 수동으로 각 레벨로 전달할 필요 없이 컴포넌트 트리를 통해 데이터를 전달하는 방법을 제공합니다. 이는 React 컴포넌트 트리에 대해 "전역적"이라고 간주될 수 있는 "전역" 데이터(현재 인증된 사용자, 테마 또는 기본 언어와 같은)를 공유하도록 설계되었습니다.
작동 방식:
- Context 생성:
React.createContext()를 사용하여Context객체를 생성합니다. 이 객체에는Provider와Consumer컴포넌트가 있습니다. - 값 제공:
Provider컴포넌트는 컴포넌트 트리에서 더 높은 곳에 배치됩니다.value속성을 받아들이며, 이 값은Provider의 모든 하위 요소에서 사용할 수 있게 됩니다. - 값 소비: 하위 컴포넌트는
useContext훅 (함수형 컴포넌트의 경우) 또는Consumer컴포넌트 (클래스 컴포넌트의 경우)를 사용하여 이 값을 "소비"할 수 있습니다.
예시: 작동하는 React Context
이전 예시를 React Context를 사용하여 리팩터링해 보겠습니다:
// ThemeContext.js import React, { createContext, useContext } from 'react'; const ThemeContext = createContext(null); export const ThemeProvider = ({ children, user }) => { return ( <ThemeContext.Provider value={user}> {children} </ThemeContext.Provider> ); }; export const useTheme = () => useContext(ThemeContext); // GrandparentComponent.js import React from 'react'; import { ThemeProvider } from './ThemeContext'; import ParentComponent from './ParentComponent'; function GrandparentComponent() { const user = { name: "Alice", theme: "dark" }; return ( <ThemeProvider user={user}> <ParentComponent /> </ThemeProvider> ); } // ParentComponent.js import React from 'react'; import ChildComponent from './ChildComponent'; // ParentComponent는 더 이상 'user' 속성을 필요로 하지 않습니다. function ParentComponent() { return <ChildComponent />; } // ChildComponent.js import React from 'react'; import GrandchildComponent from './GrandchildComponent'; // ChildComponent는 더 이상 'user' 속성을 필요로 하지 않습니다. function ChildComponent() { return <GrandchildComponent />; } // GrandchildComponent.js import React from 'react'; import { useTheme } from './ThemeContext'; function GrandchildComponent() { const user = useTheme(); // user 객체를 직접 소비합니다. return ( <div> Hello, {user.name}! Your theme is {user.theme}. </div> ); }
ParentComponent와 ChildComponent는 이제 user 데이터에 대해 완전히 알지 못합니다. GrandchildComponent는 useTheme 훅을 통해 직접 접근합니다.
Vue Provide/Inject
Vue의 provide 및 inject 옵션 (Composition API의 provide 및 inject 함수)은 React Context와 유사한 목적을 제공합니다. 부모 컴포넌트가 컴포넌트 계층 구조의 깊이에 관계없이 모든 하위 요소에 대한 종속성 제공자 역할을 할 수 있도록 합니다.
작동 방식:
- 값 제공: 부모 컴포넌트는
provide옵션 (Options API) 또는provide()함수 (Composition API)를 사용하여 값을 제공합니다. 이 값은 기본형, 객체 또는 반응형 속성일 수도 있습니다. - 값 주입: 어떤 하위 컴포넌트든
inject옵션 (Options API) 또는inject()함수 (Composition API)를 사용하여 이 제공된 값을 주입할 수 있으며,provide중에 사용된 것과 동일한 키를 참조합니다.
예시: 작동하는 Vue Provide/Inject
예시를 Vue로 번역해 보겠습니다:
<!-- GrandparentComponent.vue --> <template> <ParentComponent /> </template> <script> import { provide, reactive } from 'vue'; import ParentComponent from './ParentComponent.vue'; export default { components: { ParentComponent, }, setup() { const user = reactive({ name: 'Alice', theme: 'dark' }); provide('userKey', user); // 반응형 user 객체 제공 }, }; </script> <!-- ParentComponent.vue --> <template> <ChildComponent /> </template> <script> import ChildComponent from './ChildComponent.vue'; export default { components: { ChildComponent, }, // ParentComponent는 더 이상 'user'에 대한 속성을 선언할 필요가 없습니다. }; </script> <!-- ChildComponent.vue --> <template> <GrandchildComponent /> </template> <script> import GrandchildComponent from './GrandchildComponent.vue'; export default { components: { GrandchildComponent, }, // ChildComponent는 더 이상 'user'에 대한 속성을 선언할 필요가 없습니다. }; </script> <!-- GrandchildComponent.vue --> <template> <div> Hello, {{ user.name }}! Your theme is {{ user.theme }}. </div> </template> <script> import { inject } from 'vue'; export default { setup() { const user = inject('userKey'); // user 객체 주입 return { user, }; }, }; </script>
React Context와 유사하게, ParentComponent와 ChildComponent는 이제 user 데이터와 분리되어 데이터 흐름을 더 깔끔하고 컴포넌트를 더 독립적으로 만듭니다.
결론
컴포넌트 기반 아키텍처의 자연스러운 결과인 Prop Drilling은 컴포넌트 트리를 빠르게 복잡하게 만들고 유지 관리를 어렵게 할 수 있습니다. React Context와 Vue Provide/Inject는 개발자가 중복 속성 전달을 우회하여 먼 컴포넌트 간에 직접 데이터 채널을 설정할 수 있도록 하는 우아하고 강력한 솔루션을 제공합니다. 이러한 패턴을 신중하게 적용함으로써 더 깨끗한 코드, 개선된 컴포넌트 재사용성, 그리고 프론트엔드 애플리케이션의 전반적인 아키텍처 무결성을 달성할 수 있습니다. 궁극적으로 이러한 도구는 더 적은 오버헤드로 더 명확하게 복잡한 데이터 흐름을 관리할 수 있도록 지원합니다.

