Checkpoint 과제 진행하기
학습 목표
- React Router를 통해 라우팅을 할 수 있다.
- useState로 상태를 사용할 수 있다.
Bare minimum requirements
- react-router-dom을 이용해 Client Side Routing하는 방법을 학습합니다.
- useState를 이용해 상태를 사용하는 방법을 학습합니다.
- 쇼핑몰 애플리케이션의 주요 기능을 구현하세요.
- [장바구니 담기] 버튼을 이용해 장바구니에 해당 상품이 추가되도록 구현하세요.
- 장바구니 내 [삭제] 버튼을 이용해 장바구니의 상품이 제거되도록 구현하세요.
- 장바구니 내에서 각 아이템 개수를 변경할 수 있도록 구현하세요.
- 장바구니의 상품 개수의 변동이 생길 때마다, 상단 내비게이션 바에 상품 개수가 업데이트되도록 구현하세요.
시작하기 전에...
애플리케이션에서 사용하는 주요 상태는 다음과 같습니다.
시작하기 전에 반드시 컴포넌트 구조와, 데이터 흐름을 먼저 그림으로 그려보세요. 일을 보다 단순하게 만들 수 있습니다.
- 상품 목록 (items): 개별 상품이 배열의 형태로 담겨 있습니다.
{ "id": 1, "name": "노른자 분리기", "img": "../images/egg.png", "price": 9900 }
- 장바구니 목록(cartItems): 장바구니에는 상품 아이디와, 수량을 담은 객체가 배열의 형태로 담겨 있습니다.
{ "itemId": 1, "quantity": 1 }
npm 다운로드
1. npm install
2. npm run start
브라우저
상품 리스트 (처음 화면)
장바구니(클릭 시)
▶️ 시작하기
거의 완성되어 있는 컴포먼트 중에서 제일 최상위 컴포먼트 App.js 페이지에서 자식 컴포먼트들에게 상태를 내려주고 전달 받은 컴포먼트에서 과제의 각 요구사항에 맞게 코드를 작성 및 리팩터링하면 되는 과제였다.
최상위 컴포먼트 App.js 구현 완성 코드
import React, { useState } from 'react';
import Nav from './components/Nav';
import ItemListContainer from './pages/ItemListContainer';
import './App.css';
import './variables.css';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import ShoppingCart from './pages/ShoppingCart';
import { initialState } from './assets/state';
function App() {
const [items, setItems] = useState(initialState.items);
const [cartItems, setCartItems] = useState(initialState.cartItems);
return (
<Router>
{/* Nav 컴포넌트에 장바구니 기존 아이템 개수 보내기*/}
<Nav cartItemsLen={cartItems.length} />
<Routes>
{/* ItemListContainer 컴포넌트에 아이템, 장바구니 기존아이템, 장바구니 상태 갱신 보내기*/}
<Route
path='/'
element={
<ItemListContainer
items={items}
cartItems={cartItems}
setCartItems={setCartItems}
/>
}
/>
{/* ShoppingCart 컴포넌트에 아이템, 장바구니 기존아이템, 장바구니 상태 갱신 보내기*/}
<Route
path='/shoppingcart'
element={
<ShoppingCart
items={items}
cartItems={cartItems}
setCartItems={setCartItems}
/>
}
/>
</Routes>
<img
id='logo_foot'
src={`${process.env.PUBLIC_URL}/codestates-logo.png`}
alt='logo_foot'
/>
</Router>
);
}
export default App;
✅ TODO: 장바구니에 추가
살펴보기
ItemListContainer 컴포넌트
1. ItemListContainer 컴포넌트 안에서 handleClick 함수를 props로 Item 컴포넌트로 보내고 있는 것을 알 수 있음
{items.map((item, idx) => (
<Item item={item} key={idx} handleClick={handleClick} />
))}
Item 컴포넌트
2. 장바구니 담기 버튼을 클릭했을 때 handleClick 함수를 실행하면서 클릭된 아이템의 정보를 보내는 것을 알 수 있음
<button className="item-button" onClick={(e) => handleClick(item.id)}>장바구니 담기</button>
문제 해결
1. ItemListContainer 컴포넌트에 props로items, cartItems , setCartItems상태 보내기
{/* ItemListContainer 컴포넌트에 아이템, 장바구니 기존아이템, 장바구니 상태 갱신 보내기*/}
<Route path='/' element={<ItemListContainer items={items} cartItems={cartItems} setCartItems={setCartItems}/>}/>
2. ItemListContainer 컴포넌트에서 props로 받은 상태 받기
function ItemListContainer({ items, cartItems, setCartItems }) { // App에서 받은 상태 받기
}
3. handleClick 함수 구현하기
- 장바구니 담기를 클릭했을 때 함수로 들어온 정보를 가지고 새로운 객체를 만든다.
- 장바구니 객체 cartItems 은 아래 형식과 같음. 따라서 키 name을 똑같이 만든다.
{
"itemId": 1,
"quantity": 1
}
2. 기존 장바구니에 있으면 개수만 올리고,추가하지 않는다. / 기존 장바구니에 없으면 위에서 만든 새로운 객체를 장바구니 상태에 갱신한다.
function ItemListContainer({ items, cartItems, setCartItems }) {
const handleClick = (obj, id) => {
// 현재 장바구니 담기 클릭한 아이템 객체 만들기
const newCartItem = { itemId: id, quantity: 1 };
// 기존 장바구니에서 들어온 아이템 id 있는지 확인 후 인덱스 반환
const findIdx = cartItems.findIndex((e) => e.itemId === id);
// 장바구니에 이미 있으면 개수 하나씩 올리고 상태 갱신
if (findIdx !== -1) {
cartItems[findIdx].quantity++;
setCartItems(cartItems);
} else {
// 장바구니에 같은 물건이 없으면 새로운 아이템,기존 아이템 같이 상태 갱신
setCartItems([...cartItems, newCartItem]);
}
};
✅ TODO: 내비게이션 바에 상품 개수 표시
1. App 컴포넌트 -> Nav 컴포넌트로 장바구니 개수 props로 보내기
{/* Nav 컴포넌트에 장바구니 기존 아이템 개수 보내기*/}
<Nav cartItemsLen={cartItems.length} />
2. Nav 컴포넌트에서 보낸 props 받기, 받은 장바구니 개수 표시 하기
import React from 'react';
import { Link } from 'react-router-dom';
function Nav({ cartItemsLen }) {
// App컴포넌트에서 장바구니 개수 받기
return (
<div id='nav-body'>
<span id='title'>
<img id='logo' src='../logo.png' alt='logo' />
<span id='name'>CMarket</span>
</span>
<div id='menu'>
<Link to='/'>상품리스트</Link>
<Link to='/shoppingcart'>
{/* 받은 장바구니 개수 표시하기 */}
장바구니<span id='nav-item-counter'>{cartItemsLen}</span>
</Link>
</div>
</div>
);
}
export default Nav;
✅ TODO: 장바구니에서 제거
살펴보기
1. 처음에 ShoppingCart 컴포넌트에 가보면 장바구니에서 하는 거의 모든 일들을 처리하고 있음을 알 수 있음
2. Cartltem 컴포넌트에서 ShoppingCart 컴포넌트에서 사용중인 handleDelete 함수를 삭제 버튼을 눌렀을 때 실행되게 하는 것을 알 수 있음
<button className="cart-item-delete" onClick={() => { handleDelete(item.id) }}>삭제</button>
문제 해결
1. App 컴포넌트에서 ShoppingCart 컴포넌트에 props로items, cartItems , setCartItems상태 보내기
{/* ShoppingCart 컴포넌트에 아이템, 장바구니 기존아이템, 장바구니 상태 갱신 보내기*/}
<Route path='/shoppingcart' element={<ShoppingCart items={items} cartItems={cartItems} setCartItems={setCartItems}/>}/>
2. ShoppingCart 컴포넌트에서 props로 받은 상태 받기
export default function ShoppingCart({ items, cartItems, setCartItems }) { // App에서 받은 상태 받기
}
3. handleDelete 함수 -> setCartItems(cartItems.filter((el) => el.itemId !== itemId)) 로직 추가
- 현재 장바구니 안의 id 값과 클릭된 id 값을 비교해서 같은 값만 삭제되도록 해야됨
const handleDelete = (itemId) => {
// 장바구니의 아이템 선택(체크) 상태가 되어있는지 안되어있는지 같이 확인하는 코드
setCheckedItems(checkedItems.filter((el) => el !== itemId));
// 현재 장바구니 안의 id 값과 클릭된 id 값을 비교해서 같은 값만 삭제
setCartItems(cartItems.filter((el) => el.itemId !== itemId));
};
✅ TODO: 장바구니 수량 변경
1. ShoppingCart 컴포넌트 -> handleQuantityChange함수 -> 로직 추가
const handleQuantityChange = (quantity, itemId) => {
// 삭제할 아이템 id랑 기존 아이템 id 같은지 확인해서 인덱스 반환
let findIdx = cartItems.findIndex((item) => item.itemId === itemId);
// 기존 아이템에 찾은 인덱스에 있는 개수를 받아온 개수로 변경처리
cartItems[findIdx].quantity = quantity;
// 상태 갱신
setCartItems([...cartItems]);
};
▶️ 파일 코드
ItemListContainer 컴포넌트
import React from 'react';
import Item from '../components/Item';
function ItemListContainer({ items, cartItems, setCartItems }) {
const handleClick = (id) => {
// 현재 장바구니 담기 클릭한 아이템 객체 만들기
const newCartItem = { itemId: id, quantity: 1 };
// 기존 장바구니에서 들어온 아이템 id 있는지 확인 후 인덱스 반환
const findIdx = cartItems.findIndex((e) => e.itemId === id);
// 장바구니에 이미 있으면 개수 하나씩 올리고 상태 갱신
if (findIdx !== -1) {
cartItems[findIdx].quantity++;
setCartItems(cartItems);
} else {
// 장바구니에 같은 물건이 없으면 새로운 아이템,기존 아이템 같이 상태 갱신
setCartItems([...cartItems, newCartItem]);
}
};
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} />
))}
</div>
</div>
);
}
export default ItemListContainer;
Item 컴포넌트
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'>{item.name}</span>
<span className='item-price'>{item.price}</span>
<button className='item-button' onClick={(e) => handleClick(item.id)}>
장바구니 담기
</button>
</div>
);
}
ShoppingCart 컴포넌트
import React, { useState } from 'react';
import CartItem from '../components/CartItem';
import OrderSummary from '../components/OrderSummary';
export default function ShoppingCart({ items, cartItems, setCartItems }) {
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랑 기존 아이템 id 같은지 확인해서 인덱스 반환
let findIdx = cartItems.findIndex((item) => item.itemId === itemId);
// 기존 아이템에 찾은 인덱스에 있는 개수를 받아온 개수로 변경처리
cartItems[findIdx].quantity = quantity;
// 상태 갱신
setCartItems([...cartItems]);
};
const handleDelete = (itemId) => {
setCheckedItems(checkedItems.filter((el) => el !== itemId));
// 현재 장바구니 안의 id 값과 클릭된 id 값을 비교해서 같은 값만 삭제
setCartItems(cartItems.filter((el) => el.itemId !== 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>
);
}
▶️ 마치며
- useState를 이용해 상태를 사용하는 방법을 다시 복습한다는 생각으로 과제를 풀었던 것 같다
- 시작하기전 컴포넌트 구조를 파악하는 개념과 데이터 흐름을 파악하는 부분이 어느 정도는 어떤식으로 되어있구나 ~ 하기는 하는데 아직은 완전히 정확하게 파악되진 않는다. 누구나 마찬가지겠지?
- useState 배우는데 재미가 붙었다 ! 어서 빨리 Redux 를 배웠으면 좋겠다. 전역 상태 저장소를 사용하면 위 코드들처럼 상태를 계속 끌어 올려 받고 하는 짓을 안해도 된다는게 넘 설렌다;
'Boot Camp > code states' 카테고리의 다른 글
[ Code States ] Section 3 / KDT 회고 (0) | 2023.05.09 |
---|---|
[ Code States ] Figma 클론 과제 진행하기 (0) | 2023.04.17 |
[ Code States ] Figma 컴포넌트 구현 과제 진행하기 (0) | 2023.04.16 |
[ Code States ] Section 2 / KDT 회고 (0) | 2023.04.10 |
[ Code States ] Section 2 / 기술 면접 준비 (0) | 2023.04.10 |