Skip to content

Conversation

@wontak12
Copy link

@wontak12 wontak12 commented Aug 6, 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개 보이기

심화

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

주요 변경사항

스크린샷

image

멘토에게

페이지네이션은 혼자 못해서 검색과 ai를 활용해 하였습니다

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

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 8, 2025

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

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 8, 2025

페이지네이션은 혼자 못해서 검색과 ai를 활용해 하였습니다

페이지네이션 처음 개발하려 하면 어렵죠 ㅎㅎㅎ. 결국 문제 해결할 방법을 찾아서 해결하셨다는 점은 훌륭합니다 !

다만 AI로 산출된 코드는 모두 이해하셔야 합니다 !
AI와 인간 개발자의 큰 차이는 "책임을 질 수 있는 주체"의 유무입니다 !
AI로 산출된 코드는 검토 과정이 필요하며, 검토를 하기 위해서는 산출된 코드에 대한 이해가 선수적으로 이루저야 할거예요. 😊

또한 가장 권장드리고 싶은건 문제를 직접 해결해보시고 AI가 필요하다면 디버깅 방법을 물어보시는 것도 좋은 방법입니다. 😉

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 8, 2025

앞으로 제출하실 때 PR 이름은 [{본인이름}] Sprint{N}로 제출해주시면 감사드리겠습니다 😉

Comment on lines +9 to +18

function Items() {
const [order, setOrder] = useState("createdAt");
const [fourItems, setFourItems] = useState([]);
const [items, setItems] = useState([]);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemPerPage] = useState(10); // 한 페이지에 10개
const [bestItemPerPage, setBestItemPerPage] = useState(4);
const pageGroupSize = 5; // 페이지 버튼 5개씩
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 Items() {
const [order, setOrder] = useState("createdAt");
const [fourItems, setFourItems] = useState([]);
const [items, setItems] = useState([]);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemPerPage] = useState(10); // 한 페이지에 10개
const [bestItemPerPage, setBestItemPerPage] = useState(4);
const pageGroupSize = 5; // 페이지 버튼 5개씩
const PAGE_GROUP_SIZE = 5;
function Items() {
const [order, setOrder] = useState("createdAt");
const [fourItems, setFourItems] = useState([]);
const [items, setItems] = useState([]);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemPerPage] = useState(10); // 한 페이지에 10개
const [bestItemPerPage, setBestItemPerPage] = useState(4);

컴포넌트 내부에 있으면 리렌더링에 의한 불필요한 재선언이 될 수 있으며, 해당 컴포넌트의 자원을 사용하지 않는 함수 및 변수를 밖에서 선언해준다면 자연스럽게 컴포넌트는 필요한 자원들만 구성되게 됩니다. 😊

Comment on lines +23 to +49
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 768) {
console.log("📱 모바일");
setItemPerPage(4);
setBestItemPerPage(1);
setCurrentPage(1);
} else if (window.innerWidth < 1025) {
console.log("📲 태블릿");
setItemPerPage(6);
setBestItemPerPage(2);
setCurrentPage(1);
} else {
console.log("💻 데스크탑");
setItemPerPage(10);
setBestItemPerPage(4);
setCurrentPage(1);
}
};

handleResize(); // 최초 실행
window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
Copy link
Collaborator

Choose a reason for hiding this comment

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

(심화/응용) 해당 useEffecthook을 활용해볼 수도 있겠군요 ! 😉

import { useEffect, useState } from "react";

export default function useResponsivePagination() {
  const [config, setConfig] = useState({
    itemPerPage: 10,
    bestItemPerPage: 4,
  });

  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth < 768) {
        setConfig({ itemPerPage: 4, bestItemPerPage: 1 });
      } else if (window.innerWidth < 1025) {
        setConfig({ itemPerPage: 6, bestItemPerPage: 2 });
      } else {
        setConfig({ itemPerPage: 10, bestItemPerPage: 4 });
      }
    };

    handleResize(); // 최초 실행
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return config;
}

이렇게 '디바이스에 따라서' 페이지의 최대값을 반환하는 훅을 만들어볼 수 있겠어요 !

Copy link
Collaborator

Choose a reason for hiding this comment

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

(심화/더 나아가서) 더 나아가서 useDevice를 만들어볼 수도 있겠네요 !

리액트에서 현재 어떤 디바이스인지 알 수 있는 훅도 만들어볼 수 있을 것 같아요:

// hooks/useDevice.ts
import { useEffect, useState } from "react";

type DeviceType = "mobile" | "tablet" | "desktop";

const getDeviceType = (width: number): DeviceType => {
  if (width < 768) return "mobile";
  if (width < 1024) return "tablet";
  return "desktop";
};

export default function useDevice(): DeviceType {
  const [device, setDevice] = useState<DeviceType>(getDeviceType(window.innerWidth));

  useEffect(() => {
    const handleResize = () => {
      setDevice(getDeviceType(window.innerWidth));
    };

    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return device;
}

그리고 제안한 코드를 조금 수정해볼 수 있겠어요:

// hooks/useResponsivePagination.ts
import useDevice from "./useDevice";

export default function useResponsivePagination() {
  const device = useDevice();

  const config = {
    mobile: { itemPerPage: 4, bestItemPerPage: 1 },
    tablet: { itemPerPage: 6, bestItemPerPage: 2 },
    desktop: { itemPerPage: 10, bestItemPerPage: 4 },
  };

  return config[device];
}

이렇게 하면 useDevice의 재사용성도 더 좋겠네요. 😉😉

Comment on lines +64 to +67
fetch(
`https://panda-market-api.vercel.app/products?limit=${itemsPerPage}&page=${currentPage}`,
{ method: "GET" }
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

baseUrl을 설정하면 더욱 유지보수가 좋게 코드를 관리할 수 있겠군요 !

Suggested change
fetch(
`https://panda-market-api.vercel.app/products?limit=${itemsPerPage}&page=${currentPage}`,
{ method: "GET" }
)
fetch(
`${baseUrl}/products?limit=${itemsPerPage}&page=${currentPage}`,
{ method: "GET" }
)

위와 같은 설계가 된다면 더욱 좋을 것 같아요 !
상수로 관리할 수도 있고, 환경 변수로도 관리할 수 있을거예요.

환경 변수(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와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

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 시작하기

어떻게 세팅하면 될까? 🤔

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

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

export default instance

axios instance

인가에 필요한 accessTokenlocalStorage가 있다면 axios의 인터셉터를 활용할 수 있습니다 !

인터셉터는 혼자 해결해보시는 것을 권장드립니다. 혹시 모르시겠으면 다음 위클리 미션에 질문해주세요. 😊

사용 방법 🚀

사용 방법은 정말 간단해요. 다음과 같이 사용할 수 있습니다:

instance.get(`/user/${userId}`)

딱 보니. 마이그레이션도 정말 쉽게 할 수 있겠죠? 😊

axios API

Copy link
Collaborator

Choose a reason for hiding this comment

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

또한 보통 api 함수들은 따로 파일을 분류하여 사용하는게 일반적입니다 ! 😊

프로젝트에서는 API 호출 로직을 별도의 파일로 분리하여 관리하는 것이 일반적이예요. 이렇게 하면 컴포넌트, 페이지, 훅 등 어디든 사용될 수 있겠죠?😊
코드의 재사용성을 높이고, 유지보수가 쉬워질 수 있습니다 ! API 함수를 모듈화하여 사용하면 코드가 더 깔끔하고 읽기 쉬워집니다. 다음은 프로젝트의 디렉토리 구조와 API 함수 예제입니다:

// src/services/apis/production.api.js (예시입니다 !)
export const getProductions = async () => {
	try {
		const { data } = await axios.get('/productions');
		return data;
	} catch(error) {
		throw error;
	}
};

Comment on lines +161 to +163
header {
padding: 0 24px;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

global.css는 모든 페이지에 스타일이 적용되는 것으로 보여요 !

그런데 header 태그는 한 문서에 여러개 작성될 수 있어요.
따라서 태그 선택자보다는 클래스 선택자로 작성해볼 수 있을거예요.

또한, 글로벌로 만들지 않고 필요한 헤더 컴포넌트에서 import 시키는 방법도 지역적으로 관리하기 용이할 수 있습니다 😉

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 8, 2025

수고하셨습니다 원탁님 !
어려운 부분도 포기하지 않고 끝까지 해결해내시는 모습 정말 멋집니다 ! 💪💪
꾸준히 미션 수행해내시는 모습이 정말 멋집니다.

기초 프로젝트 시작인데 기초프로젝트에서는 어떤 모습을 보여주실지 기대가 됩니다 😊😊

@kiJu2 kiJu2 merged commit 6161b9c into codeit-bootcamp-frontend:React-김원탁 Aug 8, 2025
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants