고급 리덕스

맛없는콩두유·2022년 9월 7일
0
post-thumbnail

복습

 npm install @reduxjs/toolkit 
 
 npm install redux react-redux

설치가 끝난 후 store 폴더를 만들고 폴더 안에 index.js와
장바구니 관리용 슬라이스 장바구니 토글링 슬라이스 같은 사용자 인터페이스 로직용 슬라이스 두개로 나누겠습니다!


toggle버튼을 만들어 Shpping Cart 창을 toggle 해보겠습니다.

  • ui-slice
import { createSlice } from "@reduxjs/toolkit";

const uiSlice = createSlice({
  name: "ui",
  initialState: { cartIsVisible: false },
  reducers: {
    toggle(state) {
      state.cartIsVisible = !state.cartIsVisible;
    },
  },
});

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

다음 저장소인 index.js를 만들어보겠습니다.

  • store/index.js
import { configureStore } from "@reduxjs/toolkit";

import uiSlice from "./ui-slice";

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

export default store;

전체 애플리케이션에 리덕스 스토어를 제공하는 방법은
루트 컴포넌트인 index.js 에서 설정할 수 있습니다.

빨강색 네모 박스 부분을 설정해줘야합니다.

자 이제 cart버튼이 있는 cartButton.js 컴포넌트로 가보면

  • CartButton.js

onClick 메서드를 만들어주고 이것을 실행하기위해 dispatch를 해줘야합니다. 그리고 ui-slice에 있는 toggle메서드를 불러와서 실행해줍니다.

이것을 실행하기 위해서는 App.js 컴포넌트에서 데이터를 추출해야 합니다.

  • App.js

데이터 추출을 위해 useSelector를 이요하고 state.ui.cartIsVisible은
index.js에 있는 ui이름과 ui-slice.js에 있는 관심있는 프로퍼티 이름인 cartIsVisible을 사용해야한다.


toggle 버튼이 잘 되는 것을 볼 수 있다.

이제 장바구니의 안의 내용과 Add to Cart 버튼 그리고 MyCart버튼의 개수가 달라지는 작업을 수행해야합니다.

cart-slice.js를 살펴보겠습니다.

  • cart-slice.js
import { createSlice } from "@reduxjs/toolkit";

createSlice({
  name: "cart",
  initialState: {
    items: [],
    totaslQuantity: 0, //수량의 합
  },
  reducers: {
    addItemToCart(state, action) {
      // dispatch될 떄 추가정보를 dispatch해야하기 때문에 actions이 필요하다
      const newItem = action.payload;
      const existingItem = state.items.find((item) => item.id === newItem.id); // 이미 항목이 존재하는지 파악
      if (!existingItem) {
        state.items.push({
          itemId: newItem.id,
          price: newItem.price,
          quantity: 1,
          totalPrice: newItem.price,
          name: newItem.title,
        }); //리더스 툴킷이 자동으로 내부적으로 조작하지 않게 해서 push 사용 가능
      } else {
        //항목이 존재하는 경우
        existingItem.quantity++;
        existingItem.totalPrice = totalPrice + newItem.price;
      }
    },
    removeItemFromCart() {},
  },
});

이상으로 addItemToCart를 알아보았습니다.

이제 장바구니의 항목을 제거하려면 removeItemFormCart 상태와 작업이 필요합니다.

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

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    items: [],
    totaslQuantity: 0, //수량의 합
  },
  reducers: {
    addItemToCart(state, action) {
      // dispatch될 떄 추가정보를 dispatch해야하기 때문에 actions이 필요하다
      const newItem = action.payload;
      const existingItem = state.items.find((item) => item.id === newItem.id); // 이미 항목이 존재하는지 파악
      if (!existingItem) {
        state.items.push({
          itemId: newItem.id,
          price: newItem.price,
          quantity: 1,
          totalPrice: newItem.price,
          name: newItem.title,
        }); //리더스 툴킷이 자동으로 내부적으로 조작하지 않게 해서 push 사용 가능
      } else {
        //항목이 존재하는 경우
        existingItem.quantity++;
        existingItem.totalPrice = totalPrice + newItem.price;
      }
    },
    removeItemFromCart(state, action) {
      const id = action.payload;
      const existingItem = state.items.find((item) => item.id === id);
      if (existingItem.quantity === 1) {
        //1과 같으면 배열에서 항목을 완전히 제거한다.
        state.items = state.itmes.filter((item) => item.id !== id);
      } else {
        //1보다 크면 단지 항목의 숫자를 하나 줄이면 된다.
        existingItem.quantity--;
        existingItem.totalPrice = existingItem.totalPrice - existingItem.price;
      }
    },
  },
});

export const cartActions = cartSlice.actions;

export default cartSlice;

그리고 store/index.js에서 새로운 슬라이스를 전체 리덕스 스토어에 병합해 보겠습니다.

  • store/index.js
import cartSlice from "./cart-slice";


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

cart를 추가해줍니다

자 그리고 이제 Add Cart를 눌렀을 떄 바뀌려면 실제 제품이 필요하기 떄문에 Product.js로 가서 DUMMY_DATA를 만들겠습니다.

  • Product.js
const DUMMY_PRODUCTS = [
  {
    id: "p1",
    price: 6,
    title: "My First Book",
    description: "The first Book I ever wrote",
  },
  {
    id: "p2",
    price: 5,
    title: "My Second Book",
    description: "The secpmd Book I ever wrote",
  },
];

더미 데이터를 만들고

      <ul>
        {DUMMY_PRODUCTS.map((product) => (
          <ProductItem
            key={product.id}
            title={product.title}
            price={product.price}
            description={product.description}
          />
        ))}
      </ul>

동적으로 보여지게 합니다.

그리고 나서 ProductItem.js에 가서 Add to Cart 버튼을 만들어보겠습니다. 이것은 cart-slice.js의 addItemToCart함수에 연결하고 싶습니다.

  • ProductItem.js

이제 데이터를 업데트할 CartButton.js를 보겠습니다.

  • CartButton.js

장바구니의 숫자가 바뀌는 것을 알 수 있습니다.

이제 장바구니를 올바르게 렌더링 해보기 위해 Cart.js로 가보겠습니다.

  • cart.js

기존에 있던 항목 추가는 숫자로 잘 나타나는 것을 확인할 수 있습니다.

이제 CartItem의 더하기와 뺴기 버튼 기능을 할 차례입니다!
이를 위해 CartItem.js에서 + - 버튼을 디스패치 해야합니다!

  • CartItem.js

작동이 잘 되는 것을 알 수 있습니다.

이제 장바구니를 보낼 수 있는 서버인
백엔드를 추가해볼까 합니다.

Firebase를 이용할 것인데, 하지만 리덕스는 동기식이기 때문에 비동기식인 fetch()를 사용할 수 없습니다.

이것을 사용하기 위해서는 두 가지 옵션이 있는데 살펴보겠습니다.
컴포넌트 내부 효과인 비동기 코드를 실행하는 것부터 시작하겠습니다.

  • App.js

  • Network

Add to Cart를 누를 떄 마다 네트워크에 json형태로 생기는 것을 볼 수 있다.

  • Firebase

이제 성공과 여부를 알려주는 알림 컴포넌트를 UI 폴더에 Notification.js를 만들겠습니다.

  • 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(() => {
    const sendCartData = async () => {
      dispatch(
        uiActions.showNotification({
          status: "pending",
          title: "Sending...",
          message: "Sending cart data!",
        })
      );
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/cart.json",
        {
          method: "PUT",
          body: JSON.stringify(cart),
        }
      );

      if (!response.ok) {
        throw new Error("Sending cart data failed.");
      }

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success!",
          message: "Sent cart data successfully!",
        })
      );
    };

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

    sendCartData().catch((error) => {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "Sending cart data failed!",
        })
      );
    });
  }, [cart, dispatch]);

  return (
    <Fragment>
      {notification && (
        <Notification
          status={notification.status}
          title={notification.title}
          message={notification.message}
        />
      )}
      <Layout>
        {showCart && <Cart />}
        <Products />
      </Layout>
    </Fragment>
  );
}

export default App;
  • uislice.js
import { createSlice } from "@reduxjs/toolkit";

const uiSlice = createSlice({
  name: "ui",
  initialState: { cartIsVisible: false, notification: null },
  reducers: {
    toggle(state) {
      state.cartIsVisible = !state.cartIsVisible;
    },
    showNotification(state, action) {
      state.notification = {
        status: action.payload.status,
        title: action.payload.title,
        message: action.payload.message,
      };
    },
  },
});

export const uiActions = uiSlice.actions;

export default uiSlice;

Add to cart를 누르면 상단에 sucess할 떄 메시지가 잘 나타납니다.


잘못된 fetch 주소를 보내면 error 문구가 뜨는 것을 볼 수 있습니다.

다음으로 데이터를 fetch 하기 전에 모든 부작용 논리를 컴포넌트에 넣는 대안을 살펴보겠습니다.

  • cart-slice.js
import { createSlice } from "@reduxjs/toolkit";

import { uiActions } from "./ui-slice";

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 = existingItem.totalPrice + newItem.price;
      }
    },
    removeItemFromCart(state, action) {
      const id = action.payload;
      const existingItem = state.items.find((item) => item.id === id);
      state.totalQuantity--;
      if (existingItem.quantity === 1) {
        state.items = state.items.filter((item) => item.id !== id);
      } else {
        existingItem.quantity--;
      }
    },
  },
});

export const sendCartData = (cart) => {
  return async (dispatch) => {
    dispatch(
      uiActions.showNotification({
        status: "pending",
        title: "Sending...",
        message: "Sending cart data!",
      })
    );

    const sendRequest = async () => {
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/cart.json",
        {
          method: "PUT",
          body: JSON.stringify(cart),
        }
      );

      if (!response.ok) {
        throw new Error("Sending cart data failed.");
      }
    };

    try {
      await sendRequest();

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success!",
          message: "Sent cart data successfully!",
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "Sending cart data failed!",
        })
      );
    }
  };
};

export const cartActions = cartSlice.actions;

export default cartSlice;
  • 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 { sendCartData } from "./store/cart-slice";

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(() => {
    if (isInitial) {
      isInitial = false;
      return;
    }

    dispatch(sendCartData(cart));
  }, [cart, dispatch]);

  return (
    <Fragment>
      {notification && (
        <Notification
          status={notification.status}
          title={notification.title}
          message={notification.message}
        />
      )}
      <Layout>
        {showCart && <Cart />}
        <Products />
      </Layout>
    </Fragment>
  );
}

export default App;

이렇게 해도 위와 같은 결과를 가지고올 수 있습니다.

profile
하루하루 기록하기!

0개의 댓글