Post

[Day57] 쇼핑몰 실습 - Tab, Modal, Cart

[Day57] 쇼핑몰 실습 - Tab, Modal, Cart

🔸 tab

구현 포인트

1️⃣ useState(0)으로 현재 선택된 탭의 index 저장

1
 const [activeTab, setActive] = useState(0)

2️⃣ 탭 버튼 클릭 시 해당 index를 상태로 변경

1
2
3
4
5
6
7
8
9
{tabTiles.map((title, i) => (
  <button
    key={i}
    className={activeTab === i ? css.active : ''}
    onClick={() => setActive(i)}
  >
    {title}
  </button>
))}

3️⃣ 각 탭 콘텐츠 영역에서 activeTab === i 조건으로 보여줄 콘텐츠를 결정

1
<div className={`${css.tabContent} ${activeTab === 0 ? css.active : ''}`}>

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const DetailTabInfo = () => {
  const [activeTab, setActive] = useState(0)
  const tabTiles = ['메뉴1', '메뉴2', '메뉴3']

  return (
    <>
      <div className={css.tabBtn}>
        {tabTiles.map((title, i) => (
          <button
            key={i}
            className={activeTab === i ? css.active : ''}
            onClick={() => setActive(i)}
          >
            {title}
          </button>
        ))}
      </div>
      <div className={`${css.tabContent} ${activeTab === 0 ? css.active : ''}`}>
        <h3>제목</h3>
      </div>
      <div className={`${css.tabContent} ${activeTab === 1 ? css.active : ''}`}>
        <h3>제목2</h3>
      </div>
      <div className={`${css.tabContent} ${activeTab === 2 ? css.active : ''}`}>
        <h3>제목3</h3>
      </div>
    </>
  )
}

export default DetailTabInfo

UI

day57


🔸 Modal

구현 포인트

1️⃣ useEffect를 사용해서 모달이 켜질 때(처음 마운트 시) active 클래스 부여

1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
  const timer = setTimeout(() => {
    setIsActive(true)
    document.body.style.overflow = 'hidden'
  }, 5)

  return () => {
    clearTimeout(timer)
    document.body.style.overflow = 'auto'
  }
}, [])

✅ 트랜지션 효과 제대로 동작하게 하기 위해 setTimeout 사용

  • setTimeout을 안쓰면 컴포넌트가 렌더링되자마자 바로 active 클래스가 붙어버림
  • 문제는 애니메이션(transition)이 안 먹는 경우가 생김
  • 그래서 브라우저가 먼저 초기 스타일을 렌더링하게 한 후 active를 붙임

2️⃣ 모달이 열리면 body.style.overflow = 'hidden'으로 스크롤 방지

1
document.body.style.overflow = 'hidden' 

3️⃣ 취소 버튼이나 외부 버튼 클릭 시 닫힘 (클래스 제거 후 setTimeout으로 애니메이션 적용)

1
2
3
4
const handleClose = () => {
  setIsActive(false)
  setTimeout(onClose, 300)
}

✅ 그냥 바로 onClose() 호출하면 모달이 애니메이션 없이 툭 꺼짐

4️⃣ “장바구니 담기” 누르면 addToCart API 실행 → 모달 닫고 → /cart로 이동

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const handleAddToCart = async () => {
  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()
  navigate('/cart')
}

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const Modal = ({ product, count, onClose }) => {
  const [isActive, setIsActive] = useState(false)
  const navigate = useNavigate()

  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)
  }

  const handleAddToCart = async () => {
    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()
      navigate('/cart')
    } catch (err) {
      console.log('err', err)
    }
  }
  return (
    <div className={`${css.modal} ${isActive ? css.active : ''}`}>
      <div className={`${css.container} `}>
        <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.tilte}</p>
            <p>{product.price.toLocaleString()}</p>
            {product.discount > 0 && <p>{product.discount}%</p>}
            <p>{count}</p>
            <p>총 가격 : {(product.price * count).toLocaleString()}</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>
  )
}

UI 완성 전 ! 일단 데이터 보내는거 확인

day57


🔸 Cart

구현 포인트

1️⃣ 라우터 설정 으로 /cart 진입 시, CartPage 렌더링

1
{ path: '/cart', element: <CartPage />, loader: cartPageLoader }

2️⃣ loader 함수로 장바구니 데이터 가져오기

1
2
3
4
5
6
7
8
9
10
11
export const cartPageLoader = async () => {
  try {
    const cartItems = await getCartData()
    if (!cartItems || cartItems.length === 0) {
      return { cartItems: [] }
    }
    return cartItems
  } catch (err) {
    console.log('err', err)
  }
}

3️⃣ 장바구니 데이터 처리 cartApi.js

  • getCartData() : 장바구니 전체 조회
1
2
3
4
5
6
7
8
9
export const getCartData = async () => {
  try {
    const res = await axios.get(`/api/cart/`)
    console.log('getCartData.js : getCartData', res)
    return res.data
  } catch (err) {
    console.log('err', err)
  }
}
  • addToCart() : 동일 상품 있으면 count 증가(PUT), 없으면 새로 추가(POST)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const addToCart = async cartItem => {
  try {
    const cart = await getCartData()
    const existingItem = cart.find(item => item.id === cartItem.id)
    if (existingItem) {
      const uedateItem = {
        ...existingItem,
        count: existingItem.count + cartItem.count,
      }
      const res = await axios.put(`/api/cart/${existingItem.id}`, uedateItem)
      return res.data
    } else {
      const res = await axios.post(`/api/cart/`, cartItem)
      return res.data
    }
  } catch (err) {
    console.log('err', err)
  }
}

4️⃣ CartPage에서 useLoaderData()로 불러온 장바구니 데이터 확인

1
const cartList = useLoaderData()

UI 완성 전 ! 일단 데이터 받아오는거 확인

day57


END

This post is licensed under CC BY 4.0 by the author.