Skip to content

[박연희] sprint5 #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: React-박연희
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,266 changes: 867 additions & 399 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
Expand All @@ -34,5 +35,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
}
}
Binary file removed public/favicon.ico
Binary file not shown.
53 changes: 14 additions & 39 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<html lang="ko">

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- <meta name="theme-color" content="#000000" /> -->
<!-- <meta name="description" content="Web site created using create-react-app" /> -->
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
<title>판다마켓</title>
</head>

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
<body>
<div id="root"></div>
</body>

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
</html>
Binary file removed public/logo192.png
Binary file not shown.
Binary file removed public/logo512.png
Binary file not shown.
25 changes: 0 additions & 25 deletions public/manifest.json

This file was deleted.

3 changes: 0 additions & 3 deletions public/robots.txt

This file was deleted.

38 changes: 0 additions & 38 deletions src/App.css

This file was deleted.

31 changes: 12 additions & 19 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import logo from './logo.svg';
import './App.css';
import Nav from "./components/Nav";
import BestProductList from "./components/BestProductList";
import ProductList from "./components/ProductList";
import './components/css/App.css'

function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<>
<Nav />
<div className="Products__warp">
<BestProductList/>
<ProductList/>
</div>
</>
);
}

export default App;
export default App;
8 changes: 0 additions & 8 deletions src/App.test.js

This file was deleted.

18 changes: 18 additions & 0 deletions src/Main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import App from "./App";
import "../src/components/css/reset.css";
import "../src/components/css/common.css";
import "../src/components/css/font.css";

function Main() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/items" element={<App />} />
</Routes>
</BrowserRouter>
);
}

export default Main;
46 changes: 46 additions & 0 deletions src/api.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState, useEffect } from "react";

export async function getProducts({ page = 1, pageSize = 1 } = {}) {
const params = new URLSearchParams();
if (page) params.append("page", page);
if (page && pageSize) params.append("pageSize", pageSize);
const response = await fetch(`https://panda-market-api.vercel.app/products?${params}`);

if (!response.ok) {
throw new Error("상품을 불러오는데 실패했습니다.");
}
const body = await response.json();
// console.log("API 응답:", body);
return body;
}

export function useProductData({ page, pageSize, isPageinated = true }) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPageinated -> isPaginated 오타났네요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://marketplace.cursorapi.com/items?itemName=streetsidesoftware.code-spell-checker

code spell checker extension 설치하셔서 오타나 틀린 문법 교정 도움받아보세요! :)

const [products, setProducts] = useState([]);
const [totalPages, setTotalPages] = useState(5); //전체 페이지 수 관리

useEffect(() => {
if (!pageSize) return; // pageSize가 없으면 API를 아예 호출하지 않게 차단한다.
const fetchData = async () => {
try {
const query = isPageinated ? { page, pageSize } : { pageSize };
const data = await getProducts(query);
setProducts(data.list);
console.log("getProducts 결과:", data.list);

if (isPageinated) {
if (data.totalPages) {
setTotalPages(data.totalPages);
} else if (data.totalCount) {
setTotalPages(Math.ceil(data.totalCount / pageSize));
}
}
} catch (error) {
console.error("상품 불러오기 실패", error);
}
};

fetchData();
}, [page, pageSize, isPageinated]);
Comment on lines +21 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

페이지네이션 로직과 data fetching 로직을 분리해주면 코드 중복을 줄이고 재사용에 도움이 되겠죠?

pagination만 처리하는 커스텀 훅을 따로 만들고,
해당 훅에서 이 훅을 재사용해보는 구조로 바꿔봅시다 :)

예시를 보여드릴게요!

import { useState, useEffect } from 'react';

export function usePagination({ 
  fetchData, 
  pageSize, 
  initialPage = 1,
  isEnabled = true 
}) {
  const [currentPage, setCurrentPage] = useState(initialPage);
  const [totalPages, setTotalPages] = useState(1);
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!isEnabled || !pageSize) return;

    const loadData = async () => {
      setIsLoading(true);
      setError(null);
      
      try {
        const result = await fetchData({ page: currentPage, pageSize });
        setData(result.list);
        
        if (result.totalPages) {
          setTotalPages(result.totalPages);
        } else if (result.totalCount) {
          setTotalPages(Math.ceil(result.totalCount / pageSize));
        }
      } catch (err) {
        setError(err);
        console.error('데이터 로딩 실패:', err);
      } finally {
        setIsLoading(false);
      }
    };

    loadData();
  }, [currentPage, pageSize, isEnabled, fetchData]);

  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages) {
      setCurrentPage(page);
    }
  };

  const nextPage = () => {
    if (currentPage < totalPages) {
      setCurrentPage(prev => prev + 1);
    }
  };

  const prevPage = () => {
    if (currentPage > 1) {
      setCurrentPage(prev => prev - 1);
    }
  };

  return {
    currentPage,
    totalPages,
    data,
    isLoading,
    error,
    goToPage,
    nextPage,
    prevPage,
    setCurrentPage
  };
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 기존의 useProductData 훅을 수정해서 새로 만든 usePagination 훅을 사용하도록 변경해볼게요.

export function useProductData({ pageSize, isPageinated = true }) {
  const {
    currentPage,
    totalPages,
    data: products,
    isLoading,
    error,
    goToPage,
    nextPage,
    prevPage,
  } = usePagination({
    fetchData: getProducts,
    pageSize,
    isEnabled: isPageinated,
  });

  return {
    products,
    currentPage,
    totalPages,
    isLoading,
    error,
    goToPage,
    nextPage,
    prevPage,
  };
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 구조를 바꿔주면, 이런 장점들이 생길 수 있습니다.

  • 재사용성: usePagination 훅은 어떤 종류의 데이터 페칭 함수와도 함께 사용될 수 있고, 이때 페이지네이션 로직을 재사용할 수 있습니다.
  • 일관성 향상 및 기능 확장: 일관적이고 명확한 방식으로 로딩 상태를 관리하고, 에러를 처리하고, 페이지 이동 메서드를 관리하고, 현재 페이지 상태를 관리할 수 있습니다. 또한 페이지네이션 관련 기능을 수정하고 확장하기도 용이해집니다.
  • 관심사 분리: 페이지네이션 로직이 데이터 페칭 로직과 분리되어 있어 각각의 책임이 명확해집니다.


return isPageinated ? { products, totalPages } : { products };
}
Binary file added src/assets/arrow_down.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/arrow_left.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/arrow_right.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/ic_heart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/ic_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/ic_sort.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/img_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/img_logo_m.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/components/BestProductList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState, useEffect } from "react";
import { useProductData } from "../api.jsx";
import { pageSizebyScreenWidth } from "./pageSizebyScreenWidth.jsx";
import ProductDisplay from "./ProductDisplay.jsx";
import "./css/ProductList.css";

function BestProductList() {
const [visibleCount, setVisibleCount] = useState(1); // 보여줄 상품 개수

// api 불러오기, 20개 불러온 뒤 반응형에 따라 자름
const { products } = useProductData({ pageSize: 20, isPageinated: false });

const sortedItems = [...products].sort((a, b) => b.favoriteCount - a.favoriteCount).slice(0, visibleCount);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프레드 연산자로 복사하신점 의도하신걸까요? 자바스크립트 공부 잘하셨네요 👍

NIT: 이미 알고계실수도 있지만 배경 설명을 좀 드려보자면,
sort() 메서드는 원본 배열을 직접 수정(mutate)하는 메서드라서 리액트의 상태 관리 원칙상 상태를 직접 수정하는 것은 피하는게 좋습니다. 따라서 원본 배열을 보존하기 위해 스프레드 연산자로 복사하는 것이 맞아요! 잘하셨어요 👍

왜 리액트를 사용할때 상태의 불변성을 유지하는게 좋은지에 대해서 더 찾아보시면 좋을것같네요 :)

참고


// 브라우저 크기에 따라 상품 개수 변경
useEffect(() => {
const width = window.innerWidth;
const sizes = pageSizebyScreenWidth(width);
setVisibleCount(sizes.best);
}, []);

return (
<div>
<div className="Products__header mb16">
<h1>베스트 상품</h1>
</div>
<ProductDisplay sortedItems={sortedItems} bestList={true} />
</div>
);
}

export default BestProductList;
46 changes: 46 additions & 0 deletions src/components/Nav.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NavLink } from "react-router-dom";
import mobileLogeImg from "../assets/img_logo_m.png";
import logeImg from "../assets/img_logo.png";
import ProfileImg from "../assets/profile.png";
import "./css/Nav.css";

function getLinkStyle({ isActive }) {
return {
color: isActive ? "#3692FF" : "#333",
};
}

function Nav() {
return (
<header className="header">
<div className="header__logo">
<a href="/" aria-label="홈으로 이동">
<img
className="header__logo-img"
src={mobileLogeImg}
srcSet={`${mobileLogeImg} 103w, ${logeImg} 153w`}
sizes="(max-width: 767px) 103px, 153px"
alt="판다마켓"
/>
</a>
</div>
<ul className="nav">
<li>
<NavLink to="/" style={getLinkStyle}>
자유게시판
</NavLink>
</li>
<li>
<NavLink to="/items" style={getLinkStyle}>
중고마켓
</NavLink>
</li>
</ul>
<div className="profile">
<img src={ProfileImg} alt="프로필" />
</div>
</header>
);
}

export default Nav;
41 changes: 41 additions & 0 deletions src/components/Pagination.jsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 컴포넌트에서도, 아까 페이지네이션 로직을 따로 분리한 커스텀훅을 재사용해주세요 :)

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "../components/css/Pagination.css";
import preve from "../assets/arrow_left.png";
import next from "../assets/arrow_right.png";

function Pagination({ currentPage, totalPages, setPage }) {
//페이지 변경
const handlePageChange = (newPage) => {
setPage(newPage);
};

// console.log("현재페이지 : ", currentPage);
// console.log("총 페이지 : ", totalPages);

const pageNumbers = []; //버튼에 쓸 숫자가 들어간다.

const start = currentPage - 2 < 1 ? 1 : currentPage - 2;
const end = start + 4 > totalPages ? totalPages : start + 4;

for (let i = start; i <= end; i++) {
pageNumbers.push(i);
}

return (
<div className="pagination ">
{/* {currentPage > 1 && } */}
<button onClick={() => handlePageChange(currentPage - 1)} disabled={currentPage === 1}>
<img src={preve} alt="이전" />
</button>
{pageNumbers.map((num) => (
<button key={num} onClick={() => handlePageChange(num)} className={num === currentPage ? "active" : undefined}>
{num}
</button>
))}
<button onClick={() => handlePageChange(currentPage + 1)} disabled={currentPage === totalPages}>
<img src={next} alt="다음" />
</button>
</div>
);
}

export default Pagination;
Loading
Loading