📃CartPage 코드
import React, { useState } from 'react'
import { useLoaderData } from 'react-router-dom'
import css from './CartPage.module.css'
import { formmatCurrency } from '@/utils/features'
import { updateCartItemCount, removeFromCart } from '@/api/cartApi'
const CartPage = () => {
const cartList = useLoaderData()
const [items, setItems] = useState(Array.isArray(cartList) ? cartList : [])
console.log('cartList', items)
// 장바구니 총 수량 계산 reduce 고차함수
const totalCount = items.reduce((sum, item) => sum + item.count, 0)
// 총 계산할 금액
const totalSum = items.reduce(
(sum, item) => sum + Math.round(item.price * item.count * (1 - item.discount / 100)),
0
)
const increase = id => {
// 아이템이 없으면 일찍 함수 종료
const cuurentItem = items.find(item => item.id === id)
if (!cuurentItem) return
setItems(prev => prev.map(item => (item.id === id ? { ...item, count: item.count + 1 } : item)))
const newCount = items.find(item => item.id === id).count + 1
// cartApi에 업데이트 요청
updateCartItemCount(id, newCount).catch(err => console.log('err', err)) // 에러처리 추가;
}
const decrease = id => {
const cuurentItem = items.find(item => item.id === id)
if (!cuurentItem) return
setItems(prev =>
prev.map(item =>
item.id === id && item.count > 1 ? { ...item, count: item.count - 1 } : item
)
)
const newCount = items.find(item => item.id === id).count - 1
if (newCount >= 1) {
updateCartItemCount(id, newCount).catch(err => console.log('err', err))
}
}
const hadleDelete = id => {
if (window.confirm('정말 삭제하시겠습니까?')) {
setItems(prev => prev.filter(item => item.id !== id))
removeFromCart(id).catch(err => console.log('err', err))
}
}
return (
<main>
<h2>Shopping cart</h2>
{items.length > 0 && (
<p>
장바구니 리스트는 <strong>{items.length}</strong> 개이고, 총 상품 갯수는{' '}
<strong>{totalCount}</strong>개 입니다.
</p>
)}
{items.length === 0 ? (
<p className={css.empty}>장바구니 비었음 텅~</p>
) : (
<>
<ul className={css.cartList}>
{items.map(item => (
<li className={css.cartItem} key={item.id}>
<div className={css.cartImg}>
<img src={`/public/img/${item.img}`} alt={item.title} />
</div>
<div className={css.title}>{item.title}</div>
<div className={css.price}>{formmatCurrency(item.price)}</div>
<div className={css.btnArea}>
<button
onClick={() => {
decrease(item.id)
}}
>
-
</button>
<span>{item.count}</span>
<button
onClick={() => {
increase(item.id)
}}
>
+
</button>
</div>
<div className={css.sum}>{formmatCurrency(item.price * item.count)}</div>
<div
className={css.deleteBtn}
onClick={() => {
hadleDelete(item.id)
}}
>
<i className="bi bi-trash3"></i>
</div>
</li>
))}
</ul>
<div className={css.totalPrice}>
총금액 : <strong>{formmatCurrency(totalSum)}</strong>
</div>
</>
)}
</main>
)
}
export default CartPage
🦕 전체 구조 먼저 정리
useLoaderData() → 장바구니 데이터 가져오기
useState() → 상태 저장 (items)
map() → 장바구니 아이템 반복 렌더링
onClick → 수량 증감 및 삭제
reduce() → 총합 계산
useState() – 상태 관리의 핵심
const [items, setItems] = useState(Array.isArray(cartList) ? cartList : [])
- React 컴포넌트는 상태(State) 가 바뀌면 자동으로 화면을 다시 그림.
- useState로 만든 items는 장바구니 목록의 현재 상태.
- setItems()로 값을 바꾸면 → 화면 자동 업데이트됨.
📌 상태란 = 사용자 인터랙션에 따라 바뀌는 값 (예: 수량, 삭제 등)
useLoaderData() – 라우팅에서 데이터 받아오기
const cartList = useLoaderData()
- React Router에서 loader()로 받아온 데이터를 이 Hook으로 가져옴
- 서버에서 미리 받아온 데이터를 컴포넌트에 넣어주는 방식
reduce() – 총 수량과 총 금액 계산
const totalCount = items.reduce((sum, item) => sum + item.count, 0)
const totalSum = items.reduce((sum, item) => sum + item.price * item.count, 0)
reduce()는 배열의 값을 누적 계산할 때 사용하는 함수
map() – 리스트 뿌리기
items.map(item => (
<li key={item.id}>...</li>
))
- React는 반복문 대신 map()을 써서 리스트를 렌더링함
- key={item.id}는 꼭! 있어야 함
→ 리액트가 DOM을 효율적으로 바꿀 수 있게 돕는 고유 식별자
onClick + 상태 변경 – 인터랙션 처리
<button onClick={() => increase(item.id)}>+</button>
- 버튼 클릭 시 → 함수 호출 → 상태 변경 → 자동으로 화면 다시 그림
- increase()에서 수량을 바꾸면 → setItems() → 리렌더링
API 호출 – 서버와 동기화
updateCartItemCount(id, newCount)
- 사용자가 변경한 데이터를 서버에 반영
- 프론트와 서버의 데이터가 불일치하지 않도록 유지
삭제 기능 (filter())
setItems(prev => prev.filter(item => item.id !== id))
- filter()는 조건을 만족하는 것만 남김 → 즉, 삭제 기능 구현 가능
- 삭제된 후에 서버에도 API 요청
🧠 이해하자
1. 데이터를 받아온다 (useLoaderData)
2. useState로 상태를 만든다
3. map으로 리스트를 렌더링한다
4. onClick으로 이벤트를 연결한다
5. setState로 상태를 바꾸면 → 자동으로 리렌더링된다!
📃 ShopPage 컴포넌트 분석
🦕 기능 요약
ShopPage는 상품 리스트를 보여주는 쇼핑 페이지로, 다음과 같은 기능을 포함합니다:
- 카테고리 필터: 전체상품 / 신상품 / 인기상품 탭 버튼
- 정렬 옵션: 등록순 / 가격순 / 할인순 등의 정렬 조건 선택
- 상품 목록: 상품 리스트를 그리드 형태로 출력
- 페이지네이션: 페이지 번호 및 좌우 이동
import React, { useState } from 'react'
import css from './ShopPage.module.css'
const ShopPage = () => {
const [isDown, setIsDown] = useState(false)
return (
<main className={css.shopPage}>
<h2>Shop All</h2>
<div className={css.searchFn}>
<div className={css.category}>
<button className={css.active}>전체상품</button>
<button>신상품(new)</button>
<button>인기상품(top)</button>
</div>
<div className={`${css.sort} ${isDown ? css.active : ''}`}>
<div className={css.sortHeader} onClick={() => setIsDown(!isDown)}>
<p>등록순</p>
<i className={`bi bi-chevron-${isDown ? 'up' : 'down'}`}></i>
</div>
<ul>
<li>등록순</li>
<li className={css.active}>낮은 가격순</li>
<li>높은 가격순</li>
<li>낮은 할인순</li>
<li>높은 할인순</li>
</ul>
</div>
</div>
<div className={css.productList}>
<ul className={css.list}>
<li>상품리스트</li>
<li>상품리스트</li>
<li>상품리스트</li>
<li>상품리스트</li>
<li>상품리스트</li>
<li>상품리스트</li>
<li>상품리스트</li>
</ul>
<div className={css.paginationArea}>
<button>
<i className="bi bi-chevron-left"></i>
</button>
<button>1</button>
<button>2</button>
<button className={css.active}>3</button>
<button>4</button>
<button>5</button>
<button>
<i className="bi bi-chevron-right"></i>
</button>
</div>
</div>
</main>
)
}
export default ShopPage
useState로 드롭다운 상태 제어
const [isDown, setIsDown] = useState(false)
무엇? 컴포넌트 안에서 변경 가능한 값을 저장할 수 있는 상태를 만들때 사용
왜? 정렬 UI를 열고 닫는 토글 상태를 관리하기 위해 사용
기억할 것? useState(초기값)을 호출하면 [현재값, 변경함수]를반환한다.
드롭다운 메뉴의 열림/닫힘 상태를 isDown으로 제어
- useState(false)는 isDown의 초기값 false로 설정
- setIsDown은 상태를 변경할 수 있는 함수
Boolean(true/false) 상태로 토글할 때 많이 사용
정렬 옵션 드롭다운 UI
<div className={`${css.sort} ${isDown ? css.active : ''}`}>
<div className={css.sortHeader} onClick={() => setIsDown(!isDown)}>
<p>등록순</p>
<i className={`bi bi-chevron-${isDown ? 'up' : 'down'}`}></i>
</div>
<ul>...</ul>
</div>
템플릿 리터럴 + 삼항 연산자
isDown이 true이면 css.active 클래스 추가
그렇지 않으면 빈문자열
<i className={`bi bi-chevron-${isDown ? 'up' : 'down'}`}></i>
동적 클래스명 설정했다. 드롭다운 열림 여부에 따라 아이콘 방향이 변경된다.
- 열릴때는 bi-chevron-up
- 닫힐때는 bi-chervron-down
👉🏻 React에서 많이 쓰이는 패턴이다.
📃 ShopPage 스타일 설명
flexbox 🐸
.shopPage {
/* 자식 요소를 가로 정렬 */
display: flex;
/* 공간이 부족할때 자동 줄바꿈 */
flex-wrap: wrap;
gap: var(--fs33);
}
- display: flex
→ 자식 요소들을 가로로 나열함 - flex-wrap: wrap
→ 화면이 좁아지면 자동 줄바꿈 (반응형 레이아웃에 필수) - gap
→ 요소 간 간격 지정 (margin 대신 사용하는 걸 권장)
📌 flex + gap 조합이 기본 중 기본이다. 특히 flex-wrap을 써야 아이템이 반응형으로 자연스럽게 아래로 내려감.
width + flex-grow 조합 (고정 너비 vs 유동 너비)
.sort ul {
max-height: 0;
overflow: hidden;
transition: 0.3s;
}
.sort.active ul {
max-height: 190px;
}
- max-height를 0으로 했다가 → 특정 높이로 트랜지션
- overflow: hidden으로 내용 숨김
- transition으로 부드럽게
📌 드롭다운, 아코디언 UI, 펼쳐지는 메뉴는 다 이 패턴 쓴다.
max-height + transition은 JS 없이도 부드러운 애니메이션 구현이 가능하다는 점에서 굉장히 유용하다.
position: absolute + transform – 위치 정렬 공식
.sortHeader i {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: var(--fs12);
}
- 아이콘을 부모 기준 우측 정렬
- top: 50% + translateY(-50%)는 세로 가운데 정렬 공식
- right: 12px → 오른쪽 정렬 여백
📌 버튼 내부, 카드 내부, 검색창 내부에 아이콘 넣을 때 100% 활용한다.
Grid Layout – 카드형 상품 배치
.list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
- auto-fill + minmax()로 반응형 카드 구현
- 1fr은 너비 비율을 채우는 단위
📌 Grid는 Flex보다 나중에 나왔지만, 카드 배치나 격자형 UI엔 압도적으로 유리
상품 카드, 포트폴리오, 블로그 리스트에 필수
최근 들어 체력이 정말 바닥이다.
조금만 움직여도 금방 피곤해지고, 푹 자도 피로가 풀리지 않는다.
그래서 저녁마다 러닝이라도 시작해보려고 한다.
오늘 수업도 정신없이 지나갔고, 매시간마다 집중하기가 쉽지 않았다.
그래도 이렇게라도 글로 정리하면서 하루를 마무리해보려 한다.
그나저나 어제 청년부에서 오후 예배 시작 전에 잠깐 다 같이 외출했는데,
진짜 10분 만에 찍고 돌아온 번개 모임이었다.
뭐 특별한 걸 한 건 아니지만 사진도 찍고, 음료 내기하고, 다시 교회로 복귀했다.
그런데 오늘 단톡방에 올라온 사진을 보고 충격...
살이 너무 쪄 보여서 스스로 놀랐다. (살찌긴했다.)
오늘부터 진짜 러닝 시작한다. 🔥 ( 이러고 저녁먹고 안할듯)
작심삼일을 딱 121번만 해야겠다.
slow and steady wins the race..
학습 피드를 올리려던 찰나

베라에 당첨되었다. 잠시 살빼는건 스톱하고 러닝만 하려고한다. 😽
'💡 URECA > 🗒️ 스터디 노트' 카테고리의 다른 글
[URECA] Day59 React 총정리 🐳 (개념 배울때마다 여기에 추가하기) (0) | 2025.04.23 |
---|---|
[URECA] Day58 React PROJ Shop URL (0) | 2025.04.22 |
[URECA] Day56 React PROJ SHOP (0) | 2025.04.18 |
[URECA] Day55 React Shop PROJ 4편 디바운스 & 쓰로틀 정복기, 프록시 서버 설정 (0) | 2025.04.17 |
[URECA] Day54 리액트 lazy, suspense, axios (1) | 2025.04.15 |