240103 TIL (2)

thisisyjin·2024년 1월 3일
0

TIL 📝

목록 보기
112/113

Redux Advanced


비동기

부수 효과(Side-Effect)

  1. 컴포넌트 안에서 처리 - useEffect를 사용
  2. action creator 안에서 처리 - redux middleware

Practice Project

ui-store

store 생성

  1. 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;
  1. store/ui-slice.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  cartIsVisible: false,
};

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

export const uiActions = uiSlice.actions;
export default uiSlice;
  1. store/cart-slice.js

store 연결 (Provider)

  • index.js
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "./store";

import "./index.css";
import App from "./App";

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

useDispatch (UI)

  • 버튼 클릭 시 ui 토글 (dispatch)
// components/CartButton.jsx

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

const CartButton = (props) => {
  const dispatch = useDispatch();
  const toggleCartHandler = () => {
    dispatch(uiActions.toggle());
  };

  return (
    <button className={classes.button} onClick={toggleCartHandler}>
      <span>My Cart</span>
      <span className={classes.badge}>1</span>
    </button>
  );
};

export default CartButton;

useSelector (UI)

  • App.js 에서 useSelector로 상태 가져와서 조건에 따라 Cart 보여주기
// App.js
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useSelector } from "react-redux";

function App() {
  const isVisibleCart = useSelector((state) => state.ui.cartIsVisible);

  return (
    <Layout>
      {isVisibleCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;

cart-slice

store 생성

  • item 객체 구조
{
  id,
  price,
  totalPrice,
  name 
}
  1. addItemToCart
  2. removeItemFromCart
    // store/cart-slice.js
    import { createSlice } from "@reduxjs/toolkit";

const initialState = {
items: [],
totalQuantity: 0,
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addItemToCart(state, action) {
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.name,
    });
  } else {
    existingItem.quantity++;
    existingItem.totalPrice += newItem.price;
  }
},
removeItemFromCart(state, action) {
  const id = action.payload;
  const existingItem = state.item.find((item) => item.id === id);
  if (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;



### Action Dispatch

- 컴포넌트 수정 (Products.jsx)
``` jsx
import ProductItem from "./ProductItem";
import classes from "./Products.module.css";

const DUMMY_PRODUCTS = [
  { id: "p1", price: 6, title: "book1", description: "english book" },
  { id: "p2", price: 5, title: "book2", description: "korean book" },
];

const Products = (props) => {
  return (
    <section className={classes.products}>
      <h2>Buy your favorite products</h2>
      <ul>
        {DUMMY_PRODUCTS.map((product) => (
          <ProductItem
            key={product.id}
            title={product.title}
            price={product.price}
            description={product.description}
          />
        ))}
      </ul>
    </section>
  );
};

export default Products;
// ProductItem.jsx

import Card from "../UI/Card";
import classes from "./ProductItem.module.css";

import { useDispatch } from "react-redux";
import { cartActions } from "../../store/cart-slice";

const ProductItem = (props) => {
 const { title, price, description, id } = props;

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

 return (
   <li className={classes.item}>
     <Card>
       <header>
         <h3>{title}</h3>
         <div className={classes.price}>${price.toFixed(2)}</div>
       </header>
       <p>{description}</p>
       <div className={classes.actions}>
         <button onClick={addToCartHandler}>Add to Cart</button>
       </div>
     </Card>
   </li>
 );
};

export default ProductItem;

reducer 수정

// cart-slice.js

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

const initialState = {
  items: [],
  totalQuantity: 0,
};

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

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

useSelector

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

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

  const toggleCartHandler = () => {
    dispatch(uiActions.toggle());
  };

  return (
    <button className={classes.button} onClick={toggleCartHandler}>
      <span>My Cart</span>
      <span className={classes.badge}>{cartQuantity}</span>
    </button>
  );
};

export default CartButton;
// Cart.jsx

import Card from "../UI/Card";
import classes from "./Cart.module.css";
import CartItem from "./CartItem";

import { useSelector } from "react-redux";

const Cart = (props) => {
  const cartList = useSelector((state) => state.cart.items);
  return (
    <Card className={classes.cart}>
      <h2>Your Shopping Cart</h2>
      <ul>
        {cartList.map((cartItem) => (
          <CartItem
            key={cartItem.id}
            item={{
              title: cartItem.title,
              quantity: cartItem.quantity,
              total: cartItem.totalPrice,
              price: cartItem.price,
            }}
          />
        ))}
      </ul>
    </Card>
  );
};

export default Cart;

Action Dispatch (cart)

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

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

  const removeItemHandler = () => {
    dispatch(
      cartActions.removeItemFromCart({
        id,
        title,
        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>+</button>
        </div>
      </div>
    </li>
  );
};

export default CartItem;

비동기 코드

  • 리듀서 함수는 부수효과가 없는 순수 함수여야 함.
  • 즉, 부수효과를 생성하거나 비동기 코드는 리듀서 함수에 포함될 수 없음.
  • 즉, HTTP Request 또한 리듀서 내부에서 실행될 수 없음
  • 따라서 비동기 코드는 아래 두 방법으로 처리 가능.
  1. 컴포넌트 내부 - useEffect 사용
  • 컴포넌트 내부에서는 절대 redux state를 변경해서는 안됨.
  • (리듀서 함수 내부에서처럼 작성하면 X)
  1. 액션 생성자 내부

Logic의 위치

  1. 동기, 부수효과 없는 코드
  • Reducer 사용 O
  • 액션 생성자 or 컴포넌트 사용 X
  1. 비동기, 부수효과 있는 코드
  • 액션 생성자 or 컴포넌트 사용 O
  • Reducer 절대 사용 X ❗️❗️

Redux + useEffect

  • 컴포넌트 내에서 useEffect를 사용.
// App.js
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useSelector } from "react-redux";
import { useEffect } from "react";

function App() {
  const isVisibleCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);

  useEffect(() => {
    fetch("backendURl", {
      method: "PUT",
      body: JSON.stringify(cart);
    });
  }, [cart]);

  return (
    <Layout>
      {isVisibleCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;

위 코드대로라면, useEffect는 초기에 실행되기 때문에
cart가 빈 값으로 덮어쓰기 될 수 있음.

App.js 수정

import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useSelector } from "react-redux";
import { useEffect } from "react";

function App() {
  const isVisibleCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);

  useEffect(() => {
    const sendCartData = async () => {
      await response = fetch("backendURl", {
         method: "PUT",
         body: JSON.stringify(cart);
       });

        if (!response.ok) {
          throw new Error('Send Data Failed')
        }
       const responseData = await response.json();
     };
  }, [cart]);

  return (
    <Layout>
      {isVisibleCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;

ui-slice 수정

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

const initialState = {
  cartIsVisible: false,
  notification: null,
};

const uiSlice = createSlice({
  name: "ui",
  initialState,
  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;

App.js 수정 (dispatch)

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

function App() {
  const dispatch = useDispatch();
  const isVisibleCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);

  useEffect(() => {
    const sendCartData = async () => {
      dispatch(uiActions.showNotification({
        status: 'pending',
        title; 'Sending ...',
        message: 'Sending Cart Data'
      }));
      await response = fetch("backendURl", {
        method: "PUT",
        body: JSON.stringify(cart);
      });
      if (!response.ok) {
        dispatch(uiActions.showNotification({
          status: 'error',
          title; 'Error',
          message: 'Failed Sending Cart Data'
        }));
      }
      const responseData = await response.json();
      dispatch(uiActions.showNotification({
        status: 'Success',
        title; 'Success',
        message: 'Success Sending Cart Data'
      }));
    };

    sendCartData().catch(error => {
      dispatch(uiActions.showNotification({
        status: 'error',
        title; 'Error',
        message: 'Failed Sending Cart Data'
      }));
    })
  }, [cart, dispatch]);

  return (
    <Layout>
      {isVisibleCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;

useEffect가 처음에 실행되지 않도록 설정

App.js 수정

  • 컴포넌트 외부에 isInitial 함수 선언 (let)
  • useEffect 에서 함수 실행 전에 if-return 을 통해 첫 실행 시 fetch하지 않도록 분기
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useSelector, useDispatch } from "react-redux";
import { useEffect } from "react";
import { uiActions } from './store/ui-slice';

let isInitial = true;

function App() {
  const dispatch = useDispatch();
  const isVisibleCart = useSelector((state) => state.ui.cartIsVisible);
  const cart = useSelector((state) => state.cart);

  useEffect(() => {
    const sendCartData = async () => {
      dispatch(uiActions.showNotification({
        status: 'pending',
        title; 'Sending ...',
        message: 'Sending Cart Data'
      }));
      await response = fetch("backendURl", {
        method: "PUT",
        body: JSON.stringify(cart);
      });
      if (!response.ok) {
        dispatch(uiActions.showNotification({
          status: 'error',
          title; 'Error',
          message: 'Failed Sending Cart Data'
        }));
      }
      const responseData = await response.json();
      dispatch(uiActions.showNotification({
        status: 'Success',
        title; 'Success',
        message: 'Success Sending Cart Data'
      }));
    };

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

    sendCartData().catch(error => {
      dispatch(uiActions.showNotification({
        status: 'error',
        title; 'Error',
        message: 'Failed Sending Cart Data'
      }));
    })
  }, [cart]);

  return (
    <Layout>
      {isVisibleCart && <Cart />}
      <Products />
    </Layout>
  );
}

export default App;

액션 생성자 Thunk

  • action creator을 활용할 수 있다.
  • action creator = uiActions.showNotification 과 같은 메서드.
  • 디스패치할 액션을 생성하는 함수.

Thunk

  • 다른 작업이 완료될 때 까지 작업을 지연시키는 함수
// store/cart-slice.js

const sendCartData = (cartData) => {
  return (dispatch) => {
    dispatch(
      uiActions.showNotification({
        status: 'pending',
        title: 'Sending...',
        message: 'Sending cart data's
      })
    )
  }
  const sendCartData = async () => {
    dispatch(uiActions.showNotification({
      status: 'pending',
      title; 'Sending ...',
      message: 'Sending Cart Data'
    }));
    await response = fetch("backendURl", {
      method: "PUT",
      body: JSON.stringify(cart);
    });
    if (!response.ok) {
      dispatch(uiActions.showNotification({
        status: 'error',
        title; 'Error',
        message: 'Failed Sending Cart Data'
      }));
    }
    const responseData = await response.json();
    dispatch(uiActions.showNotification({
      status: 'Success',
      title; 'Success',
      message: 'Success Sending Cart Data'
    }));
  };

  await sendCartData();
};

Redux-Devtools

  • redux-toolkit을 사용하는 경우 바로 사용 가능.
  • redux를 사용하는 경우 바로 사용 가능
$ npm install @redux-devtools/extension
const store = createStore(
  reducer,
  composeWithDevTools(
    applyMiddleware(...middleware),
    // other store enhancers if any
  ),
);
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글