Cmarket Shopping App은 Create React App으로 만든 리액트 앱에 리덕스를 붙인 구조다
아이템 리스트 페이지(ItemListContainer)와 장바구니 페이지(ShoppingCart) 총 두 페이지로 간단하게 만들어져 있고
Store의 initial state에는 전체 아이템 목록(items), 장바구니 목록(cartItems)이 들어있다
hooks, useDispatch, useSelector를 사용해서 여러컴포넌트에서 Store(state)에 접근 할 수 있다
Redux hooks에서는 크게useSelector(), useDispatch() 이 2가지의 메소드를 기억하면 된다
액션
: 상태에 어떤 변화가 필요하면 액션이 발생한다
하나의 객체로 표현되고 액션 객체는 type을 무조건 가지고 있어야한다
예시
{
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
액션 생성 함수
: 액션 객체를 만들어주는 함수
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
}
리듀서
: 변화를 일으키는 함수
액션을 만들어 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아오고 두 값을 참조해 새로운 상태를 만들어 반환한다
const initialState = {counter : 1}
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return {
counter: state.counter + 1
}
default:
return state;
}
}
리덕스를 사용할 때는 액션타입, 액션 생성 함수, 리듀서 코드를 작성해야 한다
이 코드를 각각 다른파일에 작성하는 방법과 기능 별로 묶어서 파일하나에 작성하는 방법이 있는데
전에 블로깅을 했던 방법은 하나에 묶어서 사용했고
이번 스프린트는 액션 타입, 액션 생성 함수가 actions > index.js
리듀서코드가 reducers > itemReducer.js 에 작성되어 있다
리덕스 공식문서에서도 하나에 묶어서 사용하는 방법을 택해서
함수가 나눠져있는건 처음 봐서 이해하는데 시간이 조금 걸렸다
dispatch 할때 데이터를 실어서 보낼 수 있다
이 데이터를 리덕스의 state에 추가해주세요 라는 뜻이고 payload라고 부른다
const handleDelete = (itemId) => {
dispatch(removeFromCart(itemId))
}
payload(보낸데이터)를 인자로 받아서 action 객체를 만들어 내보내주는 액션 함수를 작성해야 한다
액션은 어플리케이션으로부터 온 데이터를 스토어에 전송하는 정보의 페이로드(payloads)라고 할 수 있다
payload 객체는 함수 실행에 대한 결과이고 addToCart함수를 실행하게 되면 payload객체가 생성된다
이 payload객체 안에는 quantity,itemId가 존재하고
리듀서의 2번째 인자 action에 저장된다
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
}
보낸 데이터는 리듀서의 2번째인자 action 파라미터에 저장되어 있고
action.payload로 꺼내서 사용할 수 있다
state는 변하지 않아야 하기 때문에 초기 state를 복사한 값 + 새로운 데이터를 더해서 state를 변경 해 줄 수 있다
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
default:
return state;
}
}
2번 reducer작성에서 나오듯이 redux의 state는 변하지 않아야 한다 이때 state 변경을 할 수 있는 유일한 방법은 액션을 보내는 것이다
1 Dispatch로 액션함수를 호출해 액션을 보내고
2 액션이 발생하게 되면
3 리듀서가 액션을 받아 초기state를 복사한값에 새로운 데이터를 더해 리턴해줘서 state 변경이 가능하다
payload는 action의 type에 따라서 필요한 state값을 가지고 있어야 한다
카트에 추가, 삭제, 값변경을 할때
필요한 값이 뭐가 있는지 생각한뒤 payload에 적어주면 된다
// action types
export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const SET_QUANTITY = "SET_QUANTITY";
export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";
// actions creator functions
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
}
}
}
리덕스 상태는 읽기 전용이다
기존에 리액트에서 setStates를 사용해서 state를 업데이트 할 때도 객체나 배열의 불변성을 지켜주기 위해 spread연산자를 사용한 것 과 같은 개념이다
상태를 업데이트 할 때 기존의 객체는 건드리지 않고 새로운 객체를 생성해야 한다
Immutability(불변성)을 지켜서 state를 변경하는 방법은
Object.assign으로 새로운 객체를 만들어 리턴해서 사용 할 수 있다
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
//TODO
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
break;
case REMOVE_FROM_CART:
//TODO
let currentItem = state.cartItems.filter((el) => el.itemId !== action.payload.itemId)
return Object.assign({}, state, {
cartItems: currentItem
})
break;
case SET_QUANTITY:
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
return {
...state,
cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)]
}
break;
default:
return state;
}
}
export default itemReducer;
코드들을 다 작성했으니 마지막으로 그 코드를 사용할 수 있게 해달라는 요청을 useDispatch()로 보내줘야 한다
handleClick함수안에 dispatch로 함수를 실행시켜 주고 인자를 넘겨주면 된다
...
function ItemListContainer() {
const state = useSelector(state => state.itemReducer);
const { items, cartItems } = state;
const dispatch = useDispatch();
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
//TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달하세요.
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;
...
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 handleQuantityChange = (quantity, itemId) => {
//TODO: dispatch 함수를 호출하여 액션을 전달하세요.
dispatch(setQuantity(itemId,quantity))
}
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId))
//TODO: dispatch 함수를 호출하여 액션을 전달하세요.
dispatch(removeFromCart(itemId))
}
....
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>
)
}