서론
React로 프로젝트 만들다가 컴포넌트 구조가 깊어지니까 답답한 상황이 생기는걸 볼 수 있음
최상위 컴포넌트에 있는 데이터를 맨 아래 컴포넌트에서 써야 하는데, 중간에 있는 컴포넌트들한테도 전부 props로 넘겨줘야 했음
중간 컴포넌트들은 그 데이터를 쓰지도 않는데 단순히 전달만 하는 용도로 props를 받고 있었음
"이거 뭔가 비효율적인데?" 싶어서 찾아보니까 이게 props drilling이라는 문제였고, 해결 방법도 있더라
그래서 props drilling이 뭔지, 어떻게 해결하는지 정리해봄
본론
props drilling은 상위 컴포넌트의 데이터를 하위 컴포넌트로 전달하기 위해 중간 컴포넌트들을 거쳐가는 현상을 말함
쉽게 말하면 데이터를 아래로 계속 파내려가는(drilling) 느낌
props drilling이 왜 문제일까?
문제점 정리
- 중간 컴포넌트들이 사용하지 않는 props를 받아서 전달만 함
- 컴포넌트 구조가 복잡해질수록 props 추적이 어려워짐
- 코드 가독성이 떨어지고 유지보수가 힘들어짐
- props 이름 바꾸거나 구조 변경하면 중간 컴포넌트 전부 수정해야 함
props drilling 예시
function App() {
const [user, setUser] = useState({ name: '홍길동', age: 25 });
return <Parent user={user} />;
}
function Parent({ user }) {
// Parent는 user를 사용하지 않지만 Child에게 전달하기 위해 받음
return <Child user={user} />;
}
function Child({ user }) {
// Child도 user를 사용하지 않지만 GrandChild에게 전달하기 위해 받음
return <GrandChild user={user} />;
}
function GrandChild({ user }) {
// 여기서 실제로 user 사용
return <div>{user.name}님 환영합니다</div>;
}
위 코드를 보면 Parent와 Child는 user를 쓰지도 않는데 단순히 전달만 하고 있음
컴포넌트가 3-4개면 괜찮은데 10개 이상 깊어지면 정말 답답해짐
해결법 1: Context API 사용
React에서 제공하는 Context API를 쓰면 중간 컴포넌트를 거치지 않고 데이터를 전달할 수 있음
Context API 사용 예시
import { createContext, useContext, useState } from 'react';
// 1. Context 생성
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: '홍길동', age: 25 });
// 2. Provider로 감싸서 하위 컴포넌트들이 접근 가능하게 함
return (
<UserContext.Provider value={user}>
<Parent />
</UserContext.Provider>
);
}
function Parent() {
// props 받을 필요 없음!
return <Child />;
}
function Child() {
// props 받을 필요 없음!
return <GrandChild />;
}
function GrandChild() {
// 3. useContext로 바로 데이터 가져옴
const user = useContext(UserContext);
return <div>{user.name}님 환영합니다</div>;
}
Context API 장점
- 중간 컴포넌트들이 props를 전달할 필요 없어짐
- 코드가 훨씬 깔끔해짐
- 필요한 컴포넌트에서만 데이터를 가져와서 씀
Context API 단점
- Context 값이 바뀌면 해당 Context를 사용하는 모든 컴포넌트가 리렌더링됨
- 너무 많은 Context를 만들면 오히려 복잡해질 수 있음
해결법 2: 상태 관리 라이브러리 사용
프로젝트 규모가 크면 Zustand, Recoil, Redux 같은 상태 관리 라이브러리를 쓰는 것도 방법임
Zustand 예시 (가장 간단함)
import create from 'zustand';
// 1. 스토어 생성
const useUserStore = create((set) => ({
user: { name: '홍길동', age: 25 },
setUser: (user) => set({ user }),
}));
function App() {
return <Parent />;
}
function Parent() {
return <Child />;
}
function Child() {
return <GrandChild />;
}
function GrandChild() {
// 2. 어디서든 스토어에서 데이터 가져옴
const user = useUserStore((state) => state.user);
return <div>{user.name}님 환영합니다</div>;
}
상태 관리 라이브러리 장점
- 전역 상태를 한 곳에서 관리할 수 있음
- 어느 컴포넌트에서든 쉽게 접근 가능
- 상태 업데이트 로직을 분리해서 관리할 수 있음
상태 관리 라이브러리 단점
- 추가 라이브러리를 설치해야 함
- 작은 프로젝트에선 오히려 복잡해질 수 있음
해결법 3: 컴포넌트 구조 개선
때로는 컴포넌트 구조 자체를 다시 설계하는 게 답일 수도 있음
컴포넌트 합성 패턴
function App() {
const [user, setUser] = useState({ name: '홍길동', age: 25 });
// GrandChild를 직접 App에서 만들어서 넘김
return (
<Parent>
<Child>
<GrandChild user={user} />
</Child>
</Parent>
);
}
function Parent({ children }) {
return <div className="parent">{children}</div>;
}
function Child({ children }) {
return <div className="child">{children}</div>;
}
function GrandChild({ user }) {
return <div>{user.name}님 환영합니다</div>;
}
이렇게 하면 중간 컴포넌트들은 children만 받아서 렌더링하면 되고, 실제 데이터는 필요한 곳에만 전달됨
결론
props drilling은 컴포넌트 구조가 깊어질 때 자연스럽게 발생하는 문제임
해결 방법은 여러 가지가 있는데 상황에 맞게 선택하면 됨
- 간단한 전역 상태 관리: Context API
- 복잡한 전역 상태 관리: Zustand, Recoil 같은 라이브러리
- 구조 개선 가능: 컴포넌트 합성 패턴
개인적으로는 작은 프로젝트면 Context API, 큰 프로젝트면 Zustand 쓰는 게 좋은 것 같음
처음엔 "그냥 props 넘기면 되는데 뭐가 문제야?" 싶었는데, 프로젝트 커지니까 props drilling이 진짜 답답하더라
앞으로는 컴포넌트 설계할 때부터 이런 문제를 고려해야겠음!
'프론트엔드 > React' 카테고리의 다른 글
| (React) Framer Motion 애니메이션 만들어보기 (0) | 2025.12.01 |
|---|---|
| (React) useMemo vs useCallback 차이 (0) | 2025.11.20 |
| (React) useRef는 언제쓰는걸까? (0) | 2025.11.17 |
| (React) 리액트란? (0) | 2025.11.16 |
| (React) 전역 상태 관리 (0) | 2025.05.11 |