더 복잡한 상태관리를 해보자...!
리액트 개발자는 주로 다수의 컴포넌트로 이루어진 앱을 만들어야 하는데,
앱이 복잡할수록, 더 많은 컴포넌트를 사용하게 된다.
보통 컴포넌트 트리로 구성이 되어있는데, 이 과정에서 상태관리가 필요하다..!!
Prop Drilling을 통해 다수의 컴포넌트를 거쳐 속성을 전달할 수 있다.
하지만 대부분의 컴포넌트가 그 데이터를 직접적으로 필요로 하지 않고, 하위로(목표까지) 전달해주는 역할만 수행한다.
=> 원하는 공유데이터를 얻기 위해 여러 컴포넌트를 거치게 되고,
이러한 방식은 컴포넌트들의 재사용성을 어렵게 한다! (+코드도 더 많이 작성해야 함..)
해결방식
- Component Composition
- Context API
- useReducer
Component Composition
컴포넌트 합성 이전 코드
// Shop.jsx
export default function Shop({ onAddItemToCart }) {
return (
<section id="shop">
<h2>Elegant Clothing For Everyone</h2>
<ul id="products">
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} onAddToCart={onAddItemToCart} />
</li>
))}
</ul>
</section>
);
}
// App.jsx
<Shop onAddToCart = {handleAddItemToCart}/>
컴포넌트 합성 코드
// Shop.jsx
export default function Shop({ children }) {
return (
<section id="shop">
<h2>Elegant Clothing For Everyone</h2>
<ul id="products">
{children}
</ul>
</section>
);
}
// App.jsx
<Shop>
{DUMMY_PRODUCTS.map((product) => (
<li key={product.id}>
<Product {...product} onAddToCart={handleAddItemToCart} />
</li>))}
</Shop>
컴포넌트 합성은 여전히 모든 층에 적용하기는 어렵다는 문제점이 있다.
결국 모든 컴포넌트가 앱 컴포넌트로 들어가고, 나머지는 감싸는 용도로만 사용되기 때문에..
React's Context API
컨텍스트 값을 생성 후 , 제공하고 여러 컴포넌트를 묶어준다.
=> 깊이 여부와 무관하게 데이터 공유 가능
src/store 폴더 내부에 관련 파일들 저장 (관습)
파일명은 뒤에 -context를 붙여줌으로써 컨텍스트 값을 관리하고 있다는 것을 명시해주자.
컨텍스트 데이터 생성 후, 필요로 하는 컴포넌트들에서 컨텍스트 정보를 가져오자.
기본 컨텍스트 값을 설정해두는 것이 좋다.
import React, { createContext } from 'react';
// 1. Context 생성
const MyContext = createContext();
function App() {
const value = { user: 'John Doe', isLoggedIn: true };
return (
// 2. Provider로 값을 전달
<MyContext.Provider value={value}>
<ChildComponent />
</MyContext.Provider>
);
}
Provider & Consumer
function ChildComponent() {
return (
<MyContext.Consumer>
{value => (
<div>
<p>User: {value.user}</p>
<p>Logged In: {value.isLoggedIn ? 'Yes' : 'No'}</p>
</div>
)}
</MyContext.Consumer>
);
}
Consumer 컴포넌트 : 특별한 함수 자식이 필요. context를 사용하는 또 다른 방법.
해당 함수는 백엔드에서 리액트에 의해 실행됨.
useContext (권장)
import { useContext } from 'react';
function ChildComponent() {
const value = useContext(MyContext); // Consumer 대신 useContext Hook 사용
return (
<div>
<p>User: {value.user}</p>
<p>Logged In: {value.isLoggedIn ? 'Yes' : 'No'}</p>
</div>
);
}
연결된 컨텍스트 값이 바뀌었다면, 리액트는 컴포넌트 함수를 재실행 함으로써 새로운 UI를 만들어 내도록 한다.
컨텍스트를 통해 공유될 값의 설정을 컴포넌트 내부에서 하고 있기 때문에, 여전히 조금 무거울 수 있다.
=> 컨텍스트 아웃소싱
복잡한 상태 관리
useState를 통해서 상태를 관리할 때,
setState(prevSnapshot) => { } 와 같은 형식으로 매번 이 패턴을 사용하기에는 조금 성가심.
useReducer
A function that reduce one or more complex values to a simpler one.
reduce 사용 예시
const totalPrice = cartCtx.items.reduce(
(acc, item) => acc + item.price * item.quantity,
0
);
useReducer 기본 형태
const [state, dispatch] = useReducer(reducer, initialState);
reducer : 상태를 업데이트하는 방식을 담고 있는 함수. state, action => newState의 형
dispatch : action을 실행시켜 상태를 변경
shopping-cart-context.jsx
import {createContext, useReducer, useState} from "react";
import {DUMMY_PRODUCTS} from "../dummy-products.js";
export const CartContext = createContext({
items: [],
addItemToCart: () => {},
updateItemQuantity : ()=> {},
});
Context 기본값. 값을 Provider로 제공받지 않았을 때 사용.
편집기에서 내부 구조를 알 수 있어, 코딩속도 향상.
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) {
updatedItems[existingCartItemIndex] = {
...existingCartItem,
quantity: existingCartItem.quantity + 1,
};
} 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 {
...state,
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;
}
action의 type에 따라 상태를 어떻게 업데이트할지의 내용을 담고 있으며,
...state, 를 통해 기존 객체의 모든 속성을 복사하고, items 속성만 새로 업데이트 한 후 반환한다.
만약 기존 객체를 수정하지 않고, 새로운 객체를 반환한다면 리렌더링이 발생한다..!
export default function CartContextProvider({children}){
const [shoppingCartState, shoppingCartDispatch] = useReducer(shoppingCartReducer);
const [shoppingCart, setShoppingCart] = useState({
items: [],
});
function handleAddItemToCart(id) {
shoppingCartDispatch({
type: 'ADD_ITEM',
payload: id,
});
}
function handleUpdateCartItemQuantity(productId, amount) {
shoppingCartDispatch({
type : "UPDATE_ITEM",
payload : {
productId,
amount,
}
})
}
const ctxValue = {
items : shoppingCartState.items,
addItemToCart : handleAddItemToCart,
updateItemQuantity : handleUpdateCartItemQuantity,
}
return <CartContext.Provider value = {ctxValue}>
{children}
</CartContext.Provider>
}
ctxValue가 없다면, 하위 컴포넌트들은 카트에 접근하려면 dispatch함수를 사용하거나 상태를 직접 받아야 한다.
하지만 ctxValue를 통해 전역적으로 접근할 수 있는 상태와 함수를 제공해준다. (value넘겨주기)
Scaling Up with Reducer and Context – React
The library for web and native user interfaces
react.dev
React-The-Complete-Guide/10-ContextAPI-useReducer at main · codrae/React-The-Complete-Guide
Contribute to codrae/React-The-Complete-Guide development by creating an account on GitHub.
github.com