Skip to content

Conversation

@cskime
Copy link
Collaborator

@cskime cskime commented Jul 25, 2025

요구사항

기본

중고마켓

  • 중고마켓 페이지 주소는 “/items” 입니다.
  • 페이지 주소가 “/items” 일때 상단네비게이션바의 '중고마켓' 버튼의 색상은 “3692FF”입니다.
  • 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어 주세요.
  • 상품 데이터 정보는 https://panda-market-api.vercel.app/docs/#/ 에 명세된 GET 메소드 “/products” 를 사용해주세요.
  • '상품 등록하기' 버튼을 누르면 “/additem” 로 이동합니다. ( 빈 페이지 )
  • 전체 상품에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다.

중고마켓 반응형

  • 베스트 상품
    • Desktop : 4개 보이기
    • Tablet : 2개 보이기
    • Mobile : 1개 보이기
  • 전체 상품
    • Desktop : 12개 보이기
    • Tablet : 6개 보이기
    • Mobile : 4개 보이기

심화

  • 페이지 네이션 기능을 구현합니다.

주요 변경사항

  • /items path로 이동하면 상품 목록을 불러오고 '베스트 상품' 및 '전체 상품' section에 상품을 표시합니다.
  • '최신순' 또는 '좋아요순' 정렬 설정 시 새로 상품 목록을 불러옵니다.

스크린샷

Desktop Tablet Mobile
desktop tablet mobile

멘토에게

  • Netlify에 배포하고 싶었는데요. npm run builddist directory에 있는 파일들을 모두 업로드해도 웹사이트가 정상적으로 표시되지 않고 있습니다. 다른 작업이 더 필요한걸까요?
  • 이번 미션에서는 강의에서 사용하던 폴더 구조를 그대로 따라했는데요. Component와 파일 갯수가 많아지면 원하는 파일을 찾아다니기 어렵겠다는 생각이 들었습니다. 일반적으로 React 프로젝트는 폴더 구조를 어떻게 잡나요?
  • React 코드에서 반응형 디자인을 구현하는 방법이 궁금합니다. 이번 미션에서는 useMediaQuery라는 custom hook을 만들어서 사용했는데요. 더 좋은 방법이 있을까요? 현업에서는 어떤 방식으로 반응형 디자인을 구현하나요?

cskime added 26 commits July 24, 2025 09:00
@cskime cskime requested a review from kiJu2 July 25, 2025 04:25
@cskime cskime added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jul 25, 2025
cskime added 2 commits July 26, 2025 15:59
- `useMediaQuery` hook
  - 이름 변경 : `useMediaQuery` -> `useDevice`
  - `change` event handler 동작 방식 개선
- Component가 과도하게 re-rendering 되는 문제 수정
- `fetchProducts` 서버 요청이 과도하게 전송되는 문제 수정
@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 29, 2025

스프리트 미션 하시느라 수고 많으셨어요.
참솔님 학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 29, 2025

Netlify에 배포하고 싶었는데요. npm run build 후 dist directory에 있는 파일들을 모두 업로드해도 웹사이트가 정상적으로 표시되지 않고 있습니다. 다른 작업이 더 필요한걸까요?

음 저도 Netlify로는 배포 해본 적이 없긴 하나 다른 배포 환경이랑 다를게 없을 것 같아요.
작성주신 내용 들어보니가 파일을 업로드 하신걸까요? 깃허브로 연결 한 번 해보시는건 어떨까요?

참고1
참고2

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 29, 2025

이번 미션에서는 강의에서 사용하던 폴더 구조를 그대로 따라했는데요. Component와 파일 갯수가 많아지면 원하는 파일을 찾아다니기 어렵겠다는 생각이 들었습니다. 일반적으로 React 프로젝트는 폴더 구조를 어떻게 잡나요?

"일반적으로"라면 참솔님께서 작성하신 구조가 일반적인 구조라고 볼 수 있을 것 같아요.
지금 작성하신 구조에서 규칙을 만들고 커스텀 합니다 !

다음 문서 참고하시면 도움 되실거예요 😊

[리액트] 네이버는 폴더구조를 어떻게 구성 하고있을까

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 29, 2025

React 코드에서 반응형 디자인을 구현하는 방법이 궁금합니다. 이번 미션에서는 useMediaQuery라는 custom hook을 만들어서 사용했는데요. 더 좋은 방법이 있을까요? 현업에서는 어떤 방식으로 반응형 디자인을 구현하나요?

자바스크립트로 DOM에 접근하여 처리하면 성능이 좋지 않기에 왠만하면 css로 해결하는게 좋습니다. 😊

@@ -0,0 +1,28 @@
const BASE_URL = "https://panda-market-api.vercel.app";
Copy link
Collaborator

Choose a reason for hiding this comment

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

base URL은 환경 변수에 저장하시는게 좋습니다!

환경 변수(Environment Variable): process.env에 내장되며 앱이 실행될 때 적용할 수 있는 값입니다!

다음과 같이 적용할 수 있습니다:

// .env.development
REACT_APP_BASE_URL="http://localhost:3000"

// .env.production
REACT_APP_BASE_URL="http://myapi.com"

// 사용시
<a href={`${process.env.REACT_APP_BASE_URL}/myroute`}>URL</a>

왜 환경 변수에 저장해야 하나요?

개발(dev), 테스트(test), 실제 사용(prod) 등 다양한 환경에서 앱을 운영하게 되는 경우, 각 환경에 따라 다른 base URL을 사용해야 할 수 있습니다. 만약 코드 내에 하드코딩되어 있다면, 각 환경에 맞춰 앱을 배포할 때마다 코드를 변경해야 하며, 이는 매우 번거로운 작업이 됩니다. 하지만, 환경 변수를 .env.production, .env.development, .env.test와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

const apiUrl = `${process.env.REACT_APP_BASE_URL}/api`;

이러한 방식으로 환경 변수를 사용하면, 배포 환경에 따라 쉽게 URL을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

실제 코드 응용과 관련해서는 다음 한글 아티클을 참고해보세요! => 보러가기

Comment on lines +3 to +8
export async function fetchProducts({
keyword = "",
page = 1,
pageSize = 10,
orderBy = "recent",
} = {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! 옵셔널한 쿼리들을 객체 인자로 받고 있군요 ! 👍

객체로 만들어 두니 사용하기 정말 편하겠어요 👍👍

Comment on lines +9 to +15
const url = new URL(`${BASE_URL}/products`);
url.searchParams.append("page", page);
url.searchParams.append("pageSize", pageSize);
url.searchParams.append("orderBy", orderBy);
if (keyword) {
url.searchParams.append("keyword", keyword);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 좋습니다. URL 객체를 사용하셨군요. searchParams에 추가 하신 점도 좋습니다 👍👍

URL 인코딩을 자동으로 처리하여 특수 문자나 공백이 포함된 값에서도 안전하게 동작되겠네요 ~!

Comment on lines +19 to +21
if (!response.ok) {
throw new Error("Failed to fetch products");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으~ 예외처리도 처리하셨군요 꼼꼼합니다 !

import "./Item.css";

function Item({ imageUrl, title, price, likeCount }) {
const priceString = Intl.NumberFormat().format(price) + "원";
Copy link
Collaborator

Choose a reason for hiding this comment

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

국제화 내장 모듈인 Intl을 사용하셨군요 ! 👍

정말 꼼꼼하시네요 참솔님 ㄷㄷㄷ

<img src={imageUrl} alt="상품 이미지" />
</div>
<div className="Item-info">
<p className="Item-title">{title}</p>
Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 태그는 "단락"(p) 보다는 "제목"(h)이라는 의미에 가깝겠군요? 😊

);
}

export { ItemsSection, ItemsSectionContent, ItemsSectionHeader };
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으.. 컴포넌트를 분할하여 정의하셨군요 정말 깔끔하네요 👍

Comment on lines +36 to +42
function ItemsSection({ spacing = 16, children }) {
return (
<section className="ItemsSection" style={{ gap: spacing }}>
{children}
</section>
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택)다음과 같이 스타일을 정의하면 더욱 유연한 스타일을 지정할 수 있습니다 !

Suggested change
function ItemsSection({ spacing = 16, children }) {
return (
<section className="ItemsSection" style={{ gap: spacing }}>
{children}
</section>
);
}
function ItemsSection({ style = { gap: 16 }, children }) {
return (
<section className="ItemsSection" style={{ ...style, /* `ItemsSection`의 고유 스타일 */ }}>
{children}
</section>
);
}

Comment on lines +8 to +17
const matchesDesktop = _matchesDesktop ?? matchMedia("(min-width: 1200px)");
const matchesTablet =
_matchesTablet ?? matchMedia("(min-width: 768px) and (max-width: 1199px)");
const matchesMobile = _matchesMobile ?? matchMedia("(max-width: 767px)");

const [deviceInfo, setDeviceInfo] = useState({
isDesktop: matchesDesktop.matches,
isTablet: matchesTablet.matches,
isMobile: matchesMobile.matches,
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으. 적절한 커스텀 훅이네요.

요구사항에 적합한 적절한 커스텀 훅입니다 !
질의에 대해서는 css로 처리하는게 좋다고 답하였으나
요구사항 대로라면 디바이스 별로 데이터 출력 수가 달라야 하는 것을 보니 js가 들어갈 수밖에 없겠네요.

이러한 상황에서 훅을 만드시면 재사용성과 유지보수성이 크게 증가할 것으로 보여요 👍👍

Comment on lines +46 to +48
const bestProducts = [...products]
.sort((a, b) => b.favoriteCount - a.favoriteCount)
.slice(0, bestProductsColumns);
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택) 해당 로직은 정렬이 들어가기에 메모이제이션을 고려해볼 수 있겠네요 !

Suggested change
const bestProducts = [...products]
.sort((a, b) => b.favoriteCount - a.favoriteCount)
.slice(0, bestProductsColumns);
const bestProducts = useMemo( () => [...products]
.sort((a, b) => b.favoriteCount - a.favoriteCount)
.slice(0, bestProductsColumns), [products]);

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 29, 2025

크으 참솔님~
리액트 첫 미션 정말 수고 많으셨습니다! 가장 빠르게 구현하셨네요 !
커스텀 훅 구성까지 전체적으로 정말 깔끔하게 잘 구현하셨어요 😊
궁금한 점도 명확히 정리해서 질문하신 점이 특히 멋졌고, 앞으로의 성장이 더욱 기대됩니다.

미션 수행하시느라 정말 수고 많으셨습니다 !! 👍👍👍

@kiJu2 kiJu2 merged commit 75fa21a into codeit-bootcamp-frontend:React-김참솔 Jul 29, 2025
@cskime cskime deleted the React-김참솔-sprint5 branch July 29, 2025 10:39
@cskime cskime self-assigned this Aug 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants