JiSoo's Devlog

[Udemy React 완벽 가이드] 컨텍스트 API & useReducer 본문

Frontend/React

[Udemy React 완벽 가이드] 컨텍스트 API & useReducer

지숭숭숭 2024. 6. 10. 15:57

Prop driling

여러 층의 컴포넌트를 거쳐 공유하고자 하는 데이터를 넘겨주는 것

다수의 컴포넌트를 거쳐 속성을 전달하는데 대부분의 컴포넌트가 그 데이터를 직접적으로 필요로 하지는 않는다

 

prop driling의 해결책 ↓

1. Component composition 컴포넌트 합성

import { DUMMY_PRODUCTS } from '../dummy-products.js';
import Product from './Product.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.js

<Shop onAddItemToCart={handleAddItemToCart}>
  {DUMMY_PRODUCTS.map((product) => (
    <li key={product.id}>
      <Product {...product} onAddToCart={handleAddItemToCart} />
    </li>
  ))}
</Shop>

 

Shop.js

import { DUMMY_PRODUCTS } from "../dummy-products.js";
import Product from "./Product.jsx";

export default function Shop({ children }) {
  return (
    <section id="shop">
      <h2>Elegant Clothing For Everyone</h2>

      <ul id="products">{children}</ul>
    </section>
  );
}

 

이렇게 해주면 컴포넌트 중첩을 한 층 삭제해 Shop 컴포넌트에 속성을 전달해주지 않아도 된다

하지만 이 방법을 사용하면 모든 컴포넌트가 앱 컴포넌트에 들어가야 하니까 앱 컴포넌트가 비대해진다

 

2. Context API

컴포넌트나 컴포넌트 레이어 간의 데이터 공유를 용이하게 해 준다

여러 컴포넌트에 제공되는 장점은 state와의 연결이 쉽다는 것

리액트 상태를 컨텍스트 값에 연결하면 앱 전체에 제공되는 방식으로 사용된다

상태에 접근이 필요하거나 변경해야 하는 컴포넌트의 경우 직접적으로 해당 컨텍스트 혹은 상태에 접근이 가능하다

 

import { createContext } from "react";

createContext();

createContext 함수로 컨텍스트 값을 생성하고 변수나 상수에 저장하면 된다

createContext로 만들어진 값은 리액트 컴포넌트가 들어있는 객체

초깃값으로 사용할 특정값을 createContext에 전달해 컨텍스트로 감쌀 모든 컴포넌트에 그 값이 전달되도록 할 수 있다

컨텍스트로 컴포넌트에 제공할 값은 숫자, 문자열, 객체, 배열 뭐든 괜찮다

export const CartContext = createContext({
  items: [],
});

 

컨텍스트를 컴포넌트로 사용하려면 Provider라는 컨텍스트 객체의 속성을 사용한다

컴포넌트 이름에 온점이 들어간 경우는 특정 오브젝트 안에 중첩된 속성이 실질적인 컴포넌트가 되는 경우 JSX 파일로써 유효해진다

Provider 속성은 유효한 리액트 컴포넌트를 가지고 있다

 

useContext 훅은 어떤 컴포넌트 함수일지라도 컨텍스트 값에 접근해 이를 사용할 수 있게 해 준다

const cartCtx = useContext(CartContext);

이런 식으로 해서 useContext로부터 값을 받을 건데 이 값은 items 속성을 가진 상태이다

<CartContext.Provider value={{ items: [] }}>

값 속성을 설정해 주고 컨텍스트 값 또한 제공해야 한다

 

export const CartContext = createContext({
  items: [],
});

이렇게 기본 컨텍스트 값을 설정하는 게 유용한 이유는 자동완성 기능을 얻을 수 있기 때문이다

 

컨텍스트 구조 분해

 const { items } = useContext(CartContext);

  const totalPrice = items.reduce(
    (acc, item) => acc + item.price * item.quantity,
    0
  );
  const formattedTotalPrice = `$${totalPrice.toFixed(2)}`;

  return (
    <div id="cart">
      {items.length === 0 && <p>No items in cart!</p>}
      {items.length > 0 && (
        <ul id="cart-items">
          {items.map((item) => {
            const formattedPrice = `$${item.price.toFixed(2)}`;

items를 저렇게 빼내서 컨텍스트 객체를 구조 분해 할 수도 있다

 

 

컨텍스트와 state 연결하기

const [shoppingCart, setShoppingCart] = useState([{
    items: [],
  }]);

.
.
.
return (
    <CartContext.Provider value={shoppingCart}>

 

컨텍스트를 통해 함수 자체도 노출시켜서 Provider 컴포넌트로 묶인 컴포넌트의 자식 컴포넌트일 경우 additemToCart 속성을 통해 handleAddItemToCart 함수의 기능을 불러올 수 있다

 const ctxValue = {
    items:shoppingCart.items,
    addItemToCart:handleAddItemToCart
  };

  return (
    <CartContext.Provider value={ctxValue}>

이렇게 컨텍스트의 사용은 읽히는 값을 제공하는 것뿐 아니라 값이나 함수를 불러와서 shoppingCart 같은 상태를 변경해 줄 수 있다

 

useContext 훅은 컴포넌트의 기능적 함수를 컨텍스트와 연결하고 함수 내에서 해당 컨텍스트 값을 사용 가능하게 만든다

이 훅을 사용하는 게 컴포넌트 안의 컨텍스트에 접근할 때 일반적으로 사용하는 방법

 

Consumer 컴포넌트는 컨텍스트 값에 대한 액세스를 가진 JSX 코드를 묶는데에 사용할 수 있다

Consumer 컴포넌트가 필요로 하는 자식 속성이 있는데 시작 및 종료 태그 사이로 전달되는 특별한 내용을 담는 함수이다

<CartContext.Consumer>
      {(cartCtx) => {
        const totalPrice = cartCtx.items.reduce(
          (acc, item) => acc + item.price * item.quantity,
          0
        );
        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>

이 함수는 컨텍스트 값을 매개변수로 받게 되고 해당 컴포넌트가 출력해야 할 실제 JSX 코드를 반환한다

 

이 접근법은 다소 길고 복잡한 데다 읽기도 힘들어서 기본 접근법으로는 적절치 않다

useContext 훅을 사용하는 게 가장 현대적이고 최선의 방법!!

 

컴포넌트의 컨텍스트 값에 접근할 때 해당 값은 그 값에 접근하는 컴포넌트 함수를 바꾸고 업데이트된 내부 상태가 사용되었거나 부모 컴포넌트가 다시 실행되는 것과 같이 리액트에 의한 재실행이 이루어진다

컴포넌트 함수가 useContext 훅을 사용해 관련 컨텍스트 값에 연결되었을 때도 재실행이 진행된다

이런 상황에서 UI 업데이트 진행을 위해 컴포넌트는 필히 재실행되어야 하고 이에 따라 해당 값이 변경된다

 

연결된 컨텍스트 값이 변경되었을 때 리액트가 컴포넌트 함수를 재실행하는 이유는 해당 컴포넌트 함수를 새로운 UI를 만들어낼 수 있게 하기 위해서이다

 

 

컨텍스트 아웃소싱

컨텍스트 관련 데이터 관리를 앱 컴포넌트가 아닌 별개의 컨텍스트 컴포넌트에 넣을 수 있게 해주는 것

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,
    updateItmeQuantity: handleUpdateCartItemQuantity,
  };

  return (
    <CartContext.Provider value={ctxValue}>{children}</CartContext.Provider>
  );
}

 

 

상태관리용 훅 useReducer

reducer는 하나 또는 그 이상의 복잡한 값을 더 단순한 형태로 만드는 함수

useReducer 훅은 상태 관리의 목적을 가지고 하나 또는 그 이상의 값을 보다 단순하게 하나의 값으로 줄이는 것

const [shoppingCartState, shoppingCartDispatch] = useReducer();

두 번째 요소는 dispatch 함수인데 이걸로 action을 보낼 수가 있고 보내진 액션은 추후 리듀서 기능에 의해 사용된다

function shoppingCartReducer(state, action){

}

리듀서 함수는 상태와 액션 두 매개변수를 받는데 액션이 디스패치를 통해 보내진 후 리액트가 리듀서 함수를 호출한다

 

728x90