React 애플리케이션에서는 상위 컴포넌트의 데이터를 여러 중간 컴포넌트를 거쳐서 최종적으로 하위 컴포넌트에 전달해야 할 때, 매 단계마다 props로 데이터를 넘겨주는 prop-drilling 문제가 발생할 수 있다. 이 방식은 컴포넌트 구조가 깊어질수록 관리가 어렵고, 불필요한 props 전달로 인해 코드가 지저분해질뿐만 아니라, 리랜더링 과정이 불필요하게 일어나기 때문에 상당히 안좋다.
이번 포스트에서는 prop-drilling의 문제를 간단한 예제로 보여주고, 이를 useContext를 활용해 보다 깔끔하게 바꾸는 법을 쓰려고 한다.
먼저, 간단한 사용자 정보를 전달하는 예제를 통해 알아보자. 여기서는 Parent 컴포넌트에서 사용자 객체를 생성한 후, Child와 GrandChild 컴포넌트를 거쳐 최종적으로 GrandChild에서 해당 정보를 사용하는 예제이다.
// Parent.jsx
import React from "react";
import Child from "./Child";
const Parent = () => {
const user = { name: "John Doe", age: 30 };
return (
<div>
<h1>Parent Component</h1>
<Child user={user} />
</div>
);
};
export default Parent;
// Child.jsx
import React from "react";
import GrandChild from "./GrandChild";
const Child = ({ user }) => {
return (
<div>
<h2>Child Component</h2>
<GrandChild user={user} />
</div>
);
};
export default Child;
// GrandChild.jsx
import React from "react";
const GrandChild = ({ user }) => {
return (
<div>
<h3>GrandChild Component</h3>
<p>User Name: {user.name}</p>
<p>User Age: {user.age}</p>
</div>
);
};
export default GrandChild;
Parent에서 생성한 user 객체를 Child를 통해 GrandChild에 계속 전달해야 한다. 만약 사용자 정보가 여러 컴포넌트에서 필요하거나, 컴포넌트 계층이 더 깊어진다면 매번 props로 넘겨줘야 하므로 관리가 복잡해진다.
useContext를 사용하면, 별도의 중간 컴포넌트에 props를 전달할 필요 없이 전역 상태처럼 데이터를 사용할 수 있다. 위 예제를 useContext를 활용해서 바꿔보자.
2.1. Context 생성
먼저, 사용자 정보를 담을 Context를 생성(파일을 따로)
// UserContext.js
import React, { createContext, useState } from "react";
export const UserContext = createContext();
export const UserProvider = ({ children }) => {
const [user] = useState({ name: "John Doe", age: 30 });
return (
<UserContext.Provider value={user}>{children}</UserContext.Provider>
);
};
여기서는 간단하게 user 객체만 전역에서 관리하도록 했지만, 상태 업데이트 함수도 함께 제공가능
2.2. Provider로 앱 최상단 감싸기
UserProvider로 최상위 컴포넌트를 감싸서, 하위 컴포넌트들이 Context에 접근할 수 있도록 한다.
// App.jsx
import React from "react";
import Parent from "./Parent";
import { UserProvider } from "./UserContext";
function App() {
return (
<UserProvider>
<Parent />
</UserProvider>
);
}
export default App;
이렇게 하면, Parent를 포함한 모든 하위 컴포넌트는 user 데이터를 Context를 통해 접근 가능
2.3. 중간 컴포넌트에서는 더 이상 props 전달이 필요 없음
이제 Child와 GrandChild에서 더 이상 props로 user를 전달할 필요가 없음
// Child.jsx (리팩토링 전)
import React from "react";
import GrandChild from "./GrandChild";
const Child = ({ user }) => {
return (
<div>
<h2>Child Component</h2>
<GrandChild user={user} />
</div>
);
};
export default Child;
// Child.jsx (리팩토링 후)
import React from "react";
import GrandChild from "./GrandChild";
const Child = () => {
return (
<div>
<h2>Child Component</h2>
<GrandChild />
</div>
);
};
export default Child;
2.4. 최종 하위 컴포넌트에서 Context 사용
GrandChild 컴포넌트에서 useContext 훅을 사용해 user 데이터를 가져오기
// GrandChild.jsx (리팩토링 후)
import React, { useContext } from "react";
import { UserContext } from "./UserContext";
const GrandChild = () => {
const user = useContext(UserContext);
return (
<div>
<h3>GrandChild Component</h3>
<p>User Name: {user.name}</p>
<p>User Age: {user.age}</p>
</div>
);
};
export default GrandChild;
이제 GrandChild는 직접 Context에서 user 객체를 읽어오므로, 중간 컴포넌트로부터 props를 전달받을 필요가 없음
1. Redux Toolkit 설치 및 기본 설정
먼저 RTK와 React-Redux를 설치
yarn add @reduxjs/toolkit react-redux
2. Slice 생성
RTK의 createSlice를 사용하여 user 상태를 관리할 slice를 만듭니다.
// userSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
user: { name: "John Doe", age: 30 }
};
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
// 만약 사용자 정보를 업데이트하는 액션이 필요하다면 여기 추가 가능
updateUser(state, action) {
state.user = action.payload;
},
},
});
export const { updateUser } = userSlice.actions;
export default userSlice.reducer;
3. Store 생성
// store.js
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice";
const store = configureStore({
reducer: {
user: userReducer,
},
});
export default store;
4.Provider로 앱 최상단 감싸기
// App.jsx
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import Parent from "./Parent";
function App() {
return (
<Provider store={store}>
<Parent />
</Provider>
);
}
export default App;
이제 Child 컴포넌트는 더 이상 user props를 전달할 필요가 없음.
// Child.jsx (리팩토링 후)
import React from "react";
import GrandChild from "./GrandChild";
const Child = () => {
return (
<div>
<h2>Child Component</h2>
<GrandChild />
</div>
);
};
export default Child;
6. 최종 하위 컴포넌트에서 Redux state 사용
// GrandChild.jsx (리팩토링 후)
import React from "react";
import { useSelector } from "react-redux";
const GrandChild = () => {
const user = useSelector((state) => state.user.user);
return (
<div>
<h3>GrandChild Component</h3>
<p>User Name: {user.name}</p>
<p>User Age: {user.age}</p>
</div>
);
};
export default GrandChild;
Context 생성
전역 데이터 관리를 위한 Context와 Provider(UserContext)를 만들기
Provider 감싸기
최상위 컴포넌트(App 등)에서 Provider로 전체 컴포넌트 트리를 감싸기
중간 컴포넌트 정리
더 이상 props를 전달하지 않도록 중간 컴포넌트에서 관련 코드를 제거
하위 컴포넌트 사용
최종 하위 컴포넌트에서 useContext로 전역 데이터를 직접 읽어오기
| 비교 항목 | useContext | Redux Toolkit (RTK) |
|---|---|---|
| 설정 및 간편함 | React 내장 API로 간단하게 설정 가능 | 추가 라이브러리 설치 필요, 초기 설정 및 학습 곡선 있음 |
| 적용 대상 | 소규모 또는 간단한 전역 상태 관리에 적합 | 복잡하고 대규모 상태 관리에 유리함 |
| 성능 | 상태 변경 시 Provider 하위 모든 컴포넌트가 리렌더링될 수 있음 | 중앙 집중식 상태 관리와 미들웨어, DevTools 지원으로 성능 관리 용이 |
| 디버깅 및 도구 | 별도의 디버깅 도구가 없음 | Redux DevTools 등 강력한 디버깅 도구 제공 |
| 코드 구조 | 단순한 데이터 전달에 적합, 코드 구조가 간단함 | 상태, 액션, 리듀서 등 체계적으로 관리하여 유지보수에 유리함 |