Redux + Redux persist 를 사용하여 Data Cart 구현

yojuyoon·2020년 8월 17일
1

프로젝트

목록 보기
9/10
post-custom-banner

Redux는 자바스크립트 앱의 상태(state)를 관리해주는 하나의 도구, 라이브러리이다. React 뿐만 아니라 어디에서도 사용할 수 있지만 이번 프로젝트에서 React와 함께 Redux로 state를 관리해보면서 Data Cart 페이지를 구현하였다.

Redux

프로젝트 규모가 커지고 컴포넌트가 여러개 생성되면서 각 컴포넌트에서 필요한 데이터가 늘어나게 된다. 이를테면 Main화면에서 보여지는 데이터, 상세페이지에서 보여지는 데이터, 장바구니에서 보여지는 데이터 등... 이 때 컴포넌트마다 가지고 있는 데이터를 공유하기 위해서 또 각 컴포넌트가 가지고 있는 데이터를 컴포넌트끼리 공유하기 위해서 props와 state를 사용하여 복잡한 구조로 데이터를 내려주고, 내려받게 된다.

이런 문제점을 해결하기 위해서 Redux라이브러리를 사용할 수 있다.

리덕스를 활용하면 상태관리 로직을 컴포넌트 밖에서 처리할 수 있다. 리덕스의 store라는 객체 내부에 상태를 담고, 스토어에서 모든 상태관리가 일어난다. 상태에 변화를 일으켜야 할 때는 action을 스토어에 전달하고 action은 변화를 일으킨다. 액션을 store에 전달하는 과정을 dispatch라고 부른다.

store가 action을 받으면 reducer가 전달받은 action을 기반으로 상태를 어떻게 변화시킬지 정하고, 새 상태를 다시 store에 저장한다. 이렇게 상태가 바뀌면 store를 구독하고있는 컴포넌트에 바로 전달이 되면서 리렌더링 된다.

참고 블로그 : 리액트 리덕스 개념_서영서

프로젝트에서 구현한 Data Cart에 빗대어 살펴보면
Main 페이지에 Data Card들은 state로 관리된다.
Card를 클릭하면 Card state에 저장이 되는 Reducer를 생성한다. 이를 action을 통해 Card에 추가한다. 저장된 Card들은 개별 삭제, 전체 삭제가 가능하다. 이 또한 Reducer에 추가한다. 각 action을 dispatch하면 해당 action의 reducer가 실행되고 기능 구현이 가능하다.

이 후 새로고침 시에 Cart페이지에서 data card 내역이 초기화 되는 문제가 발생하였다. 이 때 persist가 등장한다.

react-persist library install

$ npm i --save redux react-redux redux-persist

설치 후 persist를 어떻게 사용하는지 그리고 data cart 기능 구현과정을 코드로 살펴보자.

//cartReducer.js
**
import {
  ADD_TO_CART,
  REMOVE_FROM_CART,
  REMOVE_ALL,
} from "../actions/cartActions";

//state초기화. 추가된 card들은 배열에 담기므로 cart의 state를 빈배열로 선언
const INITIAL_STATE = []; 

//switch문을 활용
const cartReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
 //각 action을 import 후에 action이 실행되면 행해지는 case를 입력
    case ADD_TO_CART:
      //중복 card 추가 방지를 위해 기본 값 false로 생성
      let duplicateItem = false; 
      state.forEach((el) => {
        if (el.id === action.payload.id) {
          duplicateItem = true;
        }//includes사용 가능
      });
      if (duplicateItem) {
        return state;
      } else {
        return [...state, action.payload];
      }
    case REMOVE_FROM_CART: //개별삭제
      return state.filter((item) => item.id !== action.payload);
    case REMOVE_ALL: //전체삭제
      return [];
    default:
      return state;
  }
};

export default cartReducer;


//rootReducer.js
//루트 리듀서에 cartReducer 및 combineReducers, persistReducer 추가

import cartReducer from "./cartReducer";
import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";

const rootReducer = combineReducers({
  cart: cartReducer,
});

const persistConfig = {
  key: "root",
  storage: storage,
  whitelist: ["cart"],
}; 
// cart에 담긴 card Data List를 storage에 저장하기 위해서 persistConfig 사용

export default persistReducer(persistConfig, rootReducer);

persistConfig를 사용하기 위해서는 index.js에도 persistGate를 설정해주어야 한다.

//index.js

import { Provider } from "react-redux";
import { createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import rootReducer from "./redux/reducers/rootReducer";

const store = createStore(rootReducer, composeWithDevTools());

const persistor = persistStore(store);

ReactDOM.render(
  <Provider store={store}>
    <PersistGate persistor={persistor}>
      <ThemeProvider theme={theme}>
        <GlobalStyles />
        <Routes />
      </ThemeProvider>
    </PersistGate>
  </Provider>,
  document.getElementById("root")
);

//cartAction.js

export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const REMOVE_ALL = "REMOVE_ALL";

export const addToCart = (data) => {
  return {
    type: "ADD_TO_CART",
    payload: data, //추가된 card(data)
  };
};

export const removeFromCart = (id) => {
  return {
    type: "REMOVE_FROM_CART",
    payload: id, //삭제해야하는 id
  };
};

export const removeAll = () => {
  return {
    type: "REMOVE_ALL",
    payload: [], //빈배열을 return함으로서 전체삭제 
  };
};

redux의 폴더만 살펴보면 위와 같은 코드로 작성될 수 있다. (아직 persist 사용 전)

이제 이 action을 해당 기능이 구현되어야하는 컴포넌트와 연결시켜준다.

우선 카드가 클릭될 때마다 cart state에 card data가 더해지므로 column.js파일로 가보자.


import { connect } from "react-redux"; //연결!
import { addToCart } from "../../../redux/actions/cartActions";//action import

  const handleCheckMovePage = () => {
    //card를 클릭 시 token이 없으면 로그인 페이지로 이동
    if (!sessionStorage.getItem("token")) {
      alert("로그인이 필요한 서비스입니다.");
      history.push("/login"); 
    }
    // 로그인 없이도 볼 수 있는 card를 따로 만들어놓았다.
    if (data.id === 11) {
      history.push("/Industries");
    } else {
      // token이 있다면 취소/확인 버튼을 통해 확인을 하면 장바구니로 이동, 아니면 현재 페이지에 머무르도록 설정.
      if (
        sessionStorage.getItem("token") &&
        window.confirm("장바구니로 이동하시겠습니까?")
      ) {
        history.push("/cart");
      }
      //최종적으로 모든 조건문을 통과할 경우 addToCart action이 실행된다.
      addToCart(data);
    }
  };

  return 
    <ColumnContainer onClick={handleCheckMovePage} data={data}/>
    
  //첫번째 값은 mapStateToProps, 두번째 값은 mapDispatchToProps이다. 
  export default connect(null, { addToCart })(Column); 

이후 추가된 card(data) list들의 삭제 기능 구현을 위하여 Item.js파일로 이동

//Item.js

import { connect } from "react-redux";
import { removeFromCart } from "../../redux/actions/cartActions";

function Item({ item, removeFromCart }) {
  const handleRemove = () => {
    removeFromCart(item.id);
  };
  
  return 
        <button className="delBtn" onClick={handleRemove}>
        <FontAwesomeIcon className="searchIcon" icon={faMinus} />
      </button>

  export default connect(null, { removeFromCart })(Item);

Nav 상단 카트 아이콘에도 card data list의 갯수를 보여주고, cart에 card list가 추가될 때마다 아이콘이 바뀌고 리스트 추가 삭제 창이 보여지므로 마찬가지로 action을 넘겨주어야 한다.

//Nav.js

import { connect } from "react-redux";
import { removeFromCart, removeAll } from "../../redux/actions/cartActions";

function Nav({ cart, removeFromCart, removeAll }) {
 
   const handleRemove = (id) => {
   removeFromCart(id);
 };

 const handleAllRemove = () => {
   removeAll();
 };

 return(
   <RightTab>
       <span>{cart.length}</span>
       {cart.length > 0 ? (
         <img
           className="cart-red"
           onMouseEnter={() => setCartBox(true)}
           onClick={handleTokenCheck}
           alt="cart"
           src="./images/svg/cart-red.svg"
         />
       ) : (
         <img
           className="cart"
           onMouseEnter={() => setCartBox(true)}
           onClick={handleTokenCheck}
           alt="cart"
           src="./images/svg/cart.svg"
         />
       )}
     </RightTab>
     {cartBox && (
       <CartBoxContainer>
         <h3>Data Cart</h3>
         <span>{cart.length} Dataset</span>
         <ul>
           {cart.map((item) => {
             return (
               <>
                 <li>
                   {item.title}
                   <button onClick={() => handleRemove(item.id)}>X</button>
                 </li>
               </>
             );
           })}
         </ul>
         <ClearBtn onClick={() => handleAllRemove()}>
           <FontAwesomeIcon className="edit" icon={faTrashAlt} />
           <p>Clear Cart</p>
         </ClearBtn>
       </CartBoxContainer>
 )
 

 const mapStateToProps = (state) => {
 return {
   cart: state.cart,
 };
};

export default connect(mapStateToProps, { removeFromCart, removeAll })(Nav);
profile
하고싶은게 많은 사람. Front-end Developer
post-custom-banner

0개의 댓글