React 쇼핑몰 구조 이해하기 2편 - 상품 목록과 검색/정렬 분석
1편에서 프로젝트 구조를 파악했죠? 이제 상품 목록이 화면에 어떻게 표시되는지 코드를 분석해볼게요.
이번 편에서는 상품 데이터가 화면에 렌더링되는 과정과 검색/정렬 기능이 어떻게 동작하는지 살펴봅니다.
이번 편에서 알아볼 내용
- ✅ 상품 데이터 파일 구조 (fruit.js, veggie.js)
- ✅ map으로 상품 목록 렌더링하는 방법
- ✅ Products 컴포넌트 구조
- ✅ filter로 구현된 검색 기능
- ✅ sort로 구현된 정렬 기능
- ✅ useNavigate로 페이지 이동하는 방법
전체 데이터 흐름
먼저 데이터가 어떻게 흘러가는지 볼게요:
graph TD
A["fruit.js / veggie.js<br/>(상품 데이터)"] --> B["App.js<br/>(useState로 상태 저장)"]
B --> C["map으로 반복"]
C --> D["Products / ComVeggie<br/>(상품 카드 컴포넌트)"]
D --> E["화면에 표시"]
F["검색어 입력"] --> G["filter로 필터링"]
G --> C
H["정렬 선택"] --> I["sort로 정렬"]
I --> C
style A fill:#e1f5ff
style B fill:#ffe1e1
style D fill:#e1ffe1
style F fill:#fff4e1
style H fill:#e1d5ff
상품 데이터 파일 만들기
fruit.js - 과일 데이터 (전체 코드)
src/db/fruit.js 파일을 만들어주세요:
let data = [
{
id: 1,
title: "수박",
imgUrl: "img/fruit1.jpg",
content: "당도선별 프리미엄 고당도 하우스수박 5~6kg",
price: 29000,
},
{
id: 2,
title: "참외",
imgUrl: "img/fruit2.jpg",
content: "산지직송 성주 달콤참외 3kg",
price: 16900,
},
{
id: 3,
title: "사과",
imgUrl: "img/fruit3.jpg",
content: "고씨네농장 고당도 청송사과, 햇부사 5kg",
price: 13000,
},
{
id: 4,
title: "바나나",
imgUrl: "img/fruit4.jpg",
content: "델몬트 필리핀 바나나 6kg",
price: 15000,
},
{
id: 5,
title: "딸기",
imgUrl: "img/fruit5.jpg",
content: "설향 딸기, 킹스베리 1박스",
price: 14500,
},
{
id: 6,
title: "오렌지",
imgUrl: "img/fruit6.jpg",
content: "캘리포니아 네이블 오렌지 4kg",
price: 17000,
},
{
id: 7,
title: "토마토",
imgUrl: "img/fruit7.jpg",
content: "행복한 농부 완숙찰토마토 5kg",
price: 20000,
},
{
id: 8,
title: "포도",
imgUrl: "img/fruit8.jpg",
content: "팜앤프룻 프리미엄 아삭한 애플청포도",
price: 25000,
},
{
id: 9,
title: "망고",
imgUrl: "img/fruit9.jpg",
content: "프리미엄 제주애플망고 1박스",
price: 18500,
},
];
export default data;
각 상품은 이런 속성을 가지고 있어요:
id- 고유 식별자 (과일: 1~9)title- 상품명imgUrl- 이미지 경로content- 상품 설명price- 가격
veggie.js - 채소 데이터 (전체 코드)
src/db/veggie.js 파일을 만들어주세요:
let veggie = [
{
id: 10,
title: "당근",
imgUrl: "img/veggie/veggie1.jpg",
content: "국내산 햇당근 1kg",
price: 11900,
},
{
id: 11,
title: "옥수수",
imgUrl: "img/veggie/veggie2.jpg",
content: "쫀득쫀득 찰옥수수 250g",
price: 29000,
},
{
id: 12,
title: "감자",
imgUrl: "img/veggie/veggie3.jpg",
content: "강원도 두백산 수미 감자 33kg",
price: 30000,
},
];
export default veggie;
[!TIP] 과일은 id가 1~9, 채소는 id가 10 이상이에요. 이걸로 나중에 채소인지 과일인지 구분해요!
Products 컴포넌트 분석
Props로 데이터 받기
src/components/Products.js 코드:
import React from "react";
import { Nav } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
const Products = ({ id, title, price, imgUrl, content }) => {
const navigate = useNavigate();
return (
<div className="col-md-4" style={{ marginBottom: "50px" }}>
<Nav.Link className="c1" onClick={() => navigate(`/detail/${id}`)}>
<img src={imgUrl} width="80%" alt={title} />
<h5 style={{ marginTop: "10px" }}>{title}</h5>
<p>{content}</p>
<span>{price}</span>
</Nav.Link>
</div>
);
};
export default Products;
핵심 포인트:
graph LR
A["App.js에서<br/>데이터 전달"] --> B["Products<br/>id, title, price..."]
B --> C["화면에<br/>카드 표시"]
D["클릭!"] --> E["useNavigate"]
E --> F["/detail/1로 이동"]
style A fill:#e1f5ff
style B fill:#ffe1e1
style C fill:#e1ffe1
style E fill:#fff4e1
- 구조 분해 할당(Destructuring):
{ id, title, price, imgUrl, content }- props를 한 번에 꺼내서 사용 - useNavigate: 클릭하면 상세 페이지로 이동
- 템플릿 리터럴:
/detail/\${id}형태로 동적 URL 생성
App.js에서 상품 목록 렌더링
App.js에서 Products 컴포넌트를 어떻게 사용하는지 볼게요:
1. 데이터 import 및 상태 설정
import data from "./db/fruit";
import data2 from "./db/veggie";
import Products from "./components/Products";
function App() {
const [fruit, setFruit] = useState(data);
let [veggie, setVeggie] = useState(data2);
let [input, setInput] = useState(""); // 검색어
// ...
}
2. map으로 상품 목록 표시하기
<div className="container" style={{ marginTop: "30px" }}>
<div className="row">
{fruit.map((fruit) => (
<Products {...fruit} key={fruit.id} />
))}
</div>
</div>
map이 하는 일:
graph LR
subgraph "fruit 배열"
A1["수박"]
A2["참외"]
A3["사과"]
end
M["map 함수"]
subgraph "Products 컴포넌트들"
B1["🍉 수박 카드"]
B2["🍈 참외 카드"]
B3["🍎 사과 카드"]
end
A1 --> M
A2 --> M
A3 --> M
M --> B1
M --> B2
M --> B3
style M fill:#ffe1e1
fruit.map()- 배열의 각 아이템에 대해 함수 실행{...fruit}- 스프레드 연산자로 모든 속성 전달 (id={id} title={title} … 와 같음)key={fruit.id}- React가 각 아이템을 구분하는 고유 키
검색 기능 분석
검색 입력창
<input
placeholder="상품명을 입력하세요"
onChange={(e) => setInput(e.target.value)}
value={input}
style={{
padding: "10px",
borderRadius: "4px",
border: "1px solid #ccc",
width: "250px"
}}
/>
onChange- 입력할 때마다 실행e.target.value- 입력된 값setInput()- 상태 업데이트
filter로 검색 결과 필터링
{fruit
.filter((item) => {
return item.title.toLowerCase().includes(input.toLowerCase());
})
.map((fruit) => (
<Products {...fruit} key={fruit.id} />
))}
filter가 하는 일:
graph TD
A["전체 상품<br/>수박, 참외, 사과..."] --> B{"검색어: '수'"}
B --> C["filter 실행"]
C --> D["'수'가 포함된 상품만"]
D --> E["수박, 옥수수"]
E --> F["화면에 표시"]
style B fill:#fff4e1
style C fill:#ffe1e1
style E fill:#e1ffe1
filter()- 조건에 맞는 아이템만 반환toLowerCase()- 대소문자 구분 없이 검색includes()- 문자열 포함 여부 확인
정렬 기능 분석
정렬 함수들
App.js에 정의된 정렬 함수들:
// 이름순 정렬
const sortByName = () => {
let sortedFruit = [...fruit].sort((a, b) => (a.title > b.title ? 1 : -1));
setFruit(sortedFruit);
};
// 낮은 가격순
const sortByPriceLowToHigh = () => {
let sortedFruit = [...fruit].sort((a, b) => a.price - b.price);
setFruit(sortedFruit);
};
// 높은 가격순
const sortByPriceHighToLow = () => {
let sortedFruit = [...fruit].sort((a, b) => b.price - a.price);
setFruit(sortedFruit);
};
sort가 하는 일:
graph LR
subgraph "원본 배열"
A["수박 29000원"]
B["참외 16900원"]
C["사과 13000원"]
end
S["sort<br/>(낮은 가격순)"]
subgraph "정렬된 배열"
D["사과 13000원"]
E["참외 16900원"]
F["수박 29000원"]
end
A --> S
B --> S
C --> S
S --> D
S --> E
S --> F
style S fill:#ffe1e1
[!IMPORTANT]
[...fruit]- 스프레드 연산자로 새 배열을 만들어요! 원본 배열을 직접 수정하면 React가 변화를 감지 못해요.
select로 정렬 선택하기
<select
onChange={(e) => {
if (e.target.value === "low") sortByPriceLowToHigh();
if (e.target.value === "high") sortByPriceHighToLow();
if (e.target.value === "name") sortByName();
}}
>
<option value="">정렬 선택</option>
<option value="low">낮은 가격순</option>
<option value="high">높은 가격순</option>
<option value="name">이름순</option>
</select>
더 보기 버튼과 Axios
외부 데이터를 추가로 불러오는 “더 보기” 버튼:
<Button
variant="outline-success"
onClick={() => {
if (count === 1) {
axios
.get("https://sinaboro.github.io/react_data/veggie2.json")
.then((result) => {
let copy = [...veggie, ...result.data];
setVeggie(copy);
setCount(count + 1);
});
}
// ...
}}
>
+ 3개 상품 더 보기
</Button>
데이터 로딩 흐름:
sequenceDiagram
participant U as 사용자
participant A as App.js
participant S as 외부 서버
U->>A: "더 보기" 클릭
A->>S: axios.get() 요청
S-->>A: JSON 데이터 응답
A->>A: [...veggie, ...result.data]
A->>A: setVeggie() 상태 업데이트
A-->>U: 화면에 새 상품 표시
정리
오늘 2편에서 분석한 내용:
graph LR
A["데이터 파일"] --> B["useState"]
B --> C["filter<br/>(검색)"]
C --> D["sort<br/>(정렬)"]
D --> E["map<br/>(렌더링)"]
E --> F["컴포넌트"]
style C fill:#fff4e1
style D fill:#e1d5ff
style E fill:#ffe1e1
핵심 포인트:
- 📦 map - 배열을 반복해서 컴포넌트 생성
- 🔍 filter - 조건에 맞는 데이터만 선별
- 📊 sort - 데이터 정렬 (새 배열로 복사해서!)
- 🔗 스프레드 연산자 -
{...data}로 props 전달,[...arr]로 배열 복사 - 🚀 useNavigate - 프로그래밍으로 페이지 이동
다음 편에서는 상품 상세 페이지와 장바구니가 어떻게 연결되어 있는지 알아볼게요!