Skip to content

Conversation

@huuitae
Copy link
Collaborator

@huuitae huuitae commented Jul 31, 2025

요구사항

기본

  • 중고마켓 페이지 주소는 "/items" 입니다.
  • 페이지 주소가 "/items" 일 때 상단 네비게이션 바의 "중고마켓" 버튼의 색상은 "3692FF" 입니다.
  • 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어주세요
  • 전체 상품에서 드롭다운으로 "최신순" 또는 "좋아요순"을 선택해서 정렬할 수 있습니다.
  • "상품 등록하기" 버튼을 누르면 “/additem” 로 이동합니다 ( 빈 페이지 )
  • 미디어 쿼리를 사용하여 반응형 view 마다 물품 개수를 다르게 보여줍니다 (서버로 요청하는 값은 동일)
  • 베스트 상품
    • Desktop : 4개 보이기
    • Tablet : 2개 보이기
    • Mobile : 1개 보이기
  • 전체 상품
    • Desktop : 10개 보이기
    • Tablet : 6개 보이기
    • Mobile : 4개 보이기

심화

  • 페이지 네이션 기능을 구현합니다.
  • 반응형으로 보여지는 물품들의 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다.

주요 변경사항

  • 스프린트 미션 1부터 4까지의 내용을 React로 변경했습니다

스크린샷

PC Tablet Mobile
localhost_5173_items localhost_5173_items (1) localhost_5173_items (2)

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

huuitae added 30 commits June 18, 2025 21:23
@huuitae huuitae added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jul 31, 2025
@huuitae huuitae requested a review from kiJu2 July 31, 2025 06:05
@huuitae huuitae self-assigned this Aug 1, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 2, 2025

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

Comment on lines +13 to +29
<Routes>
{/* 메인 화면 */}
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
</Route>

{/* 로그인, 회원가입 */}
<Route element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="signup" element={<Signup />} />
</Route>

{/* 중고 마켓 */}
<Route element={<ItemsLayout />}>
<Route path="items" element={<Items />} />
</Route>
</Routes>
Copy link
Collaborator

Choose a reason for hiding this comment

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

굿굿 ! react-router를 적절히 사용하셨군요 !

훌륭합니다. element를 통해 레이아웃을 구성하신 것을 보니 라이브러리 사용 방법도 꼼꼼히 확인하고 적용하신 것 같네요 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

해당 이미지 속의 아이콘을 벡터 이미지를 사용해볼 수 있겠네요 !

이하 MDN
SVG(Scalable Vector Graphics)는 2차원 벡터 그래픽을 서술하는 XML 기반의 마크업 언어입니다. SVG는 텍스트 기반의 열린 웹 표준 중 하나로, 모든 사이즈에서 깔끔하게 렌더링 되는 이미지를 서술하며 CSS, DOM, JavaScript, SMIL (en-US) 등 다른 웹 표준과도 잘 동작하도록 설계됐습니다. SVG는 달리 말하자면 HTML과 텍스트의 관계를 그래픽에 적용한 것입니다

![[Pasted image 20241212111054.png]]
이미지 파일은 최소 단위가 픽셀로 되어있으며 확대했을 때 이미지가 깨질 수 있어요 !
피그마에서 export하실 때에 svgexport할 수 있습니다 😊

Rester vs Vector


꺽쇠 부분의 아이콘을 svg로 저장하고 원 모양의 백그라운드는 border-radius, background-color 등을 통해서 만들어볼 수 있겠어요 😊

Comment on lines +2 to +11
import { memo, useEffect, useState } from 'react';

function DropdownList({ changeOrder }) {
/**
* 드롭다운 리스트 메뉴
*/
const orderList = [
{ name: '최신순', value: 'recent' },
{ name: '좋아요순', value: 'favorite' },
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

orderList는 컴포넌트 내부의 상태나 props 등에 의존하지 않는군요 !

Suggested change
import { memo, useEffect, useState } from 'react';
function DropdownList({ changeOrder }) {
/**
* 드롭다운 리스트 메뉴
*/
const orderList = [
{ name: '최신순', value: 'recent' },
{ name: '좋아요순', value: 'favorite' },
];
import { memo, useEffect, useState } from 'react';
/**
* 드롭다운 리스트 메뉴
*/
const orderList = [
{ name: '최신순', value: 'recent' },
{ name: '좋아요순', value: 'favorite' },
];
function DropdownList({ changeOrder }) {

해당 값은 컴포넌트 내부의 상태나 props 등에 의존하지 않으므로 컴포넌트 바깥에 선언하시면 DropdownList가 리렌더링 될 때 불필요한 재선언을 방지할 수 있습니다 !
또한, 컴포넌트 내부에는 상태와 관련된 코드만 남게 되므로 코드 분리에도 용이하겠죠? 😉

Comment on lines +20 to +25
/**
* 드롭다운 리스트 열림/닫힘 이벤트
*/
const onClickOpen = () => {
setIsOpen(!isOpen);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

다음과 같이 updater function을 사용해볼 수 있습니다 😉

Suggested change
/**
* 드롭다운 리스트 열림/닫힘 이벤트
*/
const onClickOpen = () => {
setIsOpen(!isOpen);
};
/**
* 드롭다운 리스트 열림/닫힘 이벤트
*/
const onClickOpen = () => {
setIsOpen(prev => !prev);
};

이 전의 상태를 참고할 수 있습니다 !

orderList.map((menu) => {
return (
<li
key={menu.name}
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 +48 to +51
/**
* 화살표 모양으로 다음 페이지로 이동할 때 이벤트
* @param {event} e
*/
Copy link
Collaborator

Choose a reason for hiding this comment

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

jsdoc을 잘 사용하고 계시네요 😊

근데 너무 많이 사용하시면 휘태님도 힘드실 것 같아요 !
"자주 사용되는 공통 함수(컴포넌트)"에만 작성해도 충분하지 않을까 싶어요 !

물론 ! 주석을 꼼꼼히 작성하시는건 좋은 자세입니다 😊

Comment on lines +7 to +31
/**
* mediaQuery에 반응하여 데이터를 반환한다.
*/
export default function useMediaQuery(query) {
/**
* 반환할 데이터를 담아둘 state
*/
const [isMatch, setIsMatch] = useState(() => getMatches(query));

useEffect(() => {
const media = window.matchMedia(query);

const handleChange = (e) => {
setIsMatch(e.matches);
};

media.addEventListener('change', handleChange);

return () => {
media.removeEventListener('change', handleChange);
};
}, [query]);

return isMatch;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 깔끔하고 멋진 훅입니다 👍👍

import ic_kakao from '../../assets/icons/ic_kakao.png';
import ic_google from '../../assets/icons/ic_google.png';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
Copy link
Collaborator

Choose a reason for hiding this comment

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

오잉? react-hook-form까지 사용하셨군요? 🫢

Comment on lines +6 to +10
const requestProductList = async (
orderBy = 'recent',
page = 1,
pageSize = 10
) => {
Copy link
Collaborator

@kiJu2 kiJu2 Aug 2, 2025

Choose a reason for hiding this comment

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

(제안) 파라메터를 객체로 나타내면 순서에 상관 없이 사용할 수 있겠네요 !

이 방식도 문제는 없지만 일부 값만 전달할 때도 위치를 맞춰야 해서 사용성이 떨어질 수 있겠어요 😉

Suggested change
const requestProductList = async (
orderBy = 'recent',
page = 1,
pageSize = 10
) => {
const requestProductList = async ({
orderBy = 'recent',
page = 1,
pageSize = 10,
} = {}) => {

혹은

Suggested change
const requestProductList = async (
orderBy = 'recent',
page = 1,
pageSize = 10
) => {
/**
* 상품 목록을 서버에서 요청합니다.
*
* @param {Object} query - 상품 요청에 필요한 쿼리 파라미터입니다.
* @param {string} [query.orderBy='recent'] - 정렬 기준 (: 'recent', 'popular' ).
* @param {number} [query.page=1] - 요청할 페이지 번호.
* @param {number} [query.pageSize=10] - 페이지당 항목 .
* @returns {Promise<Object>} 서버로부터 받아온 상품 목록 데이터.
*/
const requestProductList = async (query) => {

orderBy,page, pageSize는 쿼리에 속하는 개념이므로 객체로 표현해도 좋겠어요 😉

Comment on lines +11 to +14
const url = new URL('https://panda-market-api.vercel.app/products');
url.searchParams.append('page', page);
url.searchParams.append('pageSize', pageSize);
url.searchParams.append('orderBy', orderBy);
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 훌륭합니다. searchParams를 사용하셨군요 👍👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 2, 2025

수고하셨습니다 휘태님 !!
리액트를 능숙하게 잘 구성하신게 느껴지네요 👍👍
적절한 상황에 적절한 라이브러리(react-hook-form)를 사용하신 점도 인상깊습니다 😉

미션 수행하시느라 수고 많으셨습니다 휘태님 !

@kiJu2 kiJu2 merged commit 20ca404 into codeit-bootcamp-frontend:React-황휘태 Aug 2, 2025
Comment on lines +50 to +105
/**
* 베스트 상품 목록을 가져온다.
*/
const getBestProducts = async (count) => {
try {
const { list: bestList } = await requestProductList('favorite', 1, count);
if (!bestList) {
throw new Error('베스트 상품 목록 데이터를 불러오지 못했습니다.');
}
setBestProducts(bestList);
} catch (e) {
console.error(e);
} finally {
setisLoading(false);
}
};

/**
* 전체 상품 목록을 가져온다.
*/
const getProducts = async (order, page, count) => {
try {
const productList = await requestProductList(order, page, count);

if (!productList) {
throw new Error('상품목록 데이터를 불러오지 못했습니다.');
}
setProducts(productList);
} catch (e) {
console.error(e);
} finally {
setisLoading(false);
}
};

useEffect(() => {
/**
* 현재 뷰포트에 맞추어 count를 담을 객체
*/
let currentItemCounts;

if (isMobile) {
// 모바일 뷰
currentItemCounts = { best: 1, all: 4 };
} else if (isTablet) {
// 태블릿 뷰
currentItemCounts = { best: 2, all: 6 };
} else {
// 데스크탑 뷰
currentItemCounts = { best: 4, all: 10 };
}

setItemsCounts(currentItemCounts);
getProducts(order, pageNum, currentItemCounts.all);
getBestProducts(currentItemCounts.best);
}, [isTablet, isMobile, order, pageNum]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

상품 목록을 불러오는 로직에서 useEffect()의 의존성 배열로 페이지의 번호를 넣어주고 있습니다.
따라서 페이지를 넘길 때 마다 데이터를 새로 요청하게 됩니다.
이런 방식은 큰 규모의 프로젝트 일 때는 성능에 대한 이슈가 있을거라 생각되는데, 현업에서는 어떤 방식으로 처리하는지 궁금합니다.

Comment on lines +1 to +31
import { useEffect, useState } from 'react';

const getMatches = (query) => {
return window.matchMedia(query).matches;
};

/**
* mediaQuery에 반응하여 데이터를 반환한다.
*/
export default function useMediaQuery(query) {
/**
* 반환할 데이터를 담아둘 state
*/
const [isMatch, setIsMatch] = useState(() => getMatches(query));

useEffect(() => {
const media = window.matchMedia(query);

const handleChange = (e) => {
setIsMatch(e.matches);
};

media.addEventListener('change', handleChange);

return () => {
media.removeEventListener('change', handleChange);
};
}, [query]);

return isMatch;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

반응형에 맞추어 보여지는 물품의 개수를 다르게 설정하기 위해 커스텀 훅을 만들어 사용했습니다.
실제 현업에서 이러한 상황은 어떤 방식으로 처리하는지 궁금합니다.

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