redux가 아직 감이 잘 안잡혀 반딧불반에 오게 되었다 ..ㅎㅎ 오늘은 Redux 실습인 cmarket_redux를 다시 풀어보고 의사코드를 작성해 볼 것이다.
문제
Bare minimum requirements
- Action, Reducer를 직접 작성하고 테스트 케이스를 통과합니다. 테스트 케이스 순서대로 과제를 진행해 주세요.
Getting Started
다음 Cmarket Shopping App은 Create React App으로 만든 React 애플리케이션에 Redux를 붙인 구조입니다.
- 아이템 리스트 페이지(ItemListContainer)와 장바구니 페이지(ShoppingCart) 총 두 페이지로 간단하게 구성됩니다.
- Store의 initial state에는 전체 아이템 목록(items), 장바구니 목록(cartItems)이 들어있습니다.
- 각 ItemListContainer, ShoppingCart 페이지 컴포넌트 및 components 폴더의 여러 컴포넌트들에서 Store(state)에 접근해 보세요. (Redux에서 제공하는 hooks, useDispatch, useSelector를 사용합니다.)
이 과정을 통해 Action, Dispatch, Reducer, Store가 어떻게 유기적으로 연결되어 있는지 배우실 수 있습니다. 앞선 강의에서 학습한 각각의 개념들을 다시 정리해 볼까요?
Action
Action은 말 그대로 어떤 액션을 취할 것인지 정의해 놓은 객체입니다.
{ type: ‘ADD_TO_CART’, payload: request }
보통 위와 같은 모양으로 구성됩니다. 여기서 type은 필수로 지정을 해 주어야 하며, 그 외의 것들은 선택적으로 사용할 수 있습니다.
이렇게 모든 변화를 Action을 통해 취하는 것은 우리가 만드는 앱에서 무슨 일이 일어나고 있는지 직관적으로 알기 쉽게 하는 역할을 합니다.
actions > index.js 파일에서는 Action들을 구성합니다. 장바구니를 구현하기 위해서 필요한 액션들이 무엇이 있을지 고민한 후 코드를 작성해 보세요.
Dispatch
Dispatch는 Action을 전달하는 메서드입니다. Dispatch의 전달인자로 Action 객체가 전달됩니다. 그리고 Reducer를 호출해 state의 값을 바꾸는 역할을 합니다.
Store
말 그대로 state가 관리되는 오직 하나뿐인 저장소의 역할을 합니다. Redux 앱의 state가 저장되어 있는 공간이죠. 다음은 createStore 메서드를 활용해 Reducer를 연결하는 방법인데요, createStore와 더불어 다른 Reducer의 조합을 인자로 넣어서 스토어를 생성할 수 있습니다. (실제 소스 코드에서는 미들웨어와 Redux devtools 지원을 위해 두 번째 인자에 추가적인 내용이 들어가 있습니다.)
const store = createStore(rootReducer);
store > store.js 파일에서 createStore 메서드를 활용해 rootReducer를 연결해 주고 있습니다.
Reducer
Reducer는 현재의 state와 Action을 이용해서 새로운 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;
}
}
보통 위와 같은 모양으로 구성됩니다. 위의 예시에서는 switch문을 통해서 코드를 작성했지만 if문으로 작성해도 무방합니다.
Reducer의 Immutability(불변성)
Reducer 함수를 작성할 때 주의해야 할 점이 있습니다. 바로 Redux의 state 업데이트는 immutable한 방식으로 변경해야 한다는 것인데요. Redux의 장점 중 하나인 변경된 state를 로그로 남기기 위해서 꼭 필요한 작업입니다.
그렇다면 immutable한 방식으로 state를 변경하기 위해서는 어떻게 코드를 작성해야 할까요? 위의 itemReducer 예제 코드에서 Object.assign을 통해 새로운 객체를 만들어 리턴하는 것을 통해 힌트를 얻을 수 있습니다.
이제 Redux Hooks로 각각의 개념들을 연결시켜 줍시다.
useSelector()
먼저 useSelector()는 컴포넌트와 state를 연결하는 역할을 합니다. 컴포넌트에서 useSelector 메서드를 통해 Store의 state에 접근할 수 있는 것이죠.
어떤 컴포넌트에서 useSelector를 이용해 state에 접근하고 있는지 Cmarket Redux 과제 코드에서 확인해 보세요! useSelector의 전달인자로는 콜백 함수를 받으며 콜백 함수의 전달인자로는 state 값이 들어갑니다.
useDispatch()
useDispatch()는 Action 객체를 Reducer로 전달해 주는 메서드입니다. Action이 일어날만한 곳은 클릭 등의 이벤트가 일어나는 컴포넌트겠죠.
어떤 컴포넌트에서 useDispatch를 이용해 Action을 Reducer로 전달해 줄 수 있을지 고민해 보세요.
actions / index.js
// 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) => {
//장바구니에 더할 때는 itemId를 인자로 받아 type으로 ADD_TO_CART 로 선언해주고 payload로는 추가되는 itemId와 quantity를 객체
//형식으로 작성해준다. 여기서 왜 type을 변수로 따로 선언해주었냐면 자동완성기능을 사용하기 위함과 재사용시 오타를 줄이기 위해서이다.
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
}
export const removeFromCart = (itemId) => {
//장바구니에서 제거할 때는 itemId를 인자로 받고 type은 REMOVE_FROM_CART ,payload로는 itemId만 작성한다 . 장바구니에
// 삭제하는 것은 quantity를 따로 필요로 하지 않기 때문이다.
return {
type: REMOVE_FROM_CART,
payload: {
itemId
}
}
}
export const setQuantity = (itemId, quantity) => {
//장바구니에 있는 item의 수량을 바꿔주는 액션이다. 위의 액션과 동일하게 작성하였고 quantity를 다루기 때문에 payload에 작성해주었다.
return {
type: SET_QUANTITY,
payload : {
itemId ,
quantity
}
}
}
export const notify = (message, dismissTime = 5000) => dispatch => {
const uuid = Math.random()
dispatch(enqueueNotification(message, dismissTime, uuid))
setTimeout(() => {
dispatch(dequeueNotification())
}, dismissTime)
}
export const enqueueNotification = (message, dismissTime, uuid) => {
return {
type: ENQUEUE_NOTIFICATION,
payload: {
message,
dismissTime,
uuid
}
}
}
export const dequeueNotification = () => {
return {
type: DEQUEUE_NOTIFICATION
}
}
itemReducer.js
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";
const itemReducer = (state = initialState, action) => {
//리듀서는 초기 상태를 첫번째인자로 두번째 인자로는 action을 받는다.
//switch문을 통해 action.type에 따라 어떻게 상태를 변경할지 결정한다.
switch (action.type) {
case ADD_TO_CART:
//기존 state에 새롭게 할당한 action.payload 를 추가해준다.
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
case REMOVE_FROM_CART:
//filter 메소드를 활용하여 삭제할 itemId가 아닌것들을 배열로 받고 그것을 다시 assign 해 객체에 추가해준다.
let cartItems = state.cartItems.filter(el => el.itemId !== action.payload.itemId)
return Object.assign({},state,{
cartItems: [...cartItems]
})
case SET_QUANTITY:
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
// 수량을 변경할 item이 몇번째 인덱스에 위치해있는지 구한 후 slice를 통해 그 위치에 있는 장바구니 수량을 변경해준다
return {
...state,
cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)]
}
default:
return state;
}
}
export default itemReducer;
itemListContainer.js
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);
const { items, cartItems } = state;
const dispatch = useDispatch(); //redux-hooks인 useDispatch()를 사용하기 위해 선언
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
//TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달하세요.
dispatch(addToCart(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;
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); //컴포넌트와 state를 연결하기 위함 Store의 state에 접근가능
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) => {
//TODO: dispatch 함수를 호출하여 액션을 전달하세요.
dispatch(setQuantity(itemId,quantity)) // 수량 변경 디스패치를 실행
}
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId))
//TODO: dispatch 함수를 호출하여 액션을 전달하세요.
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>
)
}
이렇게 redux를 활용한 cmarket 과제를 풀어보았다. 사실 처음부터 redux를 사용했으면 정말 헤맸을 거 같은데 대부분 코드가 짜여져있고 간단한 부분만 작성하면 되는 것이라 어렵진 않았다. 하지만 이걸 내가 스스로 혼자 처음부터 할 수 있을까 ? 하면 .. 잘 모르겠다 사실 코드도 여러번 작성해봐야 눈에 익고 머리에 익듯 여러번 작성해보는 연습을 해봐야겠다.
'코드스테이츠44기 프론트엔드' 카테고리의 다른 글
Section 4 기술 면접 준비 (0) | 2023.06.08 |
---|---|
솔로프로젝트 - Coz Shopping 프로젝트 계획 및 요구사항 / 헤더 푸터 구현 (0) | 2023.05.29 |
[리액트] Cmarket (Hooks 버전 ) 구현하기 (0) | 2023.04.22 |
섹션2를 마무리하며 KTP회고 (0) | 2023.04.10 |
Section 2 기술면접 준비 (0) | 2023.04.10 |