Skip to content

Conversation

@gummmmmy0v0
Copy link

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

심화

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

주요 변경사항

  • sprint2까지 수행 후 sprint5 바로 수행하였습니다.

스크린샷

스크린샷 2025-08-11 오전 12 22 59 스크린샷 2025-08-11 오전 12 23 18 스크린샷 2025-08-11 오전 12 23 58

멘토에게

  • 지금 상황에서 버튼을 따로 컴포넌트로 만드는 건 비효율적인가요?
  • 폴더랑 파일이 많아지면서 조금씩 헷갈리고 있는데 혹시 이럴 때 팁이나 좋은 정리 방법이 있을까요?
  • 피그마 디자인을 보면 모바일 뷰어에서
    (윗줄: 타이틀, 상품 등록 버튼)
    (아랫줄: 검색창, 소트 버튼) 으로 구현되어 있는데 이 부분을 어떻게 만들 수 있을까요...?
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@gummmmmy0v0 gummmmmy0v0 requested a review from kiJu2 August 10, 2025 15:25
@gummmmmy0v0 gummmmmy0v0 self-assigned this Aug 10, 2025
@gummmmmy0v0 gummmmmy0v0 added the 순한맛🐑 마음이 많이 여립니다.. label Aug 10, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

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

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

지금 상황에서 버튼을 따로 컴포넌트로 만드는 건 비효율적인가요?

버튼 컴포넌트를 따로 만드는건 흔한 방법입니다 ! 만약 안만들어두셨다면 버튼 컴포넌트를 만드시는게 좋겠네요 😉

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

폴더랑 파일이 많아지면서 조금씩 헷갈리고 있는데 혹시 이럴 때 팁이나 좋은 정리 방법이 있을까요?

폴더 구조는 정말 다양합니다 ! 다음 문서 참조하시면 도움이 될 것 같네요 😉
네이버는 폴더구조를 어떻게 구성 하고있을까

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

피그마 디자인을 보면 모바일 뷰어에서
(윗줄: 타이틀, 상품 등록 버튼)
(아랫줄: 검색창, 소트 버튼) 으로 구현되어 있는데 이 부분을 어떻게 만들 수 있을까요...?


다음과 같은 상황으로 보입니다 ! :
Desktop:

[Header][검색창Input][상품등록하기버튼][솔트드롭다운]

Mobile:

[Header][상품등록하기버튼]
[검색창Input][솔트드롭다운]

이 때, grid를 활용해볼 수 있어요 😊

다음과 같이 작성해볼 수 있습니다 !:

const Layout = styled.header`
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  grid-template-areas: 
    "header search add dropdown";
  gap: 12px;
  align-items: center;

  @media (max-width: 768px) {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      "header   add"
      "search   dropdown";
    gap: 8px 10px;
  }
`;

const Header = styled.div`grid-area: header;`;
const Search = styled.div`grid-area: search;`;
const AddBtn = styled.div`grid-area: add;`;
const SortDropdown = styled.div`grid-area: dropdown;`;

Comment on lines +9 to +24
function App() {
return (
<BrowserRouter>
<Header />
<div className="withHeader">
<Routes>
<Route index element={<HomePage />} />
<Route path="community" element={<CommunityPage />} />
<Route path="items" element={<MarketPage />} />
<Route path="profile" element={<ProfilePage />} />
<Route path="additem" element={<AddItemPage />} />
</Routes>
</div>
</BrowserRouter>
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

react-routes-dom에서 레이아웃을 설정할 수 있어요. 😊

Consider: 판다마켓 초기에 작성한 로그인과 회원가입과 같은 페이지는 Header가 없는 디자인이예요. 지금과 같이 한다면 특정 페이지의 레이아웃을 조정하기 어려울거예요 !

Routes에서 element를 지정하여 중첩 레이아웃을 사용할 수 있으니 참고해서 설계해보세요 😊

tl;dr

// App.tsx
<Routes>
  <Route path="/" element={<Main />}> // 중첩 라우팅
    <Route path="user-management" element={<UserManagement />} />
    <Route path="child-management" element={<ChildManagement />} />
  </Route>
  <Route path="/login" element={<Login />} />
</Routes>
// Main.tsx
<MainWrapper>
  <MainMenu />
  <Outlet /> // children과 같은 효과 ! ✨
</MainWrapper>

[React] React-router 'Outlet' 사용하여 레이아웃 구성하기

react-routes-dom/layout

Comment on lines +1 to +2
export async function getProducts(params = {}) {
const query = new URLSearchParams(params).toString();
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 함수가 분리되어 있으니 재사용 및 유지관리가 용이하겠어요 👍

@@ -0,0 +1,12 @@
export async function getProducts(params = {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 params는 어떤 매개변수를 가질지 모르겠군요 !

다음과 같이 만들어볼 수 있을거예요 😉:

Suggested change
export async function getProducts(params = {}) {
export async function getProducts({
page = 1,
pageSize = 10,
orderBy = "recent",
keyword = "",
}) {

Copy link
Collaborator

Choose a reason for hiding this comment

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

(더 나아가서)혹은 jsdoc을 활용해볼 수도 있습니다 !

/**
 * 상품 목록을 서버에서 가져옵니다.
 *
 * @param {Object} params - 상품 조회에 필요한 파라미터
 * @param {number} [params.page=1] - 조회할 페이지 번호 (기본값: 1)
 * @param {number} [params.pageSize=10] - 한 페이지에 표시할 상품 수 (기본값: 10)
 * @param {"recent"|"popular"|"price"} [params.orderBy="recent"] - 상품 정렬 기준 (기본값: "recent")
 * @param {string} [params.keyword=""] - 검색 키워드 (기본값: 빈 문자열)
 * @returns {Promise<{
  "totalCount": 0,
  "list": [
    {
      "createdAt": "2025-08-11T07:00:52.930Z",
      "favoriteCount": 0,
      "ownerNickname": "string",
      "ownerId": 1,
      "images": [
        "https://example.com/..."
      ],
      "tags": [
        "전자제품"
      ],
      "price": 0,
      "description": "string",
      "name": "상품 이름",
      "id": 1
    }
  ]
}>} 서버로부터 받은 상품 데이터 객체
 *
 * @example
 * // 최근 등록된 상품 10개를 가져오기
 * const products = await getProducts({ page: 1, pageSize: 10, orderBy: "recent" });
 */
export async function getProducts({
  page = 1,
  pageSize = 10,
  orderBy = "recent",
  keyword = "",
}) {
  // ...
}

그래서 JSDoc이 뭔가요?

JSDoc은 다음과 같이 작성할 수 있어요:

  /**
   * SMS를 전송합니다.
   * @param {string} title - SMS 제목 (선택 사항)
   * @param {string} text - SMS 내용
   * @param {string} to - 수신자 전화번호
   * @returns {Promise} SMS 전송 결과를 포함한 객체
   */
  sendSMS(title, text, to);

이렇게 하면 Visual Studio Code에서 해당 함수를 호출할 때 매개변수가 어떤 것이 있는지도 볼 수 있으며 마우스를 호버하거나 함수가 포커스 되었을 때 JSDoc을 볼 수도 있습니다 😊
개발자 경험이 많이 좋아지겠죠?

JSDoc 공식문서

const query = new URLSearchParams(params).toString();

const response = await fetch(
`https://panda-market-api.vercel.app/products?${query}`
Copy link
Collaborator

Choose a reason for hiding this comment

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

https://panda-market-api.vercel.app는 base url로 따로 관리해볼 수 있겠네요 !

Suggested change
`https://panda-market-api.vercel.app/products?${query}`
const BASE_URL = 'https://panda-market-api.vercel.app';
// ...
`${baseUrl}/products?${query}`

하지만 위 방법 보다는 환경 변수를 사용하시는걸 추천드립니다 !

환경 변수(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을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

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

export async function getProducts(params = {}) {
const query = new URLSearchParams(params).toString();

const response = await fetch(
Copy link
Collaborator

Choose a reason for hiding this comment

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

(제안/선택)axios를 사용해보는건 어떨까요?

현재 fetch를 그대로 사용해도 동작은 하겠으나, 반복되는 baseURLfetch로 받은 response에 대한 공통적인 후처리, request 보내기 전 공통적인 처리 등 또한 고려해볼 수 있을거예요.
다음 사항들을 고려해볼 수 있어요:

  1. 만약 get이 아닌 메써드(post, patch, delete 등)일 경우는 어떻게 처리할 수 있을까요?
  2. querybody가 필요할 때는 어떻게 처리 할 수 있을까요?
  3. 로그인 인가를 위한 토큰을 request 전에 자동으로 삽입할 수는 없을까요? (인증/인가를 자동으로 할 수 없을까요?)
  4. 처음 한 번에 Base URL을 지정할 수는 없을까요?
    1. Base URL을 사용하다가 다른 엔드포인트에 요청해야 할 때는 어떻게 할 수 있을까요?
      이 모든 요구사항들을 반영한 아키텍쳐를 구상하기는 어렵습니다. 따라서 이 모든 요구사항이 잘 만족하는 http client를 사용해보시는건 어떨까요 ?

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

사용 방법 🚀

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

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

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

axios API

Comment on lines +4 to +6

const PaginationBar = ({ totalPageNum, activePageNum, onPageChange }) => {
const maxVisiblePages = 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
const PaginationBar = ({ totalPageNum, activePageNum, onPageChange }) => {
const maxVisiblePages = 5;
const MAX_VISIBLE_PAGES = 5;
const PaginationBar = ({ totalPageNum, activePageNum, onPageChange }) => {

좀 더 상세히 말하면 컴포넌트 내부 자원(props, 혹은 상태)을 사용하지 않는다면 외부에 선언해볼 수 있습니다. 😉
이렇게하면 리렌더링 시 불필요한 재선언을 방지할 수 있으며 최종적으로 컴포넌트 내부에는 컴포넌트 자원을 사용하는 코드가 남게 됩니다 👏

Comment on lines +10 to +12
const sortDropdown = () => {
setIsSortVisible(!isSortVisible); //true면 false로, false면 true로
};
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 sortDropdown = () => {
setIsSortVisible(!isSortVisible); //true면 false로, false면 true로
};
const sortDropdown = () => {
setIsSortVisible(prev => !prev); //true면 false로, false면 true로
};

위와 같이 사용해볼 수 있어요 😉

return (
<div>
<div className="allContainer">
<h1 className="allTitle">전체 상품</h1>
Copy link
Collaborator

Choose a reason for hiding this comment

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

<h1> 태그는 문서에 하나만 넣는게 어떨까요?

현재 여러 컴포넌트에서 h1 태그가 여럿 보이는군요 ! 🤔

이하 MDN 발췌

페이지 당 하나의 <h1>만 사용하세요. 여러 개를 써도 오류는 나지 않겠지만, 단일 <h1>이 모범 사례로 꼽힙니다. 논리적으로 생각했을 때도, <h1>은 가장 중요한 제목이므로 전체 페이지의 목적을 설명해야 할 것입니다. 두 개의 제목을 가진 책이나, 여러 개의 이름을 가진 영화는 볼 수 없죠! 또한 스크린 리더 사용자와 SEO에도 더 적합합니다.

MDN <h1>-<h6>: HTML 구획 제목 요소

<nav>
<ul>
<li>
<NavLink to="/community" style={getLinkStyle}>
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
<NavLink to="/community" style={getLinkStyle}>
<NavLink to="/community" style={{ color: isActive ? 'var(--blue)' : undefined }}>

@kiJu2
Copy link
Collaborator

kiJu2 commented Aug 11, 2025

수고하셨습니다 인화님 !
정말 오래간만이네요 ㅎㅎㅎ
꾸준히 잘 제출하시다가 안보여서 리액트 하시는 데에 어려움이 많았나.. 했습니다
근데 과제 보니까 정말 잘 적응하셔서 안심이네요 👍

미션 수행하시느라 정말 수고 많으셨습니다 !
첫 협업프로젝트네요 ! 인화님의 역량이 팀원들에게 큰 도움이 될겁니다 ! 💪

@kiJu2 kiJu2 merged commit aec9f92 into codeit-bootcamp-frontend:React-유인화 Aug 11, 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