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: 화면 자동 업데이트
useDispatch()- Redux에 요청 보내는 함수addItem- store.js에서 정의한 액션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
핵심 포인트:
- 🔗 useParams - URL에서 동적 파라미터 추출
- 🔍 find - 배열에서 조건에 맞는 요소 찾기
- 📤 dispatch - Redux Store에 액션 보내기
- 📥 useSelector - Redux Store에서 상태 가져오기
- 🔄 Redux 흐름 - dispatch → store → useSelector → 렌더링
다음 편에서는 store.js를 더 자세히 분석해볼게요. Redux Toolkit의 createSlice, configureStore가 어떻게 구성되어 있는지 알아봅니다!