
configureStore, createSlice, createAsyncThunk 등의 쉬운 API 제공npm install @reduxjs/toolkit react-redux// src/stores/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
totalNum: 0,
totalPrice: 0,
cartList: [],
};
export const cartSlice = createSlice({
// (1) 슬라이스 이름
name: 'cart',
// (2) 초기 상태 값
initialState: initialState,
// (3) 리듀서
reducers: {
addToCart: (state, action) => {
const itemToAdd = action.payload;
const existingItem = state.cartList.find(
(cartItem) => cartItem.id === itemToAdd.id
);
if (existingItem) {
existingItem.quantity += 1;
} else {
state.cartList.push({ ...action.payload, quantity: 1 });
}
state.totalNum += 1;
state.totalPrice += itemToAdd.price;
},
clearCart: () => initialState,
// clearCart: (state) => {
// state = initialState; // 정상 작동하지 않음!
// }
// ...리듀서 함수들
);
// export. 모든 액션 생성자 함수 (컴포넌트에서 dispatch 하기 위해)
export const {
addToCart,
clearCart,
} = cartSlice.actions;
// default export. 전체 리듀서 함수 (스토어에 등록하기 위함)
export default cartSlice.reducer;state값을 직접 변경해도, Immer 라이브러리가 자동적으로 새로운 객체를 만들어 불변성을 유지시켜줌clearCart 처럼, state 값에 새로운 값을 대입하는 것이 아니라, 반환 값으로 객체를 반환해야함// src/stores/store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
export const store = configureStore({
reducer: {
cart: cartReducer,
// user: userReducer, ... 처럼 확장해서 사용
},
});react-redux에서 제공하는 컴포넌트인, <Provider>로 최상위에서 감싸서 사용<Provider>의 props로 위에서 등록한 store을 전달하면, 하위 컴포넌트에서 접근 가능// main.jsx
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './stores/store.js';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);
react-redux에서 제공하는 useDispatch() 훅 사용cartSlice.js에서 정의한 액션 생성자를 import해서, action 객체를 생성하여 사용// src/features/product/components/productItem/ProductItem.jsx
import { useDispatch } from 'react-redux';
import { addToCart } from '../../../../stores/cartSlice.js';
function ProductItem({ product }) {
const dispatch = useDispatch();
const handleClickAdd = (item) => {
// addToCart 함수 === 액션 생성자 함수
// 생성 결과: { type: 'cart/addToCart', payload: item객체 }
dispatch(addToCart(item));
};
return (
// ...
<button
className="product-add-button"
onClick={() => handleClickAdd(product)}
>
장바구니에 추가
</button>
// ...
);
}react-redux의 useSelector() 훅 사용useSelector()를 통해 상태 트리의 데이터를 읽어올 수 있음// src/features/cart/Cart.jsx
import CartItem from './components/cartItem/CartItem';
import { useDispatch, useSelector } from 'react-redux';
import { clearCart } from '../../stores/cartSlice';
function Cart() {
// store.js에서 등록한 이름으로 불러옴
const cart = useSelector((state) => state.cart);
// cart = { totalNum : 0, totalPrice: 0, cartList: [] }
// ...
return (
// ...
<div>
{cart.cartList.map((item) => (
<CartItem key={item.id} item={item} />
))}
</div>
// ...
);
}Context API는 “전역 상태 관리 도구로 볼 수 없다”라는 이야기를 들어왔는데, 정확히 어떤 차이점이 있는지 알아보자.
state의 일부 만을 사용한다고 해도 전체 state를 구독해야함state.a 만 사용한다고 가정할 때, state.b가 변경되어도 해당 컴포넌트는 리렌더링 됨 (불필요한 리렌더링)const a = useSelector((state) => state.user.a) 처럼 선택적 구독이 가능함state.user.b가 변경되어도 해당 컴포넌트는 리렌더링이 되지 않음따라서 정리하자면,
상태의 범위가 작거나 지역적으로 상태를 관리하고 싶다면, 가벼운 설정이 가능한 Context API + useReducer를,
상태의 범위가 크고 전역적으로 상태를 관리하고자 한다면 Redux Toolkit 등의 전역 상태 관리 도구를 채택하여 사용하는 것이 적절한 방식이다.