본문 바로가기

카테고리 없음

Context API & useReducer [React- The Complete Guide 2024]

더 복잡한 상태관리를 해보자...!

 

리액트 개발자는 주로 다수의 컴포넌트로 이루어진 앱을 만들어야 하는데, 

앱이 복잡할수록, 더 많은 컴포넌트를 사용하게 된다. 

보통 컴포넌트 트리로 구성이 되어있는데, 이 과정에서 상태관리가 필요하다..!!

 

Prop Drilling을 통해 다수의 컴포넌트를 거쳐 속성을 전달할 수 있다.

하지만 대부분의 컴포넌트가 그 데이터를 직접적으로 필요로 하지 않고, 하위로(목표까지) 전달해주는 역할만 수행한다.

=> 원하는 공유데이터를 얻기 위해 여러 컴포넌트를 거치게 되고,
    이러한 방식은 컴포넌트들의 재사용성을 어렵게 한다! (+코드도 더 많이 작성해야 함..)

 

해결방식

  1. Component Composition
  2. Context API
  3. 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