React

React 쇼핑몰 구조 이해하기 3편 - 상품 상세와 장바구니 흐름


상품 목록을 클릭하면 상세 페이지로 이동하죠? 그리고 “주문하기” 버튼을 누르면 장바구니에 담기고요.

이번 편에서는 이 과정이 코드에서 어떻게 동작하는지 분석해볼게요. React Router의 동적 라우팅과 Redux 흐름을 이해할 수 있습니다.

이번 편에서 알아볼 내용

  • ✅ useParams로 URL 파라미터 받는 방법
  • ✅ Detail 컴포넌트의 상품 정보 표시 로직
  • ✅ Redux dispatch로 장바구니에 상품 추가되는 흐름
  • ✅ Cart 컴포넌트에서 useSelector로 상태 가져오는 방법

전체 데이터 흐름

graph TD
    A["상품 클릭<br/>/detail/1"] --> B["useParams<br/>paramId = '1'"]
    B --> C["props.fruit에서<br/>id가 1인 상품 찾기"]
    C --> D["Detail 컴포넌트<br/>상품 정보 표시"]
    D --> E["'주문하기' 클릭"]
    E --> F["dispatch(addItem)"]
    F --> G["Redux store<br/>cart 배열에 추가"]
    G --> H["Cart 컴포넌트<br/>장바구니 목록 표시"]
    
    style B fill:#ffe1e1
    style F fill:#e1d5ff
    style G fill:#e1f5ff

Detail.js 전체 코드 (복사해서 사용)

src/components/Detail.js 파일을 만들어주세요:

import { useParams, Link } from "react-router-dom";
import { Nav, Button } from "react-bootstrap";
import { useState } from "react";
import { addItem } from "../store.js";
import { useDispatch } from "react-redux";

function Detail(props) {
  let [tap, setTap] = useState(0);
  let { paramId } = useParams();
  const dispatch = useDispatch();

  // ID로 상품 찾기
  const paramIdNum = Number(paramId);
  const isVeggie = paramIdNum >= 10;

  let selproduct = null;
  if (isVeggie) {
    selproduct = props.veggie ? props.veggie.find((x) => x.id === paramIdNum) : null;
  } else {
    selproduct = props.fruit ? props.fruit.find((x) => x.id === paramIdNum) : null;
  }

  if (!selproduct) {
    return <div>해당 상품이 존재하지 않습니다.</div>;
  }

  const { id, imgUrl, title, content, price } = selproduct;

  return (
    <div className="container">
      <div className="row">
        <div className="col-md-6">
          <img src={"/" + imgUrl} width="100%" alt={title} />
        </div>
        <div className="col-md-6">
          <h5 className="pt-5">{title}</h5>
          <p>{content}</p>
          <p>{price}원</p>
          <button
            className="btn btn-danger"
            onClick={() => {
              let imgurlValue = imgUrl.replace("img/", "");
              dispatch(
                addItem({
                  id: id,
                  imgurl: imgurlValue,
                  name: title,
                  count: 1,
                })
              );
            }}
            style={{ marginRight: "10px" }}
          >
            주문하기
          </button>
          <Link to="/cart">
            <Button variant="outline-success">주문상품 확인하기</Button>
          </Link>
        </div>
      </div>

      {/* 탭 UI */}
      <Nav variant="tabs" defaultActiveKey="link0" style={{ marginTop: "50px" }}>
        <Nav.Item>
          <Nav.Link onClick={() => setTap(0)} eventKey="link0">상세정보</Nav.Link>
        </Nav.Item>
        <Nav.Item>
          <Nav.Link onClick={() => setTap(1)} eventKey="link1">배송안내</Nav.Link>
        </Nav.Item>
        <Nav.Item>
          <Nav.Link onClick={() => setTap(2)} eventKey="link2">교환/반품</Nav.Link>
        </Nav.Item>
      </Nav>
      <div style={{ padding: "20px" }}>
        {tap === 0 ? "상품 상세 정보입니다." : tap === 1 ? "배송 안내입니다." : "교환/반품 안내입니다."}
      </div>
    </div>
  );
}

export default Detail;

useParams - URL 파라미터 받기

graph LR
    A["URL: /detail/3"] --> B["useParams()"]
    B --> C["{ paramId: '3' }"]
    C --> D["Number(paramId) → 3"]
    D --> E["fruit.find(x => x.id === 3)"]
    E --> F["사과 상품 데이터"]
    
    style B fill:#ffe1e1
    style E fill:#e1f5ff
// App.js의 Route 설정
<Route path="/detail/:paramId" element={<Detail fruit={fruit} veggie={veggie} />} />

// Detail.js에서 파라미터 받기
let { paramId } = useParams();

:paramId 부분이 동적 파라미터예요. /detail/1, /detail/2, /detail/99 등 어떤 숫자든 받을 수 있어요.

과일 vs 채소 구분 로직

const paramIdNum = Number(paramId);  // 문자열 → 숫자
const isVeggie = paramIdNum >= 10;   // 10 이상이면 채소

let selproduct = null;
if (isVeggie) {
  selproduct = props.veggie.find((x) => x.id === paramIdNum);
} else {
  selproduct = props.fruit.find((x) => x.id === paramIdNum);
}

[!TIP] find() 메서드는 조건에 맞는 첫 번째 요소를 반환해요. 없으면 undefined를 반환하니까 예외 처리가 필요해요!

장바구니에 상품 추가되는 흐름

dispatch 사용법

import { addItem } from "../store.js";
import { useDispatch } from "react-redux";

function Detail(props) {
  const dispatch = useDispatch();
  
  return (
    <button
      className="btn btn-danger"
      onClick={() => {
        let imgurlValue = imgUrl.replace("img/", "");
        dispatch(
          addItem({
            id: id,
            imgurl: imgurlValue,
            name: title,
            count: 1,
          })
        );
      }}
    >
      주문하기
    </button>
  );
}

dispatch 작동 원리:

sequenceDiagram
    participant D as Detail 컴포넌트
    participant P as dispatch()
    participant S as Redux Store
    participant C as Cart 컴포넌트
    
    D->>P: dispatch(addItem({상품정보}))
    P->>S: store.js의 addItem 실행
    S->>S: cart 배열에 상품 추가
    S-->>C: 상태 변화 알림
    C->>C: 화면 자동 업데이트
  1. useDispatch() - Redux에 요청 보내는 함수
  2. addItem - store.js에서 정의한 액션
  3. dispatch(addItem({...})) - “이 상품을 cart에 추가해줘!” 요청

Cart.js 전체 코드 (복사해서 사용)

src/components/Cart.js 파일을 만들어주세요:

import { useDispatch, useSelector } from "react-redux";
import { Table, Button } from "react-bootstrap";
import { addCount, decreaseCount, deleteItem, sortName } from "../store.js";

function Cart() {
  // Redux 상태 가져오기
  const { user: { name, age }, cart } = useSelector((state) => state);
  const dispatch = useDispatch();

  return (
    <div className="container" style={{ marginTop: "40px" }}>
      <h5>{name}의 장바구니 ({age}세)</h5>
      <Button
        variant="outline-primary"
        onClick={() => dispatch(sortName())}
        style={{ marginBottom: "20px" }}
      >
        이름순 정렬
      </Button>
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>#</th>
            <th>상품이미지</th>
            <th>상품명</th>
            <th>수량</th>
            <th>변경하기</th>
          </tr>
        </thead>
        <tbody>
          {cart.map(({ id, imgurl, name, count }, i) => (
            <tr key={i}>
              <td>{id}</td>
              <td>
                <img src={`img/${imgurl}`} alt={name} width="80" />
              </td>
              <td>{name}</td>
              <td>{count}</td>
              <td>
                <Button
                  variant="outline-primary"
                  onClick={() => dispatch(addCount(id))}
                  style={{ marginRight: "5px" }}
                >
                  +
                </Button>
                <Button
                  variant="outline-secondary"
                  onClick={() => dispatch(decreaseCount(id))}
                  style={{ marginRight: "5px" }}
                >
                  -
                </Button>
                <Button
                  variant="outline-danger"
                  onClick={() => dispatch(deleteItem(id))}
                >
                  상품삭제
                </Button>
              </td>
            </tr>
          ))}
        </tbody>
      </Table>
    </div>
  );
}

export default Cart;

useSelector 작동 원리:

graph LR
    A["Redux Store"] --> B["useSelector"]
    B --> C["{ user, cart }"]
    C --> D["컴포넌트에서 사용"]
    
    A2["상태가 바뀌면"] --> B2["자동 리렌더링"]
    
    style B fill:#e1d5ff

장바구니 버튼들

{cart.map(({ id, imgurl, name, count }, i) => (
  <tr key={i}>
    <td>{id}</td>
    <td><img src={`img/${imgurl}`} alt={name} /></td>
    <td>{name}</td>
    <td>{count}</td>
    <td>
      <Button onClick={() => dispatch(addCount(id))}>+</Button>
      <Button onClick={() => dispatch(decreaseCount(id))}>-</Button>
      <Button onClick={() => dispatch(deleteItem(id))}>상품삭제</Button>
    </td>
  </tr>
))}

장바구니 액션들:

graph LR
    subgraph "버튼 클릭"
        A["+ 버튼"]
        B["- 버튼"]
        C["삭제 버튼"]
    end
    
    subgraph "Redux 액션"
        A1["addCount(id)"]
        B1["decreaseCount(id)"]
        C1["deleteItem(id)"]
    end
    
    A --> A1
    B --> B1
    C --> C1
    
    style A1 fill:#e1ffe1
    style B1 fill:#fff4e1
    style C1 fill:#ffe1e1

정리

오늘 3편에서 분석한 내용:

graph LR
    A["useParams"] --> B["상품 ID 받기"]
    B --> C["find로 상품 찾기"]
    C --> D["Detail에 표시"]
    D --> E["dispatch로 추가"]
    E --> F["Redux Store"]
    F --> G["useSelector"]
    G --> H["Cart에 표시"]
    
    style A fill:#ffe1e1
    style E fill:#e1d5ff
    style G fill:#e1f5ff

핵심 포인트:

  1. 🔗 useParams - URL에서 동적 파라미터 추출
  2. 🔍 find - 배열에서 조건에 맞는 요소 찾기
  3. 📤 dispatch - Redux Store에 액션 보내기
  4. 📥 useSelector - Redux Store에서 상태 가져오기
  5. 🔄 Redux 흐름 - dispatch → store → useSelector → 렌더링

다음 편에서는 store.js를 더 자세히 분석해볼게요. Redux Toolkit의 createSlice, configureStore가 어떻게 구성되어 있는지 알아봅니다!