React

 

 

 

 

소스:

https://github.com/braverokmc79/macaronics-reduxcart20

 

 

 

 

https://macaronics-reduxcart20.netlify.app/

 

 

1.리덕스 툴킷 설치:

 리덕스 툴킷을 설치하려면 다음 명령어를 사용합니다.
 

# NPM을 사용하는 경우
npm install @reduxjs/toolkit react-redux
 
# Yarn을 사용하는 경우
yarn add @reduxjs/toolkit react-redux

 

 

2.리덕스 툴킷 적용

리덕스 툴킷을 적용하려면 configureStore를 사용하여 스토어를 생성합니다. 이때, configureStore의 인자로 리듀서를 포함하는 객체를 전달합니다.

 

store/index.js

import {configureStore } from '@reduxjs/toolkit' ;
import uiSlice from './ui-slice';
import cartSlice from './cart-slice';


const store=configureStore({
    reducer:{
        ui:uiSlice.reducer,
        cart:cartSlice.reducer
    }
});


export default store;


 

 

3. Slice 생성: 
createSlice를 사용하여 Slice를 생성합니다. 
createSlice는 객체를 인자로 받으며, 이 객체에는 name, initialState, reducers가 필요합니다.

 

1) store/cart-slice.js

import { createSlice } from "@reduxjs/toolkit";

const cartSlice = createSlice({
  name: "cart",
  initialState: { 
    items: [], 
    totalQuantity: 0, 
 },
  reducers: {
    addItemToCart(state,action) {
        const newItem=action.payload;
        const  existingItem=state.items.find(item=>item.id===newItem.id);
       // console.log("existingItem ", existingItem);
        state.totalQuantity++;


        if(!existingItem) {
            state.items.push({
                id:newItem.id,
                price:newItem.price,
                quantity:1,
                totalPrice:newItem.price,
                name:newItem.title
            });

        }else{
         //   console.log("기존 제품 수량 업데이트");
            existingItem.quantity++;
            existingItem.totalPrice += newItem.price;
        }

    },
    removeItemFromCart(state, action) {
      const id=action.payload;
      const existingItem=state.items.find(item=>item.id===id);
      state.totalQuantity--;

      if(existingItem && existingItem.quantity === 1) {
        state.items=state.items.filter(item=>item.id!==id);
      }else{
        existingItem.quantity--;
        existingItem.totalPrice -= existingItem.price;
      }
    },
    
  },
});

export const cartActions = cartSlice.actions;
export default cartSlice;

 

 

 

2) store/ui-slice.js

import {createSlice} from '@reduxjs/toolkit';

const uiSlice=createSlice({
    name:"ui",
    initialState:{ cartIsVisible:false},
    reducers:{       
        toggle(state){
            console.log("장바구니 클릭");
            state.cartIsVisible =!state.cartIsVisible;
        },
    }
});

export const uiActions=uiSlice.actions;
export default uiSlice;

 

//dispatch  로 데이터를 넘겨주기위해

export const uiActions=uiSlice.actions;


 

//index 에서 ui:uiSlice.store 로 사용

//state 값을 가져올때 state.ui  로 접근

export default uiSlice;

 

 

 

 

 

4.컴포넌트에서 사용:

컴포넌트에서는 useSelector와 useDispatch를 사용하여 리덕스 스토어의 상태를 조회하거나 액션을 디스패치할 수 있습니다

 

CartItem.js   ( dispatch 사용 예)

import { useDispatch } from 'react-redux';
import classes from './CartItem.module.css';
import { cartActions } from '../../store/cart-slice';

const CartItem = (props) => {
  const dispatch=useDispatch();
  const {id, title, quantity, total, price } = props.item;

   const removeItemHandler=()=>{
    dispatch(cartActions.removeItemFromCart(id));
   }

   const addItemHandler=()=>{
    dispatch(cartActions.addItemToCart({id,title,quantity,price}));
   }




  return (
    <li className={classes.item}>
      <header>
        <h3>{title}</h3>
        <div className={classes.price}>
          ${total.toFixed(2)}{' '}
          <span className={classes.itemprice}>(${price.toFixed(2)}/item)</span>
        </div>
      </header>
      <div className={classes.details}>
        <div className={classes.quantity}>
          x <span>{quantity}</span>
        </div>
        <div className={classes.actions}>
          <button onClick={removeItemHandler}>-</button>
          <button onClick={addItemHandler}>+</button>
        </div>
      </div>
    </li>
  );
};

export default CartItem;

 

 

CartButton.js  ( useSelector 사용예 )

import { useDispatch, useSelector } from "react-redux";
import { uiActions } from "../../store/ui-slice";
import classes from "./CartButton.module.css";

const CartButton = (props) => {
  const dispatch = useDispatch();
  const cartQuantity= useSelector(state=>state.cart.totalQuantity)


  const toggleCartHandler = (event) => {
    event.preventDefault();
    dispatch(uiActions.toggle());
    console.log("장바구니");
  };

  return (
    <button className={classes.button} onClick={toggleCartHandler}>
      <span>나의 장바구니</span>
      <span className={classes.badge}>{cartQuantity}</span>
    </button>
  );
};

export default CartButton;

 

 

5. root index.js  에 다음과 같이 설정을 해줘야 한다.

import ReactDOM from 'react-dom/client';
import {Provider} from 'react-redux';
import './index.css';
import App from './App';
import store from './store';


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Provider  store={store} ><App /></Provider>);

 

 

 

 

 

※ 리덕스 툴킷과+ useEffect +  fetch 를 이용한 데이터 가져오기

 

App.js

import { Fragment, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Cart from './components/Cart/Cart'; 
import Layout from './components/Layout/Layout'; 
import Products from './components/Shop/Products'; 
import { uiActions } from './store/ui-slice';
import Notification from './components/UI/Notification';


let isInitial =true;


function App() {  
  const dispatch=useDispatch();
  const showCart = useSelector(state => state.ui.cartIsVisible); // 장바구니 표시 여부
  const cart = useSelector(state => state.cart); // 장바구니 데이터
  const notification=useSelector(state => state.ui.notification);

  // useEffect 훅을 사용하여 cart 데이터가 변경될 때마다 실행
  useEffect(() => {
    const sendCardData = async () => {
          dispatch(uiActions.showNotification({
            status:'pending',
            title:'Sending...',
            message:'장바구니 데이터를 전송 중입니다!'
          }));

          const response = await fetch('https://react-http-6b4a6.firebaseio.com/cart.json', {
            method: 'PUT',
            body: JSON.stringify(cart),
          })

          if (!response || !response.ok) {;
            throw new Error('장바구니 데이터를 전송하지 못했습니다.');
          }

          dispatch(uiActions.showNotification({
            status:'success',
            title:'Success!',
            message:'장바구니 데이터가 성공적으로 전송되었습니다!'
          }));

    };


    if(isInitial){
      isInitial=false;
      return;
    }

    sendCardData().catch((error) => {
      //console.error(" 에러 :", error);
       dispatch(uiActions.showNotification({
          status:'error',
          title:'Error!',
          message:'장바구니 데이터 전송에 실패했습니다!'
        }));
     });

  }, [cart, dispatch]); // useEffect 의존성 배열에 cart와 dispatch 추가

  return (
    <Fragment>
       {notification &&  <Notification   status={notification.status}  title={notification.title} message={notification.message}   />  }

       
        <Layout> {/* 레이아웃 컴포넌트 */}
          {/* 장바구니 표시 여부에 따라 Cart 컴포넌트 조건부 렌더링 */}
          {showCart && <Cart />} 
          
          {/* 제품 목록 컴포넌트 */}
          <Products />
        </Layout>
    </Fragment>
  );
}

export default App;

 

 

 

 

 

 

※ 리액트에서 Thunk 란 무엇인가?

 

리액트에서 Thunk는 주로 비동기 작업을 처리하는 데 사용되는 미들웨어입니다. 
Redux Thunk를 사용하면 액션 객체가 아닌 함수를 디스패치할 수 있게 해줍니다. 
즉, 디스패치(dispatch)할 때 객체가 아닌 함수를 디스패치할 수 있게 되는 것입니다.

이렇게 함수를 디스패치하면, 해당 함수에서 디스패치와 getState를 파라미터로 받아와서 원하는 작업을 수행할 수 있습니다. 
이렇게 중간에 원하는 작업을 함수를 통해 넣을 수 있고, 그것이 중간에 실행되는 것입니다. 
이 함수를 Thunk 함수라고 부릅니다.

Redux Thunk는 리덕스에서 비동기 작업을 처리할 때 가장 많이 사용하는 미들웨어로, 이 미들웨어를 사용하면 액션 객체가 아닌 함수를 디스패치할 수 있습니다. 함수를 디스패치할 때는, 해당 함수에서 디스패치와 getState를 파라미터로 받아와야 합니다. 
이 함수를 만들어주는 함수를 Thunk라고 부릅니다.

즉, Thunk는 리덕스에서 비동기 작업을 처리하기 위해 사용되는 미들웨어로, 액션 생성자가 액션 객체가 아닌 함수를 반환하게 해주는 역할을 합니다.
이 함수는 원하는 작업을 수행한 후에 액션 객체를 디스패치하게 됩니다.

 

 

https://github.dev/braverokmc79/macaronics-reduxcart20

 

 

1. 다음은 리덕스 툴킷이다.

리덕스 툴킷 만으로는 비동기식 데이터를 처리할수 없기 때문에  Thunk  사용한다고 생각하면 된다.

cart-slice.js

import { createSlice } from "@reduxjs/toolkit";

const cartSlice = createSlice({
  name: "cart",
  initialState: { 
    items: [], 
    totalQuantity: 0, 
 },
  reducers: {
    replaceCart(state, action) {
      state.totalQuantity = action.payload.totalQuantity;
      state.items = action.payload.items;
    },
    
    addItemToCart(state,action) {
        const newItem=action.payload;
        const  existingItem=state.items.find(item=>item.id===newItem.id);
        state.totalQuantity++;

        if(!existingItem) {
            state.items.push({
                id:newItem.id,
                price:newItem.price,
                quantity:1,
                totalPrice:newItem.price,
                name:newItem.title
            });

        }else{
            existingItem.quantity++;
            existingItem.totalPrice += newItem.price;
        }

    },
    removeItemFromCart(state, action) {
      const id=action.payload;
      const existingItem=state.items.find(item=>item.id===id);
      state.totalQuantity--;

      if(existingItem && existingItem.quantity === 1) {
        state.items=state.items.filter(item=>item.id!==id);
      }else{
        existingItem.quantity--;
        existingItem.totalPrice -= existingItem.price;
      }
    },
  },
});





export const cartActions = cartSlice.actions;
export default cartSlice;


 

 

 

 

2.다음과 같이 비동기식 createAsyncThunk  만든다.

cart-action.js

import { createAsyncThunk } from "@reduxjs/toolkit";
import { uiActions } from "./ui-slice";
import { cartActions } from "./cart-slice";


export const sendCartData = createAsyncThunk(
  "cart/sendCartData",

  async (cartData, { dispatch }) => {
    dispatch(
      uiActions.showNotification({
        status: "pending",
        title: "Sending...",
        message: "장바구니 데이터를 전송 중입니다!",
      })
    );

    try {
      const response = await fetch(
        "https://react-http-6b4a6.firebaseio.com/cart.json",
        {
          method: "PUT",
          body: JSON.stringify(cartData),
        }
      );

      if (!response || !response.ok) {
        throw new Error("장바구니 데이터를 전송하지 못했습니다.");
      }

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success!",
          message: "장바구니 데이터가 성공적으로 전송되었습니다!",
        })
      );

    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "장바구니 데이터 전송에 실패했습니다!",
        })
      );
    }
  }
);

//장바구니 가져오기
export const fetchCartData = createAsyncThunk(
  "cart/fetchCartData",

  async (cartData, { dispatch }) => {
    try {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/todos",
        {
          method: "GET",
        }
      );

      if (!response || !response.ok) {
        throw new Error("장바구니 가져오기 실패.");
      }

      const data = await response.json();
      

      const cartDataItem=  data.map((item, index)=>{
          const price=getRandomPrice(300, 500)*100;
          return (     
          {
            id:item.id,
            price:price,
            quantity:1,
            totalPrice:price,
            name:item.title,
            img:`https://source.unsplash.com/random/320*240?random=${index}`          }
        )
    });

      
      const cart={
        items: cartDataItem, 
        totalQuantity:cartDataItem.length, 
      }
      
      dispatch(cartActions.replaceCart(cart));
      console.log("장바구니 가져오기 성공");
    } catch (error) {
        console.error("에러(장바구니 가져오기 실패) : " , error );
    }
  }
);

function getRandomPrice(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}



export const getCart = createAsyncThunk(
  "cart/getCart",
  async (cartData, { dispatch }) => {
    // 여기에 비동기 작업을 수행하세요.
  }
);

 

 

3. 사용 예
 

cart-action.js 에서

액션 생성자가 액션 객체가 아닌 함수를 반환하여 함수 자체를 사용하고 있다.

dispatch(sendCartData(cart));

App.js

import { Fragment, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Cart from './components/Cart/Cart'; 
import Layout from './components/Layout/Layout'; 
import Products from './components/Shop/Products'; 
import Notification from './components/UI/Notification';
import { fetchCartData, sendCartData } from './store/cart-action';


let isInitial =true;


function App() {  
  const dispatch=useDispatch();
  const showCart = useSelector(state => state.ui.cartIsVisible); // 장바구니 표시 여부
  const cart = useSelector(state => state.cart); // 장바구니 데이터
  const notification=useSelector(state => state.ui.notification);

  useEffect(()=>{
    //1.장바구니 목록 가져오기
    dispatch(fetchCartData());
  }, [dispatch]);

  //2.장바구니 넣기
  useEffect(() => {   
    if(isInitial){
      isInitial=false;
      return;
    }
    dispatch(sendCartData(cart));
    
  }, [cart, dispatch]); // useEffect 의존성 배열에 cart와 dispatch 추가




  return (
    <Fragment>
       {notification &&  <Notification   status={notification.status}  title={notification.title} message={notification.message}   />  }       
        <Layout> {/* 레이아웃 컴포넌트 */}
          {/* 장바구니 표시 여부에 따라 Cart 컴포넌트 조건부 렌더링 */}
          {showCart && <Cart />} 
          
          {/* 제품 목록 컴포넌트 */}
          <Products />
        </Layout>
    </Fragment>
  );
}

export default App;


 

 

 

 

 

about author

PHRASE

Level 60  라이트

북두 칠성이 앵돌아졌다 , 일이 낭패가 되었다는 말.

댓글 ( 0)

댓글 남기기

작성