[#57] React 쇼핑몰 - 상품 상세 페이지(탭, 슬라이더) + 장바구니 조회/추가

2025. 4. 18. 16:12·LG 유플러스 유레카 SW/React

🛍️ 상품 상세 페이지

로딩 상태 처리

상품 상세 페이지에서 데이터를 불러오는 동안 로딩 스피너를 보여주고, 이후 내용을 표시

✅ 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
'LG 유플러스 유레카 SW/React' 카테고리의 다른 글
  • [#59] React 쇼핑몰 - Shop 페이지 상품 필터링/정렬 + 페이지네이션
  • [#58] React 쇼핑몰 - 장바구니 수정/삭제 + 반응형 Shop 페이지
  • [#56] React 쇼핑몰 - 상품 상세 페이지
  • [#55] React 쇼핑몰 - 스켈레톤 UI + 반응형 메인 리스트 페이지
nueos
nueos
  • nueos
    nueos 공부 기록
    nueos
  • 전체
    오늘
    어제
    • 분류 전체보기 (193)
      • 해커톤 (1)
      • 네이버 BoostCamp (6)
      • LG 유플러스 유레카 SW (5)
        • React (21)
        • TypeScript (2)
        • JavaScript (2)
        • HTML+CSS (5)
        • Spring (7)
        • Java (6)
        • SQL (2)
        • Algorithm (8)
        • CX (6)
        • Git (2)
        • 프로젝트 (2)
        • 스터디 (9)
        • 과제 (8)
        • 특강 (1)
      • React (3)
      • Next (0)
      • Javascript (2)
      • HTML (2)
      • CSS (9)
      • Algorithm (6)
      • Database (0)
      • OS (13)
      • C++ (24)
      • Python (1)
      • jQuery (1)
      • Django (1)
      • Git (1)
      • 개발 지식 (3)
      • 정보 보안 (22)
      • 포렌식 (1)
      • 암호 (2)
      • 기타 (4)
      • 패스트캠퍼스 FE 프로젝트십 (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Queue
    exhaustive search
    Stack
    완전 탐색
    힙
    디지랩챌린지
    heap
    제주지역혁신플랫폼지능형서비스사업단
    스택
    큐
    기술로바꾸는세상
    제주해커톤
    디지털혁신
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
nueos
[#57] React 쇼핑몰 - 상품 상세 페이지(탭, 슬라이더) + 장바구니 조회/추가
상단으로

티스토리툴바