React

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
  1. 구조 분해 할당(Destructuring): { id, title, price, imgUrl, content } - props를 한 번에 꺼내서 사용
  2. useNavigate: 클릭하면 상세 페이지로 이동
  3. 템플릿 리터럴: /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

핵심 포인트:

  1. 📦 map - 배열을 반복해서 컴포넌트 생성
  2. 🔍 filter - 조건에 맞는 데이터만 선별
  3. 📊 sort - 데이터 정렬 (새 배열로 복사해서!)
  4. 🔗 스프레드 연산자 - {...data}로 props 전달, [...arr]로 배열 복사
  5. 🚀 useNavigate - 프로그래밍으로 페이지 이동

다음 편에서는 상품 상세 페이지와 장바구니가 어떻게 연결되어 있는지 알아볼게요!