지난번엔 useState를 통해 상태를 관리했었다.
하지만, 컴포넌트 간 props로만 상태를 전달하고 관리하려니, props drilling
이 일어나게 되었다.
이럴 때 사용하는 것이 Redux
이다.
Redux는 자바스크립트에서 상태를 관리하기 위한 도구다.
Redux는 단 하나의 store에서 전체 app의 상태를 관리하는 것이다.
store는 읽기 전용이며, action이라 불리는 객체를 통해 상태를 변경 할 수 있다.
Reducer는 현재 상태와 액션을 입력으로 받아서 새로운 상태를 반환하는 순수 함수다.
순수 함수이므로, 예측 가능한 결과를 도출한다.
const itemReducer = (state = initialState, action) => {
// 초기값,action
switch (action.type) {
case ADD_TO_CART:
//TODO..
case REMOVE_FROM_CART:
//TODO..
case SET_QUANTITY:
// TODO..
default:
return state;
}
};
action에 따라 state를 업데이트 시킨다.
리듀서는 순수 함수이고 내부 case는 객체의 불변성을 지키므로 App 상태의 추론을 더욱 쉽게 만들어준다.
react-redux는 react 안에서 보다 redux를 편하게 사용할 수 있게 해주는 라이브러리다.
react-redux의 사용 방법은 다음과 같다. (Counter)
1. React-Redux 설치하기
npm install react-redux // npm install redux
2. 액션 생성자 만들기
export const incrementCounter = () => ({ type: 'INCREMENT_COUNTER' }); export const decrementCounter = () => ({ type: 'DECREMENT_COUNTER' });
3. 액션 생성자를 통한 리듀서 만들기.
const initialState = { count: 0 }; const counterReducer = (state = initialState, action) => { switch (action.type) { case 'INCREMENT_COUNTER': return { ...state, count: state.count + 1 }; case 'DECREMENT_COUNTER': return { ...state, count: state.count - 1 }; default: return state; } }; export default counterReducer;
4. Reducer를 기반으로 한 store 만들기
import { createStore } from 'redux'; import counterReducer from './reducers'; const store = createStore(counterReducer);
5. App의 최상위 컴포넌트를 Provider로 감싸주기
import { Provider } from 'react-redux'; import store from './store'; function App() { return ( <Provider store={store}> <Counter /> </Provider> ); }
6.위에서 만든 것을 사용하는 Counter컴포넌트 만들기
import { useSelector, useDispatch } from 'react-redux'; import { incrementCounter, decrementCounter } from './actions'; function Counter() { const count = useSelector(state => state.count);// useSelector를 이용해 store에서 state를 가져온다. //state는 store에서 count:0이 default임. const dispatch = useDispatch(); const increment = () => { dispatch(incrementCounter()); }; const decrement = () => { dispatch(decrementCounter()); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); }
각 컴포넌트들은 Reducer에서 state를 받아와 하위 컴포넌트에 전달해주고
컴포넌트에서 상태 변경이 필요할 경우 dispatch를 통해 Reducer로 넘겨준다.
//store.js
import {legacy_createStore as createStore} from "redux"
import rootReducer from '../reducers/index"
const store = createStore(rootReducer);
store.js에서 store를 선언 해준다.
그 후, rootReducer를 import 해와서 Store에 넣어준다.
이러면, rootReducer에 있는 모든 reducer가 store에 저장이 된다.
// reducers/index.js
import { combineReducers } from "redux";
import itemReducer from "./itemReducer";
import notificationReducer from "./notificationReducer";
const rootReducer = combineReducers({
itemReducer,
notificationReducer,
});
export default rootReducer;
reducer가 선언되는 곳이다.
현재 CMARKET에는 두 개의 reducer가 사용되는데
여러 개의 reducer를 사용하려면 redux에 있는 combineReducers
를 사용한다.
// reducers/initialState.js
export const initialState = {
items: [
{
id: 1,
name: "노른자 분리기",
img: "../images/egg.png",
price: 9900,
},
{
id: 2,
name: "2020년 달력",
img: "../images/2020.jpg",
price: 12000,
},
......
cartItems: [
{
itemId: 1,
quantity: 1,
},
{
itemId: 5,
quantity: 7,
},
{
itemId: 2,
quantity: 3,
},
],
Reducer의 초기값을 담당하는 객체 배열이다.
상품 item과 카트에 담긴 상품의 배열이 담겨져있다.
// reducers/itemReducer.js
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";//action의 type과 payload를 가져오는 함수
import { initialState } from "./initialState";//초기값
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
//TODO : cartItems에 아이템 추가
// 객체의 불변성을 위해 ...state로 기존 배열을 복사한 후 추가한다.
//state(initialState)의 cartItems를 변경시키는 것.
return {
...state,
cartItems: [...state.cartItems, action.payload],
};
case REMOVE_FROM_CART:
//TODO : cartItem에서 해당 상품 제거
// action.payload << 여기에 itemId 들어가 있음.
const removearr = state.cartItems.filter((it) => {
return it.itemId !== action.payload.itemId;
});
return {
...state,
cartItems: removearr,
};
case SET_QUANTITY:
// TODO : 상품의 수량 변경
let idx = state.cartItems.findIndex(
(el) => el.itemId === action.payload.itemId
); // cartItem 배열에서 payload로 온 itemid랑 같은 인덱스
let updatedcart = [...state.cartItems];
// cartItems 배열 복사
const sameItem = updatedcart[idx];
// 복사한 배열에서 상품 추출
let updated = {
...sameItem,
quantity: action.payload.quantity,
};
updatedcart[idx] = updated;
return {
...state,
cartItems: updatedcart,
};
default:
return state;
}
};
export default itemReducer;
초기값을 state로 사용하는 itemReducer다.
각 action.type마다 실행하는 로직이 있으며, initialState의 상태를 변경시킨다.
//actions/index.js
export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const SET_QUANTITY = "SET_QUANTITY";
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId,
},
};
};
export const removeFromCart = (itemId) => {
return {
type: REMOVE_FROM_CART,
payload: {
itemId,
},
};
};
export const setQuantity = (itemId, quantity) => {
return {
type: SET_QUANTITY,
payload: {
quantity,
itemId,
},
};
};
각 함수마다 전달하는 action type,payload가 담겨져있다.
//pages/ItemListContainer
import React from "react";
import { addToCart, notify } from "../actions/index";
import { useSelector, useDispatch } from "react-redux";
import Item from "../components/Item";
function ItemListContainer() {
const state = useSelector((state) => state.itemReducer);
// rootReducer에서 가져온 reducer 중 하나인 itemReudcer
// 해당 reducer에서 useSelector를 통하여 state를 가져온다.
const { items, cartItems } = state;
// state(initialState)는 총 2개의 배열이 있으며 이것을 구조 분해 할당을 통해 변수로 선언해준다.
const dispatch = useDispatch();
// action을 전달하기 위한 useDispatch 선언
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(addToCart(item.id));
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
} else {
dispatch(notify("이미 추가된 상품입니다."));
}
};
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">쓸모없는 선물 모음</div>
{items.map((item, idx) => (
<Item
item={item}
key={idx}
handleClick={() => {
handleClick(item);
}}
/>
))}
</div>
</div>
);
}
export default ItemListContainer;
상품을 보여주는 메인 화면이다.
상품 추가를 해주는 함수 handleClick이 선언되어있다.
// pages/ShoppingCart.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { removeFromCart, setQuantity } from "../actions";
import CartItem from "../components/CartItem";
import OrderSummary from "../components/OrderSummary";
export default function ShoppingCart() {
const state = useSelector((state) => state.itemReducer);
const { cartItems, items } = state;
const dispatch = useDispatch();
const [checkedItems, setCheckedItems] = useState(
cartItems.map((el) => el.itemId)
);
const handleCheckChange = (checked, id) => {
if (checked) {
setCheckedItems([...checkedItems, id]);
} else {
setCheckedItems(checkedItems.filter((el) => el !== id));
}
};
const handleAllCheck = (checked) => {
if (checked) {
setCheckedItems(cartItems.map((el) => el.itemId));
} else {
setCheckedItems([]);
}
};
// 수량 수정 함수
const handleQuantityChange = (quantity, itemId) => {
// 선택된 상품의 id와 수량을 넘겨준다.
dispatch(setQuantity(itemId, quantity));
};
// 장바구니 상품 제거 함수
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId));
dispatch(removeFromCart(itemId));
};
const getTotal = () => {
let cartIdArr = cartItems.map((el) => el.itemId);
let total = {
price: 0,
quantity: 0,
};
for (let i = 0; i < cartIdArr.length; i++) {
if (checkedItems.indexOf(cartIdArr[i]) > -1) {
let quantity = cartItems[i].quantity;
let price = items.filter((el) => el.id === cartItems[i].itemId)[0]
.price;
total.price = total.price + quantity * price;
total.quantity = total.quantity + quantity;
}
}
return total;
};
const renderItems = items.filter(
(el) => cartItems.map((el) => el.itemId).indexOf(el.id) > -1
);
const total = getTotal();
return (
<div id="item-list-container">
<div id="item-list-body">
<div id="item-list-title">장바구니</div>
<span id="shopping-cart-select-all">
<input
type="checkbox"
checked={checkedItems.length === cartItems.length ? true : false}
onChange={(e) => handleAllCheck(e.target.checked)}
></input>
<label>전체선택</label>
</span>
<div id="shopping-cart-container">
{!cartItems.length ? (
<div id="item-list-text">장바구니에 아이템이 없습니다.</div>
) : (
<div id="cart-item-list">
{renderItems.map((item, idx) => {
const quantity = cartItems.filter(
(el) => el.itemId === item.id
)[0].quantity;
return (
<CartItem
key={idx}
handleCheckChange={handleCheckChange}
handleQuantityChange={handleQuantityChange}
handleDelete={handleDelete}
item={item}
checkedItems={checkedItems}
quantity={quantity}
/>
);
})}
</div>
)}
<OrderSummary total={total.price} totalQty={total.quantity} />
</div>
</div>
</div>
);
}
장바구니를 보여주는 페이지다.
장바구니에서 상품 삭제 및 수량 변경이 가능하다.
import React from 'react'
export default function Item({ item, handleClick }) {
return (
<div key={item.id} className="item">
<img className="item-img" src={item.img} alt={item.name}></img>
<span className="item-name" data-testid={item.name}>{item.name}</span>
<span className="item-price">{item.price}</span>
<button className="item-button" onClick={(e) => handleClick(e, item.id)}>장바구니 담기</button>
</div>
)
}
ItemListContainer에 있는 컴포넌트로 장바구니 담기의 기능을 담당하는 함수에 params를 전달해준다.
import React from 'react'
export default function CartItem({
item,
checkedItems,
handleCheckChange,
handleQuantityChange,
handleDelete,
quantity
}) {
return (
<li className="cart-item-body">
<input
type="checkbox"
className="cart-item-checkbox"
onChange={(e) => {
handleCheckChange(e.target.checked, item.id)
}}
checked={checkedItems.includes(item.id) ? true : false} >
</input>
<div className="cart-item-thumbnail">
<img src={item.img} alt={item.name} />
</div>
<div className="cart-item-info">
<div className="cart-item-title" data-testid={`cart-${item.name}`}>{item.name}</div>
<div className="cart-item-price">{item.price} 원</div>
</div>
<input
type="number"
min={1}
className="cart-item-quantity"
value={quantity.toString().replace(/(^0+)/, "")}
// input에서 한 자리 숫자면 앞에 0이 붙는걸 제거해주는 표현식
onChange={(e) => {
handleQuantityChange(Number(e.target.value), item.id)
}}>
</input>
<button className="cart-item-delete" onClick={() => { handleDelete(item.id) }}>삭제</button>
</li >
)
}
장바구니 페이지인 ShoppingCart에 있는 컴포넌트.
수량 변경 및 장바구니에서 상품 삭제를 담당하는 함수에 params를 넘겨준다.