Skip to content

Conversation

@hyunhwa21
Copy link
Collaborator

요구사항

배포 : https://poetic-fenglisu-a8fdd0.netlify.app/items

기본

  • [x]Github에 PR(Pull Request)을 만들어서 미션을 제출합니다.
  • [x]피그마 디자인에 맞게 페이지를 만들어 주세요.
  • [x]React를 사용합니다

중고마켓

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

중고마켓 반응형

베스트 상품

  • [x]Desktop : 4개 보이기
  • [x]Tablet : 2개 보이기
  • [x]Mobile : 1개 보이기
    전체 상품
  • [x]Desktop : 12개 보이기
  • [x]Tablet : 6개 보이기
  • [x]Mobile : 4개 보이기중고마켓

심화

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

주요 변경사항

스크린샷

멘토에게

image

배포 후 리퀘스트를 확인해보니 200코드와 304코드가 함께 오는데, 무엇 때문인지 이유를 모르겠습니다.

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

@hyunhwa21 hyunhwa21 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Aug 28, 2025
@hyunhwa21 hyunhwa21 changed the title React-김현화-sprint5 [김현화] sprint5 Aug 29, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 30, 2025

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

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 30, 2025

배포 후 리퀘스트를 확인해보니 200코드와 304코드가 함께 오는데, 무엇 때문인지 이유를 모르겠습니다.

해당 요청은 브라우저가 캐싱한 데이터를 보여주는 것으로 보입니다. 😉
image

이렇게 캐시 비활성화 시키면 200으로 받는 것을 볼 수 있습니다 !

하나는 200, 하나는 304가 되는 것은 요청을 두 번 보내고 있어서 그런 것으로 보여요 😊

Comment on lines +6 to +9
const [isLogin, setIsLogin] = useState(true);
return (
<>
<Header isLogin={isLogin} />
Copy link
Collaborator

Choose a reason for hiding this comment

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

오호 나중을 대비해서 일단 isLogin을 만들어둔건가 보군요? 😉

HeaderisLogin(혹은 user)을 받아두고 있으면 나중이 인증 인가 할 때 편하겠네요 👍

import axios from "axios";

//베스트 상품
const BASE_URL = "https://panda-market-api.vercel.app/products?page=";
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 +7 to +9
const response = await axios.get(
`${BASE_URL}1&pageSize=${pageSize}&orderBy=favorite`
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

쿼리는 URLSearchParams로 손쉽게 사용할 수 있어요 !

    const params = new URLSearchParams({ pageSize });
    const { data } = await instance.get(`${BASE_URL}`, { params });

axios를 사용하실 경우 URLSearchParams와 함께 객체로 손쉽게 핸들링할 수 있습니다 !
객체로 구성할 수 있어 가독성이 좋고, URL 인코딩을 자동으로 처리하여 특수 문자나 공백이 포함된 값에서도 안전하게 동작합니다 !

URLSearchParams: URLSearchParams 인터페이스는 URL의 쿼리 문자열을 대상으로 작업할 수 있는 유틸리티 메서드를 정의합니다.

쿼리를 생성하실 때에 참고해서 사용해보세요 😊

Comment on lines +19 to +21
const response = await axios.get(
`${BASE_URL}${page}&pageSize=${pageSize}&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.

axios를 직접 사용하고 계시군요 !

axios를 직접 사용하는 것도 현재 기능상 문제가 없으나 이렇게 되면 BASE_URL을 통신 할 때마다 계속해서 불러와야되겠어요 !

그럼 어떻게? 🤔

instance를 만들어서 export를 하고 사용해보실 수 있어요. axios-instance 파일(다른 이름도 됩니다 !)을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.BASE_URL; // 왠만하면 `path`를 제외한 BASE URL로 저장하면 좋아요 ! (= `https://panda-market-api.vercel.app`)

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance

그리고 다음과 같이 사용해볼 수 있어요 😉

Suggested change
const response = await axios.get(
`${BASE_URL}${page}&pageSize=${pageSize}&orderBy=${orderBy}`
);
import instance from '...';
// ...
const response = await instance.get(
`/products?page=${page}&pageSize=${pageSize}&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.

(선택/제안) 무언가 공통적으로 사용될 것 같은 컴포넌트군요 ?

여러 컴포넌트에서 사용될 것 같다면 src/components/common/Container.jsx 혹은src/components/common/Container/index.jsx와 같이 디렉토리를 구분해도 될 것 같군요 🤔😉


라고 적고 확인해보니 사용을 안하고 있었네요 😅
파일 구조 컨벤션에 따라 다를 수 있으나 두 곳 이상에서 사용될 것 같으면 common으로 영역을 구분한다 ! 정도로만 받아들여주시면 좋겠습니다 !

Comment on lines +5 to +9

function Pagination({ onPageLoad, totalCount, itemListLength }) {

//페이지네이션 최대 생성 갯수
const LIST_MAX = 5;
Copy link
Collaborator

Choose a reason for hiding this comment

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

LIST_MAX의 경우 상수로 보입니다 !

Suggested change
function Pagination({ onPageLoad, totalCount, itemListLength }) {
//페이지네이션 최대 생성 갯수
const LIST_MAX = 5;
//페이지네이션 최대 생성 갯수
const LIST_MAX = 5;
function Pagination({ onPageLoad, totalCount, itemListLength }) {

해당 값은 리렌더링 시 불필요한 선언이 될 수 있으므로 컴포넌트 바깥에서 선언해볼 수 있습니다 !
이렇게 하다보면 컴포넌트 안에는 상태나 props를 참조하는 것들만 남게 되겠죠? 😉

Comment on lines +15 to +21
const paginationArr = (startPage, maxPages) => {
const pages = [];
for (let i = 0; i < maxPages; i++) {
pages.push(startPage + i);
}
return pages;
};
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 +63 to +77
// 페이지네이션 렌더링
const renderPages = () => {
return pages.map((number) => (
<li key={number}>
<button
className={`${wrap.button} ${
currentPage === number ? wrap.active : ""
}`}
onClick={() => handlePageClick(number)}
>
{number}
</button>
</li>
));
};
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 renderPages = () => {
return pages.map((number) => (
<li key={number}>
<button
className={`${wrap.button} ${
currentPage === number ? wrap.active : ""
}`}
onClick={() => handlePageClick(number)}
>
{number}
</button>
</li>
));
};
// 페이지네이션 렌더링
const PageNumbers = ({ onClick }) => {
return pages.map((number) => (
<li key={number}>
<button
className={`${wrap.button} ${
currentPage === number ? wrap.active : ""
}`}
onClick={onClick}
>
{number}
</button>
</li>
));
};

그리고 onClick을 매개변수로 받아볼 수 있을 것 같아요 !
(이어서..)

<button className={wrap.button} onClick={handlePrevClick}>
<img src={leftArrowIcon} alt="이전페이지" />
</button>
<ul>{renderPages()}</ul>
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
<ul>{renderPages()}</ul>
<ul>
<PageNumbers onClick={handlePageClick} />
</ul>

Comment on lines +61 to +69
useEffect(() => {
handleListLoad({ page, pageSize: itemListLength, orderBy });
}, [page, orderBy, isTABLET, isMOBILE]);

//선택한 페이지 리스트 불러오기
const handlePageLoad = (pageNumber) => {
setPage(pageNumber);
handleListLoad({ page: pageNumber, pageSize: itemListLength, orderBy });
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

이미 useEffect에서 page가 변경될 때 통신을 하고 있습니다 !

Suggested change
useEffect(() => {
handleListLoad({ page, pageSize: itemListLength, orderBy });
}, [page, orderBy, isTABLET, isMOBILE]);
//선택한 페이지 리스트 불러오기
const handlePageLoad = (pageNumber) => {
setPage(pageNumber);
handleListLoad({ page: pageNumber, pageSize: itemListLength, orderBy });
};
useEffect(() => {
handleListLoad({ page, pageSize: itemListLength, orderBy });
}, [page, orderBy, isTABLET, isMOBILE]);
//선택한 페이지 리스트 불러오기
const handlePageLoad = (pageNumber) => {
setPage(pageNumber);
};

따라서 handlePageLoad에서 handleListLoad는 제거해주셔도 될 것 같아요 😊
만약 handleListLoad가 있다면

  1. 다른 페이지를 클릭한다.
  2. handlePageLoad에 의해 handleListLoad이 호출된다. (통신)
  3. setPage에 의해 page가 변경된다.
  4. page를 의존하고 있는 useEffect가 실행되며 handleListLoad가 실행 된다. (통신)

이렇게 중복 통신이 되는 원인으로 보입니다 !

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 30, 2025

크으 ~ 수고하셨습니다 현화님 !!
미션 5 언제 하시나 했는데 기초 프로젝트 끝나자마자 곧바로 하셨군요 ! 😊😊😊

순수하게 직접 탐구하시면서 작성하신 것으로 보여서 정말 뿌듯하게 지켜봤습니다 !
금방 금방 성장하실 것으로 기대됩니다 ! 👍👍

궁금하신 점 있으시면 DM 주세요 현화님 ! (타입스크립트도 화이팅 ! 👏)

@kiJu2 kiJu2 merged commit c942d77 into codeit-bootcamp-frontend:React-김현화 Aug 30, 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.

3 participants