🛍️ 상품 상세 페이지
로딩 상태 처리
상품 상세 페이지에서 데이터를 불러오는 동안 로딩 스피너를 보여주고, 이후 내용을 표시
✅ useState로 로딩 상태 정의
const [isLoading, setIsLoading] = useState(true)
✅ useEffect로 데이터 준비 확인 후 상태 업데이트
useEffect(() => {
// 컴포넌트가 마운트된 직후에는 로딩 상태로 표시
setIsLoading(true)
// 데이터가 로드된 후 로딩 상태 해제
if (product && product.id) {
// 약간의 지연 효과를 줘서 로딩 화면을 확인할 수 있도록
const timer = setTimeout(() => {
setIsLoading(false)
}, 1000)
return () => clearTimeout(timer)
}
}, [product])
✅ Bootstrap 로딩 스피너 활용
if (isLoading) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ height: '50vh' }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
)
}
➡️ 임시로 로딩 상태를 표시했지만, 로딩 상태는 전역에서 관리하는 게 일반적
탭
상품 상세 페이지에 들어가는 탭 구현
✅ 탭 상태 관리
버튼을 클릭하면 activeTab을 해당 인덱스로 변경
const [activeTab, setActiveTab] = useState(0)
const tabTitles = ['Description', 'Additional Information', 'Reviews']
return (
<>
<div className={css.tabBtn}>
{tabTitles.map((title, idx) => (
<button
key={idx}
className={activeTab === idx ? css.active : ''}
onClick={() => setActiveTab(idx)}
>
{title}
</button>
))}
</div>
</>
)
✅ 탭이 선택됐을 때 보여주는 콘텐츠 처리
activeTab에 따른 조건부 렌더링
{activeTab === 0 && (
<div className={`${css.tabContent} ${activeTab === 0 ? css.visible : ''}`}>
</div>
)}
{activeTab === 1 && (
<div className={`${css.tabContent} ${activeTab === 1 ? css.visible : ''}`}>
</div>
)}
{activeTab === 2 && (
<div className={`${css.tabContent} ${activeTab === 2 ? css.visible : ''}`}>
</div>
)}
상품 슬라이더 (feat. Swiper)
같은 카테고리에 속한 상품들을 슬라이더 형식으로 보여주기
✅ breakpoints로 화면 크기에 따른 슬라이드 개수 조절
const SimilarProducts = ({ products }) => {
return (
<div className={css.similarCon}>
<Swiper
slidesPerView={1}
spaceBetween={10}
pagination={{ clickable: true }}
modules={[Pagination]}
className={css.similarSlider}
breakpoints={{
640: {
slidesPerView: 2,
spaceBetween: 20,
},
768: {
slidesPerView: 4,
spaceBetween: 40,
},
1024: {
slidesPerView: 5,
spaceBetween: 50,
},
}}
>
{products.map(item => (
<SwiperSlide key={item.id}>
<ProductCard item={item} />
</SwiperSlide>
))}
</Swiper>
</div>
)
}
export default SimilarProducts
✅ 페이지네이션 위치 조정
swiper-pagination이 두 번째 div로 들어오는 걸 활용해서 위치 조정
.similarSlider > div:nth-of-type(2) {
bottom: -8px;
}
.similarCon {
overflow: hidden;
padding-bottom: 50px;
}
.similarSlider {
overflow: visible;
}
🛒 상품 장바구니
모달
장바구니 담기 버튼 클릭 시 모달로 띄어주기
✅ 모달 구조
브라우저 전체를 덮는 영역을 만들어서 사용자가 다른 UI와 상호작용하지 못하게 하고, 모달에 집중하도록 함
<div className={`${css.modal} ${isActive ? css.active : ''}`}>
<div className={css.modalCon}>
<div className={css.inner}>
<h2>장바구니</h2>
<div className={css.imgWrap}>
<img src={`/public/img/${product.img}`} alt={product.title} />
</div>
<div className={css.info}>
<p>{product.title}</p>
<p>{formmatCurrency(product.price)}</p>
{product.discount > 0 && <p>{product.discount}%</p>}
<p>{count}</p>
<p>총 가격: {formmatCurrency(product.price * count)}</p>
</div>
<button onClick={handleClose}>취소</button>
<button onClick={handleAddToCart}>장바구니 담기</button>
</div>
<button className={css.btnClose} onClick={handleClose}>
<i className="bi bi-x-lg"></i>
</button>
</div>
</div>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2); /* 어두운 배경 */
backdrop-filter: blur(5px); /* 살짝 흐림 효과 */
z-index: 99999;
display: flex;
justify-content: center;
align-items: center;
}
.modalCon {
position: relative;
opacity: 0;
background-color: var(--dark-colors-white-dark);
transform: translateY(-50px);
transition:
transform 0.3s,
opacity 0.1s;
padding: var(--fs33);
border-radius: var(--fs16);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
.modal.active .modalCon {
opacity: 1;
transform: translateY(0px);
}
✅ 모달 조건부 렌더링
부모 컴포넌트에서 호출
{isModalOpen && <Modal product={product} count={count} onClose={closeModal} />}
✅ 트랜지션 적용을 위한 isActive 제어
- 모달 오픈 시, 트랜지션이 자연스럽게 보이게끔 살짝 지연을 줌
- 동시에 스크롤 잠금 처리
const [isActive, setIsActive] = useState(false)
useEffect(() => {
const timer = setTimeout(() => {
setIsActive(true)
document.body.style.overflow = 'hidden'
}, 5)
return () => {
clearTimeout(timer)
document.body.style.overflow = 'auto'
}
}, [])
✅ 모달 닫을 때 트랜지션 시간 고려
const handleClose = () => {
setIsActive(false)
setTimeout(onClose, 300)
}
장바구니 조회
✅ 장바구니 데이터를 서버에서 가져오기
export const getCartData = async () => {
try {
const res = await axios.get(`/api/cart/`)
return res.data
} catch (err) {
console.log('[error]', err)
}
}
✅ React Router에서 loader 사용
장바구니 페이지를 렌더링하기 전에 데이터를 로드
{ path: '/cart', element: <CartPage />, loader: cartLoader },
✅ useLoaderData로 데이터 받기
import React from 'react'
import { useLoaderData } from 'react-router-dom'
const CartPage = () => {
const cartItems = useLoaderData()
console.log(cartItems)
return (
<main>
<h2>CartPage</h2>
</main>
)
}
export default CartPage
장바구니 담기
✅ 모달 내에서 상품을 장바구니에 추가 + 장바구니 페이지로 네비게이션
const handleAddToCart = async () => {
// 장바구니에 상품을 추가(json-server에 추가)
try {
const cartItem = {
id: product.id,
title: product.title,
img: product.img,
price: product.price,
category: product.category,
discount: product.discount,
count: count,
}
await addToCart(cartItem)
handleClose()
} catch (err) {
console.log('[error]', err)
}
// 장바구니 페이지로 이동
navigate('/cart')
}
✅ 서버에 장바구니 상품 추가 + 업데이트
export const addToCart = async cartItem => {
console.log(cartItem)
try {
// 기존 장바구니 리스트 조회
const cart = await getCartData()
// 이미 저장된 리스트가 있는지 확인
const existingItem = cart.find(item => item.id == cartItem.id)
// 리스트가 존재하면 count만 증가. put 요청
if (existingItem) {
const updateItem = {
...existingItem,
count: existingItem.count + cartItem.count,
}
const res = await axios.put(`/api/cart/${existingItem.id}`, updateItem)
return res.data
}
// 리스트가 없으면 전체 데이터 추가. post 요청
const res = await axios.post('/api/cart/', cartItem)
return res.data
} catch (err) {
console.log('[error]', err)
}
}
728x90
반응형
'LG 유플러스 유레카 SW > React' 카테고리의 다른 글
[#59] React 쇼핑몰 - Shop 페이지 상품 필터링/정렬 + 페이지네이션 (0) | 2025.04.22 |
---|---|
[#58] React 쇼핑몰 - 장바구니 수정/삭제 + 반응형 Shop 페이지 (0) | 2025.04.21 |
[#56] React 쇼핑몰 - 상품 상세 페이지 (1) | 2025.04.17 |
[#55] React 쇼핑몰 - 스켈레톤 UI + 반응형 메인 리스트 페이지 (0) | 2025.04.16 |
[#54] React 쇼핑몰 - 성능 향상 + Hero 페이지 제작 (2) | 2025.04.15 |