사실 장바구니 기능을 구현하게 되면서 Redux Toolkit을 알게되었습니다!
사실 장바구니에 대한 정보는 서버에서 관리할 필요가 있을 수도 없을 수도 있습니다!
장바구니는 단순히 물건을 담아놓았을 뿐, 실제 구매를 하지는 않은 값 입니다!
그래서 딱히 서버에 내용을 저장할 이유는 없겠죠..?
하지만 저장해야 하는 경우도 있습니다!
로그인 한 회원이 웹이든 앱이든 어떤 곳에서 접속을 해도 해당 사용자가 가지고 있는 장바구니에 대한 정보를 저장하고 싶다면!
이때는 상태 관리 라이브러리가 아닌 따로 서버에 저장을 해서 관리해주어야 합니다!
저는 따로 서버가 없고 단순히 기능만 구현할 거라 상태관리라이브러리를 사용하도록 하겠습니다!ㅎㅎ
왜 많고 많은 라이브러리 중에 이건가요?? 하신다면,,,
일단 이걸 보십쇼!
간단하게 정리를 하자면
Redux
Recoil
Zustand
인데 사실 필요에 따라 사용하면 됩니다..!
저는 리덕스가 나온지 오래되었기 때문에 관련 내용도 많고 편리할 것으로 생각되어 사용하려 했습니다..!
(뒤에서 말할거긴 하지만 redux-persist 때문에 사용한 것도 있습니다!)
하
지
만
리덕스는 단점도 있습니다.
이러한 점들을 보완해서 나온것이 바로
Redux-toolkit 입니다!
여기서 제가 좀 더 알아보고 싶었던 부분은 바로 불변성 입니다.
1. 하나의 애플리케이션 안에는 하나의 스토어가 있습니다.
2. 상태는 읽기전용 입니다.
3. 변화를 일으키는 함수, 리듀서는 순수한 함수여야 합니다.
이 규칙 중 2번째가 불변성 유지와 연관이 있습니다.
리덕스에서 불변성 을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위해서 shallow equality 검사를 하기 때문입니다.
또한! 개발자 도구를 이용하면 상태를 뒤로 돌릴 수도 있고 앞으로도 돌릴 수 있습니다!
Redux Toolkit의 createReducer API는 내부적으로 자동으로 Immer를 사용합니다.
이를 통해서 불변성을 유지해 createReducer에 전달되는 모든 리듀서 함수 내부에서 상태를 "변경"하는 것이 이미 안전하게 됩니다.
그래서 Immer 패턴 사용시에는 상태 값을 변경하거나 새로운 상태 값을 반환하는 것이 좋습니다!
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// ❌ ERROR: mutates state, but also returns new array size!
brokenReducer: (state, action) => state.push(action.payload),
// ✅ SAFE: the `void` keyword prevents a return value
fixedReducer1: (state, action) => void state.push(action.payload),
// ✅ SAFE: curly braces make this a function body and no return
fixedReducer2: (state, action) => {
state.push(action.payload)
},
},
})
출처: https://itchallenger.tistory.com/706 [Development & Investing:티스토리]
1번째는 변경된 상태를 반환하고 있기 때문에 돌연변이 상태입니다!
2번째는 'void' 키워드를 통해서 반환값을 막아주고 있기 때문에 안전합니다!
3번째는 함수안에 넣었기 때문에 따로 반환값이 없어 안전합니다!
이러한 패턴을 지켜서 Redux-toolkit을 사용하도록 합시다!
간단하게 필요한 기능만 정리했습니다!
npm install @reduxjs/toolkit
(저는 store 폴더를 만들고 그 안에 작성했습니다!)
import cart from "./cart/cartSlice.js";
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
cart: cart.reducer,
},
});
export default store;
store는 애플리케이션의 전역 상태를 저장하고 관리하는 역할을 합니다.
configureStore의 역할은 Redux 스토어를 쉽게 생성할 수 있도록 도와줍니다.
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { RouterInfo } from "./util/router.jsx";
import { Provider } from "react-redux";
import store from "./store/store.js";
const RouterObject = createBrowserRouter(RouterInfo);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={RouterObject} />
</Provider>
</React.StrictMode>
);
Provider를 이용해 전역 상태를 사용할 범위를 지정해줍니다.
이때 앞에서 작성했던 store 파일을 prop으로 전달받습니다!
import { createSlice, createSelector } from "@reduxjs/toolkit";
let cart = createSlice({
name: "cart",
initialState: [],
reducers: {
increaseCount(state, action) {
const item = state.find((obj) => obj.id === action.payload);
if (item) {
item.count += 1;
}
},
decreaseCount(state, action) {
const itemId = action.payload;
const updatedState = state.map((item) => {
if (item.id === itemId) {
return {
...item,
count: Math.max(0, item.count - 1),
};
}
return item;
});
return updatedState.filter((item) => item.count > 0);
},
insertItem(state, action) {
const newItem = action.payload;
const existingIndex = state.findIndex((obj) => obj.id === newItem.id);
if (existingIndex === -1) {
return [...state, newItem];
} else {
return state.map((item, index) =>
index === existingIndex
? {
...item,
count: item.count + newItem.count,
}
: item
);
}
},
clearCart() {
return [];
},
},
});
export let { increaseCount, decreaseCount, insertItem, clearCart } =
cart.actions;
export const selectCartItems = (state) => state.cart;
export const selectCartTotal = createSelector([selectCartItems], (cartItems) =>
cartItems.reduce(
(total, item) =>
total +
(item.discountPercent === 0
? item.price * item.count
: (item.price * (100 - item.discountPercent)) / 100) *
item.count,
0
)
);
export default cart;
상태, 리듀서, 액션 생성 함수를 포함하는 slice 파일을 만듭니다!
구현한 액션함수
🚨 불변성은...?
Redux Toolkit은 Immer 라이브러리를 내부적으로 사용하고 있으므로, 이 라이브러리 덕분에 상태를 직접 변경>하는 것처럼 코드를 작성해도 불변성이 유지됩니다!
그리고 저는 선택자를 만들어 주었습니다.
export const selectCartTotal = createSelector([selectCartItems], (cartItems) =>
cartItems.reduce(
(total, item) =>
total +
(item.discountPercent === 0
? item.price * item.count
: (item.price * (100 - item.discountPercent)) / 100) *
item.count,
0
)
);
선택자는 상태를 읽어오는 함수의 일종입니다.
따라서 전역적으로 관리되는 상태를 읽어오는 역할을 합니다!
저는 금액의 총액을 편하게 받아오기 위해서 위와 같은 선택자를 생성해주었습니다!
useDispatch
store의 디스패치 함수를 반환합니다.
useDispatch를 통해 액션을 dispatch 하면, 해당 액션을 처리하는 리듀서가 실행되어 상태가 업데이트 됩니다.
import { useDispatch } from "react-redux";
import { insertItem } from "../store/cart/cartSlice";
const dispatch = useDispatch();
dispatch(insertItem(obj));
위 처럼 액션 함수를 사용할 수 있게 됩니다.
저같은 경우에는 장바구니에 상품을 추가할 때 사용해 주었습니다!
useSelector
store의 상태를 조회하는데 사용합니다.
useSelector 에는 상태를 조회하는 선택자 함수를 인자로 전달하면, 해당 선택자를 통해 조회된 상태를 반환받을 수 있습니다.
import { useSelector } from "react-redux";
import { selectCartTotal } from "../store/cart/cartSlice";
const total = useSelector(selectCartTotal);
이 경우가 선택자를 인자로 줘서 사용하는 경우입니다!
import { useSelector } from "react-redux";
const list = useSelector((state) => state.cart);
이 경우는 단순히 상태 값을 가져와서 사용하는 경우입니다!
따라서 useSelector 인자로 함수를 주어 장바구니에 있는 모든 상태 값을 가져오고 있습니다.
잘 작동하는 것을 확인하실 수 있습니다!
근데 사실.... 저희가 짠 장바구니는 새로고침하면 사라지게 됩니다!!!! 뜨헉...??
상태를 브라우저에 지속적으로 저장할 수 있도록 해주는 라이브러리입니다!
따라서 새로고침을 해도 저희의 상태가 유지되게 됩니다!
npm install redux-persist
persistReducer와 persistStore를 사용해 리듀서와 스토어를 감싸줍니다.
import { configureStore } from "@reduxjs/toolkit";
import cart from "./cart/cartSlice";
import storage from "redux-persist/lib/storage";
import { combineReducers } from "redux";
import { persistStore, persistReducer } from "redux-persist";
const persistConfig = {
key: "root",
storage: storage,
};
const reducer = combineReducers({
cart: cart.reducer,
});
const persistedReducer = persistReducer(persistConfig, reducer);
export const store = configureStore({
reducer: persistedReducer,
});
export const persistor = persistStore(store);
persistReducer
persistStore
🧐 스토리지 선택?
세션과 로컬 차이
LocalStorage
SessionStorage
장바구니는 계속 값이 유지되고, 값들이 독립적으로 동작하면 안되기 때문에 로컬스토리지를 쓰는게 더 적합하다고 판단됩니다!
로컬 스토리지
import storage from "redux-persist/lib/storage";
const persistConfig = {
key: "root",
storage: storage,
};
윗 부분을 통해서 로컬 스토리지에 내용을 저장하게 됩니다!
세션 스토리지
import storageSession from 'redux-persist/lib/storage/session';
const persistConfig = {
key: 'root',
storage: storageSession, // 변경된 부분
};
위와 같이 import 해주고 persistConfig의 storage를 변경해주면 됩니다!
import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { RouterInfo } from "./util/router.jsx";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { store, persistor } from "./store/store.js"; // Redux 스토어와 persistor 가져오기
const RouterObject = createBrowserRouter(RouterInfo);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={RouterObject} />
</PersistGate>
</Provider>
</React.StrictMode>
);
장바구니에 값도 잘 들어가고
무엇보다도! 새로고침해도 값이 사라지지 않습니다!
이렇게 확인해보면 로컬 스토리지에 값이 들어간 것이 보입니다!
.
.
.
.
.
긴 글... 읽어주셔서 감사합니다!
리덕스를 써야 할 때 & 리덕스의 장단점 (feat. 공식문서)
Recoil을 이용한 손쉬운 상태관리
상태 관리 라이브러리
리덕스의 장점, 단점
리덕스의 3가지 규칙
Redux Toolkit : Immer와 함께 사용하기
[React] 장바구니에 아이템 추가해보기