
본 포스트는 Udemy 리액트 완벽가이드 2024 를 듣고 정리한 내용입니다.
목차 🌳
Section 9: 프로젝트 관리 앱 만들기
Section 10:
1️⃣ prop Drilling
2️⃣ Component 합성을 통한 문제 해결
3️⃣ useContext 사용 ✨
4️⃣ Reducer 이란? ✨


리액트는 컴포넌트 Tree 로 구성되어 있음
특정 상태값 변경 State 을 감시해야 하는, 다른 위치의 컴포넌트 가 있는 경우!
(Tree에서 최상위 컴포넌트를 기점으로 갈라지는 경우 등)
prop을 통해 값의 공유와 업데이트가 이루어진다

위의 트리 형태에서 볼 수 있듯이, 여러 층의 컴포넌트를 거쳐 공유하는 상태값 state 을 넘겨주다보면
부모 -> 자식(부모) -> 자식(부모) -> 자식 과 같이 prop 이 연쇄적으로 인자를 통해 타고 내려가는 Prop Drilling 이 발생한다.
(중간 컴포넌트에서 그 값이 필요하지 않더라도, 자식 컴포넌트가 필요하면 끊기지 않게 일단 전달해줘야 함)
🥸 이게 왜 문제인가요?
1️⃣ 누락의 가능성
-타고 내려가는 구조 이기 때문에,필요한 prop이 타켓 컴포넌트에 전달되지 않은 경우, 어떤 중간 컴포넌트에서 누락된건지 타고 올라가면서 확인해야 함.
2️⃣ 중간 컴포넌트에서 불필요하게 prop을 전달
-간결하지 못하고, 불필요한 코드의 중복 발생
3️⃣ 이름을 통해 prop 을 추적하기 어려움
-prop 이름이 계층을 타고 내려가면서 변경되면, 그 값을 추적하고 업데이트 하기 어려움
컴포넌트 합성을 통해,
prop을 다시 전달하는 부분을 부모쪽으로 올려서 구성하는 방법이다.
이로써, 컴포넌트의 중첩을 한 층 삭제하여 프롭 드릴링 일부 제거 가능!
컴포넌트 합성 전
App -> Shop -> Product로 handleAddItemToCart 함수 전달
App 컴포넌트
function App() {
... 생략
return (
<>
<Header ... 생략 .../>
<Shop onAddItemToCart={handleAddItemToCart} />
</>
);
}
Shop 컴포넌트
export default function Shop({ onAddItemToCart }) {
return (
<section id="shop">
<ul id="products">
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} onAddToCart={onAddItemToCart} />
</li>
))}
</ul>
</section>
);
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️✨ 컴포넌트 합성 이후 ✨ ⬇️⬇️⬇️⬇️⬇️⬇️⬇️
컴포넌트 합성 후
App -> Product로 handleAddItemToCart 함수 전달
App 컴포넌트
function App() {
... 생략
return (
<>
<Header ... 생략 .../>
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} onAddToCart={handleAddItemToCart} />
</li>
))}
</Shop>
</>
);
}
그치만 컴포넌트 합성은 최고의 선택이 아님 🙃
왜나면!
모든 컴포넌트의 층에 사용하기 적합하지 않음
-> 결국 모든 컴포넌트가 App.jsx 에 있는 App component에 들어가고 나머지 컴포넌트는 감싸는 용도로만 사용되기 때문에 App component가 매우 커짐 주의!! ⚠️

Context를 통해,prop사용없이 컴포넌트 트리 전역적으로 데이터 제공 가능!
엄밀히 말하자면, 컨텍스트 안에 묶인 컴포넌트간 데이터 공유가 자유로운 것!✨
그래서 어떻게 사용하나요? 🤨
1️⃣
createContext: context 객체 생성
-> 관습이지만,store폴더에이름-store.jsx파일 안에서 생성
-> 단, createContext로 만든 객체는 첫글자를 대문자로 사용하기
(왜냐하면, 리액트 컴포넌트가 들어있는 객체이기 때문)
2️⃣
Provider: 데이터를 공유할 컴포넌트들을 Context 로 묶어주는 역할 (공유 데이터 전달해주는 역할)
->context이름.Providerwrapper를 통해 값에 접근할 컴포넌트들을 묶어줌
-> ex.<CartContext.Provider> ... 컴포넌트들... </CartContext.Provider>
✨ 이때! <CartContext.Provider> 안에 공유하고자 하는 값을 value 라는 prop으로 설정하면 자식 컴포넌트에서 해당 값에 바로 접근 가능하다!!!
자동완성 기능 쓸 수 있게 해줌! -> 기본값으로 설정되어, 구조분해를 할 수도 있음!✨
그렇기 때문에,
function App () {
... 생략
const ctxValue = {
items: shoppingCart.items, // state 값
addItemToCart: handleAddItemToCart, //함수
};
return (
<CartContext.Provider value={ctxValue}>
... 생략
</CartContext.Provider>
);
}
다음과 같이 객체로 같이 넘겨주게 되면, context를 받는 자식 컴포넌트에서 구조분해할당하여 사용 가능!
const { addItemToCart } = useContext(CartContext);
const { items } = useContext(CartContext);
🚨 단, store안에서 creatContext 도 value 값과 동일하게 선언해주어야 함
import { createContext } from 'react';
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
});
3️⃣
useContext: 받아온 데이터를 실제 사용하기 위해,Context불러오는 방법
->useContext(Context이름)으로 받아와 사용하면 된다~
Provider과 마찬가지로 Wrapper로써 내부를 감싸긴 하는데, 그 안에 함수를 랜더링해야 한다.
<CartContext.Consumer>
// Consumer 사용하면, 함수를 렌더링 해야하며, provider의 value을 인자로 함
{(cartCtx) => <div>{cartCtx.items}</div>}
</CartContextt.Consumer>
Consumer 실제 사용 코드
import { CartContext } from '../store/shopping-cart-store';
export default function Cart({ onUpdateItemQuantity }) {
return (
<CartContext.Consumer>
{(cartCtx) => {
const totalPrice = cartCtx.items.reduce((acc, item) => acc + item.price * item.quantity, 0);
const formattedTotalPrice = `$${totalPrice.toFixed(2)}`;
return (
<div id="cart">
{cartCtx.items.length === 0 && <p>No items in cart!</p>}
{cartCtx.items.length > 0 && (
<ul id="cart-items">
{cartCtx.items.map((item) => {
const formattedPrice = `$${item.price.toFixed(2)}`;
return (
<li key={item.id}>
<div>
<span>{item.name}</span>
<span> ({formattedPrice})</span>
</div>
<div className="cart-item-actions">
<button onClick={() => onUpdateItemQuantity(item.id, -1)}>-</button>
<span>{item.quantity}</span>
<button onClick={() => onUpdateItemQuantity(item.id, 1)}>+</button>
</div>
</li>
);
})}
</ul>
)}
<p id="cart-total-price">
Cart Total: <strong>{formattedTotalPrice}</strong>
</p>
</div>
);
}}
</CartContext.Consumer>
);
}
단점
코드가 길고 복잡하고, 읽기 어려움
-> 함수 안에 넣어서 return 해야 하기 때문에 좀 번거로움
혹시 Consumer로 적힌 코드가 있으면, 어떻게 읽고 고치면 좋을지 알아볼 수 있게 하기 위한 용으로 알아두기! (context 기본 사용법으로는 비추)
Context를 분리하여, 이 안에 state 부여 해주면!App 컴포넌트를 간결하게 관리 가능!
분리 전
import { useState } from 'react';
import Header from './components/Header.jsx';
import Shop from './components/Shop.jsx';
import Product from './components/Product.jsx';
import { CartContext } from './store/shopping-cart-store.jsx';
import { DUMMY_PRODUCTS } from './dummy-products.js';
function App() {
const [shoppingCart, setShoppingCart] = useState({
items: [],
});
function handleAddItemToCart(id) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const existingCartItemIndex = updatedItems.findIndex((cartItem) => cartItem.id === id);
const existingCartItem = updatedItems[existingCartItemIndex];
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
updatedItems[existingCartItemIndex] = updatedItem;
} else {
const product = DUMMY_PRODUCTS.find((product) => product.id === id);
updatedItems.push({
id: id,
name: product.title,
price: product.price,
quantity: 1,
});
}
return {
items: updatedItems,
};
});
}
function handleUpdateCartItemQuantity(productId, amount) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const updatedItemIndex = updatedItems.findIndex((item) => item.id === productId);
const updatedItem = {
...updatedItems[updatedItemIndex],
};
updatedItem.quantity += amount;
if (updatedItem.quantity <= 0) {
updatedItems.splice(updatedItemIndex, 1);
} else {
updatedItems[updatedItemIndex] = updatedItem;
}
return {
items: updatedItems,
};
});
}
const ctxValue = {
items: shoppingCart.items,
addItemToCart: handleAddItemToCart,
updateItemQuantity: handleUpdateCartItemQuantity,
};
return (
<CartContext.Provider value={ctxValue}>
<Header />
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} />
</li>
))}
</Shop>
</CartContext.Provider>
);
}
export default App;
⬇️⬇️⬇️⬇️⬇️⬇️✨ context 관련 함수 및 상태 store 파일로 이동 ✨⬇️⬇️⬇️⬇️⬇️⬇️
분리 후
shopping-cart-store.jsx
import { createContext, useState } from 'react';
import { DUMMY_PRODUCTS } from '../dummy-products';
// context 생성
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateItemQuantity: () => {},
});
// context provider
export default function CartContextProvider({ children }) {
const [shoppingCart, setShoppingCart] = useState({
items: [],
});
function handleAddItemToCart(id) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const existingCartItemIndex = updatedItems.findIndex((cartItem) => cartItem.id === id);
const existingCartItem = updatedItems[existingCartItemIndex];
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
updatedItems[existingCartItemIndex] = updatedItem;
} else {
const product = DUMMY_PRODUCTS.find((product) => product.id === id);
updatedItems.push({
id: id,
name: product.title,
price: product.price,
quantity: 1,
});
}
return {
items: updatedItems,
};
});
}
function handleUpdateCartItemQuantity(productId, amount) {
setShoppingCart((prevShoppingCart) => {
const updatedItems = [...prevShoppingCart.items];
const updatedItemIndex = updatedItems.findIndex((item) => item.id === productId);
const updatedItem = {
...updatedItems[updatedItemIndex],
};
updatedItem.quantity += amount;
if (updatedItem.quantity <= 0) {
updatedItems.splice(updatedItemIndex, 1);
} else {
updatedItems[updatedItemIndex] = updatedItem;
}
return {
items: updatedItems,
};
});
}
const ctxValue = {
items: shoppingCart.items,
addItemToCart: handleAddItemToCart,
updateItemQuantity: handleUpdateCartItemQuantity,
};
return <CartContext.Provider value={ctxValue}>{children}</CartContext.Provider>;
}
App 컴포넌트
import Header from './components/Header.jsx';
import Shop from './components/Shop.jsx';
import Product from './components/Product.jsx';
import { DUMMY_PRODUCTS } from './dummy-products.js';
import CartContextProvider from './store/shopping-cart-store.jsx';
function App() {
return (
<CartContextProvider> // 아웃소싱한 Provider 컴포넌트 사용하기
<Header />
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} />
</li>
))}
</Shop>
</CartContextProvider>
);
}
export default App;
useState와 마찬가지로, 상태 관리 훅이다!
근데 둘중 하나만 쓰면 되지 왜 useReducer도 있냐고?🤨
- 여러개의 하위값을 포함하는 복잡한 state 관리에 유용하기 때문!
- 또한, 구조화된 방식으로 상태 관리 가능
그래서 어떻게 사용하나요? 🤨
const [state, dispatch] = useReducer(reducer_리듀서함수, initialState_초기값)
function reducer (state, action) {}
🤝 useReducer 훅을 이용해서 상태값과, dispatch를 선언해주고,
인자로 action이 담긴 함수를 첫번째로 그리고 state의 초기값을 두번째로 준다.
🤝 action들이 담긴 reducer 함수는 인자로 가장 최신의 state값과, action을 받는다.
내부에는 조건문 (if문이나, switch문)을 사용하여 type에 따른 함수를 작성한다.
(꼭 type이라고 할 필요는 없고, identifier 등 자유 -> 대신 사용시 action.type과 같이 action으로 전달됨을 명심할 것)
🤝 state를 바꾸고 싶을때는, dispatch에 인자로 어떤 action을 취할지 보내면 된다.
dispatch({type: 'ADD_ITEM'})
혹시 같이 전달되어야 할 인자가 있다면, dispatch 인자로 같이 넘기면 되는데,
관습으로는 payload 객체 안에 넣어서 전달해준다.
dispatch({
type: 'ADD_ITEM'
payload: {
id,
product
}
})
1️⃣
dispatch: 상태를 업데이트 해주는 함수, action 내용을전달해줌 (주문서)
Dispatch(Action)
- dispatch 함수에 Action을 인자로 넣어서, Reducer로 전달해주면 됨.
dispatch를 호출하면 -> reducer 함수가 돌아감
2️⃣
state: 상태값
->context이름.Providerwrapper를 통해 값에 접근할 컴포넌트들을 묶어줌
3️⃣
action: state를 어떻게 변경하고 싶은지 요구사항 -> dispatch로 전달됨(주문)
4️⃣
reducer: dispatch 를 확인하여, action대로 state를 변경하는 함수function reducer (state, action) {}
- 디스패치에 action이 전달되어 호출되면, 리액트가 reducer를 호출함 (그래서 인자에 action이 있는거)
- 첫번째 인자인 state는 useReducer로 관리되는
상태의 보장된 최신값!!
-> 이때, reducer 함수는 useReducer가 선언된 컴포넌트 밖에 선언한다.
밖에 만드는 이유) 리액트 컴포넌트가 재랜더링될때마다, reducer의 불필요한 재랜더링을 막기 위해
reducer 함수 예시
function shoppingCartReducer(state, action) {
if (action.type === 'ADD_ITEM') {
const updatedItems = [...state.items];
const existingCartItemIndex = updatedItems.findIndex((cartItem) => cartItem.id === action.payload);
const existingCartItem = updatedItems[existingCartItemIndex];
if (existingCartItem) {
const updatedItem = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
updatedItems[existingCartItemIndex] = updatedItem;
} else {
const product = DUMMY_PRODUCTS.find((product) => product.id === action.payload);
updatedItems.push({
id: action.payload,
name: product.title,
price: product.price,
quantity: 1,
});
}
return {
items: updatedItems,
};
}
if (action.type === 'UPDATE_ITEM') {
const updatedItems = [...state.items];
const updatedItemIndex = updatedItems.findIndex((item) => item.id === action.payload.productId);
const updatedItem = {
...updatedItems[updatedItemIndex],
};
updatedItem.quantity += action.payload.amount;
if (updatedItem.quantity <= 0) {
updatedItems.splice(updatedItemIndex, 1);
} else {
updatedItems[updatedItemIndex] = updatedItem;
}
return {
...state,
items: updatedItems,
};
}
return state;
}
useReducer과 dispatch 사용 예시
const [shopingCartState, shoppingCartDispatch] = useReducer(shoppingCartReducer, {
items: [],
});
function handleAddItemToCart(id) {
shoppingCartDispatch({
type: 'ADD_ITEM',
payload: id,
});
}
function handleUpdateCartItemQuantity(productId, amount) {
shoppingCartDispatch({
type: 'UPDATE_ITEM',
payload: {
productId,
amount,
},
});
}