Skip to content

Conversation

@DreamPaste
Copy link
Collaborator

@DreamPaste DreamPaste commented May 26, 2025

배포 링크

https://gentle-lamington-bbd035.netlify.app/items

요구사항

  • 중고마켓 페이지 주소는 “/items” 입니다.
  • 페이지 주소가 “/items” 일때 상단네비게이션바의 “중고마켓" 버튼의 색상은 “3692FF”입니다.
  • 상단 네비게이션 바는 이전 미션에서 구현한 랜딩 페이지와 동일한 스타일로 만들어 주세요.
  • 전체 상품에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다.
  • 베스트 상품의 정렬 기준은 favorit, favorit이 가장 높은 상품 4가지를 표시합니다.
  • ‘상품 등록하기’ 버튼을 누르면 “/additem” 로 이동합니다. ( 빈 페이지 )
  • 카드 데이터는 제공된 백엔드 API 페이지의 GET 메소드인 “/products”를 사용해주세요.
  • 미디어 쿼리를 사용하여 반응형 view 마다 물품 개수를 다르게 보여줍니다 (서버로 요청하는 값은 동일)
  • 페이지 네이션 기능을 구현합니다.
  • 반응형으로 보여지는 물품들의 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다.

구현

1. 프로젝트 환경 세팅

  • Vite + TypeScript로 초기화

  • ESLint, Stylelint, Prettier, Husky 연동

2. 라우팅 구조 잡기

  • / (랜딩 페이지)

  • /board (자유게시판)

  • /items (중고마켓)

  • /additem (상품등록 빈 페이지)

3. 글로벌 인증 컨텍스트

  • AuthProvider 생성: user, isLoggedIn 상태 관리

  • useAuth 훅으로 상태 접근 및 setUser 제공

  • useAuthService 훅으로 로그인·로그아웃 비즈니스 로직 분리

4. 헤더 컴포넌트 구현

  • 로고 / 내비게이션 버튼

  • 로그인 상태에 따라 UserProfile ↔️ 로그인 | 회원가입 버튼 표시

  • useAuthService.login으로 모의 로그인 처리

  • useAuthService.logout 수행

  • [todo]: 기존 회원가입 로그인 페이지 마이그레이션 및 인증 수행

5. 미디어 쿼리용 훅 제작

  • useMediaQuery 로 현재 브레이크포인트(mobile/tablet/desktop) 반환

  • getMediaCount 로 각 뷰포트별 bestProductsCount·allProductsCount 제공

6. API 연동

  • fetchProducts(page, pageSize, sort, keyword) 함수 작성

  • axios 기반 에러 핸들링 로직 구현

7. API 호출 / 로딩 / 에러 훅

  • useApi(apiFn, deps) 훅: loading·data·error 상태 자동 관리

  • useCallback 으로 apiFn 메모이제이션

8. 베스트 상품 컴포넌트

  • bestProductsCount 따라 1/2/4개 요청

  • useApi(() => fetchProducts(1, count, 'favorite', ''), [count])

  • ProductCard + 로딩 스켈레톤 SkeletonCard 렌더링

9. 전체 상품 컴포넌트

  • page, sort, keyword 상태 선언

  • 헤더(AllProductsHeader) 분리: 검색·정렬·등록

  • useDebounce 커스텀 훅으로 검색어 300ms 디바운스

  • useApi(() => fetchProducts(page, pageSize, sort, debouncedKeyword), [...])

  • ProductCard + 로딩 스켈레톤 그리드 표시

10. 페이지네이션 컴포넌트

  • totalPages = ceil(totalCount / pageSize) 계산

  • 최대 5개 버튼: 현재 페이지 중심으로 앞뒤 2개씩

  • “‹”, “›” 이전·다음 버튼

  • 클릭 시 setPage 호출 → useApi 자동 재호출

11. 반응형 스타일링

  • Mobile / Tablet / Desktop 에 따라

  • 베스트 상품: 1/2/4 컬럼

  • 전체 상품: 4/6/10 개 요청, 그리드 컬럼 수 조절

  • 검색·정렬 UI 배치 변경

  • select 아이콘을 모바일에서는 원형 버튼, 태블릿 이상에서 텍스트+화살표로

image
image

멘토에게

  • 아직 srcset 적용을 잘 못하는 것 같습니다. 로 이미지를 감싸지 않으면 반응형으로 로고가 전환되지 않습니다.

  • 다양한 디자인 패턴으로 최적화를 하고 싶은데 수정할 부분들이 있는지 궁금합니다.

  • AllProducts.tsx 컴포넌트에서 여러 상태를 관리하는데 최적화를 어떻게 진행해야 할지 궁금합니다.

    여러 상태들을 객체로 담아야 할지, useReducer 같은 커스텀 상태관리를 사용해도 되는지 궁금합니다.

  • /MarketPage 경로의 경우 레이아웃과 컴포넌트가 모여있는데 디렉터리 구조를 어떻게 관리하는게 직관적일지 궁금합니다.

  • AllProductHeader에서 요소들의 위치를 변경하는 반응형 css가 미흡한거같습니다.

    컨테이너에서 요소들을 flex로 담고 모바일 화면에서는 버튼만 absolute로 이동하는 식으로 구현했는데, 반응형 디자인에서 요소들의 배치 순서가 달라질때 어떻게 변경하는게 깔끔한지 궁금합니다.

  • 타입스크립트로 제작하였는데 타입 정의를 잘 하면서 제작했는지 잘 모르겠습니다.

DreamPaste added 26 commits May 20, 2025 02:17
@DreamPaste DreamPaste added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label May 26, 2025
@addiescode-sj addiescode-sj removed their assignment May 27, 2025
@addiescode-sj addiescode-sj self-requested a review May 27, 2025 00:20
Copy link
Collaborator

@addiescode-sj addiescode-sj left a comment

Choose a reason for hiding this comment

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

수고하셨습니다!
여러가지 좋은 시도들이 보여서 저도 깊게 공부하실만한 토픽을 몇개 드려봤어요.
코멘트 보시고 천천히 리팩토링해보세요!

주요 리뷰 포인트

  • SRP 원칙에 의거한 복잡도 해결
  • Request waterfall + 로딩 처리 가독성 문제 해결
  • 디바운스 처리 시 좀 더 매끄러운 UX 만들기

<Header />
<main className={style.main}>
{/* {내부 페이지 전환을 위한 라우터} */}
<Router />
Copy link
Collaborator

Choose a reason for hiding this comment

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

Router보다는 Routes 가 더 네이밍이 적절할것같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

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

한가지 관심사에 너무 여러 종류의 세분화된 커스텀 훅이 있는것도 불필요한 관리가 추가되기때문에 좋지않아요.

현재 구조의 문제점이 있다면, useAuth는 단순히 context를 가져오는 역할만 하고 있어 불필요한 추상화로 보이고,
useAuthService는 실제 인증 로직을 담당하지만, 이름이 서비스임에도 불구하고 훅으로 구현되어 있어 다소 일관성이 떨어집니다 (보통 서비스는 순수한 비즈니스 로직을 담당하는 클래스나 객체를 의미합니다).

따라서 AuthContext 내부에서 user, isLoggedIn, login, logout 을 모두 관리하는 형태로 구조화하면 좋을것같아요.

이왕이면 타입스크립트를 쓰고계시니 인증 관련 타입을 정의하는 파일을 만들고 타입 안정성을 높여볼까요?

export interface User {
  id: string;
  userName: string;
  userAvatar: string;
}

export interface AuthContextType {
  user: User | null;
  isLoggedIn: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

그리고 useAuthService는 제거하고, useAuthContext 하나면 남기도록 해요!

export const useAuthContext = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuthContext는 <AuthProvider> 내부에서만 사용해야 합니다.');
  }
  return context;
};

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 바꿔주면 타입 안정성도 좋아지고, 여러 파일 거칠 필요 없이 디버깅의 범위가 한정되어있으니, 나중에 실제 API 연동이나 토큰 관리 등의 기능을 추가할 때도 쉽게 확장할 수 있는 장점이 있겠죠?

Comment on lines +44 to +49
const { totalCount, list } = response.data;

return {
totalCount,
list,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금이야 미션이라 그럴일이 없겠지만 나중에 스키마가 변경되면 로직이 깨지니까 api 핸들러에선 가급적 response 그대로 리턴하시는게 좋습니다. 실제 이 핸들러를 사용하는측에서 얼마든지 필요에 따라 가공해서 쓸수있으니까요! :)

Comment on lines +54 to +75
if (error.response) {
// 서버가 상태 코드로 응답했을 때
errMsg += ` 상태 코드: ${error.response.status}`;
console.error(errMsg, error.response.status, error.response.data);
throw new Error(errMsg);
} else if (error.request) {
// 요청이 이루어졌으나 응답이 없을 때
errMsg += ` 응답이 없습니다.`;
console.error(errMsg, error.request);
throw new Error(errMsg);
} else {
// 요청 설정 중 오류가 발생했을 때
errMsg += ` 설정 오류`;
console.error(errMsg, error.message);
throw new Error(errMsg);
}
} else {
// 예상치 못한 오류
errMsg += ` 예상치 못한 오류가 발생했습니다.`;
console.error(errMsg, error);
throw new Error(errMsg);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

꽤 복잡해보이는데요? ㅎㅎ
너무 여러가지 상황을 가정해서 미리 대비하기보다는,
swagger 보시면서 실제 어떤 에러가 어떤 메시지로 오는지보시고
이런식으로 범용성있고 일관성있게 유지될수있게끔 처리하는것도 좋을것같아요.

      if (error.response) {
        errMsg += ` (${error.response.status})`;
      }
      console.error(errMsg, error);
    } else {
      console.error(errMsg, error);
    }

    throw new Error(errMsg);
  }

Comment on lines +37 to +40
{/* 좋아요 여부에 따라 좋아요 수 증가 */}
<span className={style['product-card__like-count']}>
{isLike ? favoriteCount + 1 : favoriteCount}
</span>
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 +10 to +11
import useDebounce from '@/hooks/useDebounce';
function AllProducts() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

항상 import 구문과 컴포넌트 시작 사이에 공백 한칸 띄워주세요 :)

const [sort, setSort] = useState<SortKey>('recent');
const [keyword, setKeyword] = useState('');
// 키워드 디바운스 처리
const debouncedKeyword = useDebounce(keyword, 300);
Copy link
Collaborator

Choose a reason for hiding this comment

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

키워드 디바운스 처리 잘하셨네요 👍 좋은 시도입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

NIT: 다만, useDebounce 훅 내부를 보니 setTimeout를 사용하고있어, 디바운싱된 값이 늦게 반영되면서 UI가 느리게 반응(UI 업데이트 지연)하는 것처럼 느껴질 수 있습니다.

React 18부터는 동시성(Concurrent) 기능을 활용해, 상태 업데이트의 우선순위를 조절할 수 있어서 이 문제를 해결해볼 수 있는데요!

이럴 때 우선순위를 조절하는 useTransition 훅을 사용하면, 입력값(검색어) 자체는 즉시 반영하고, 실제 API 호출 등 무거운 작업만 낮은 우선순위로 처리하게 만들어 프레임레이트 및 UX를 매끄럽게 개선할수있답니다 :)

아래 공식 문서 참고해보시고 적용해보시면 좋을것같네요!

참고

))}
</div>
{/* 페이지네이션 컴포넌트 */}
<PageNations
Copy link
Collaborator

Choose a reason for hiding this comment

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

페이지네이션의 경우 Pagination 이 올바른 철자입니다 :)
간단한 단어 교정이나 오타는 익스텐션 도움을 받아볼까요?

https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker

Comment on lines +62 to +71
{/* 전체 상품 리스트 */}
<div className={style['all-products__list']}>
{loading
? Array.from({ length: pageSize }).map((_, idx) => (
<SkeletonCard key={idx} />
))
: products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
Copy link
Collaborator

Choose a reason for hiding this comment

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

주석의 위험성이네요 ㅎㅎㅎ
주석 없이도 한눈에 잘 읽힐정도로 가독성 좋은 코드를 짜보도록합시다! :)

전체 상품 리스트의 경우 로딩 상태일때는 스켈레톤을 띄우고, 아닐때는 products 배열의 갯수만큼 ProductCard를 렌더링하는 역할을 맡고 있습니다.
이 뿐만 아니라 페이지네이션, 정렬, 검색등의 일들을 한 컴포넌트에서 처리하고있기때문에 복잡도가 큰 편이고 SRP 원칙에 따라 컴포넌트를 분리해보면서 해당 컴포넌트의 복잡도를 줄여보는걸 목표로 리팩토링해봐요 :)

이때 스켈레톤의 경우 지금과 같이 스켈레톤을 띄우기 위한 비즈니스 로직을 바깥으로 드러내는것보다 좀 더 선언적으로, 원하는 count에 대한 props를 넘기기만 하면 되는 구조로 바꿔봅시다:

// SkeletonList 컴포넌트를 분리
function SkeletonList({ count }: { count: number }) {
  return (
    <>
      {Array.from({ length: count }).map((_, idx) => (
        <SkeletonCard key={idx} />
      ))}
    </>
  );
}

ProductCard또한, AllProducts의 역할을 좀 더 단순화하기위해 ProductList 컴포넌트로 분리해봐요.

// ProductList 컴포넌트를 분리
function ProductList({ products }: { products: Product[] }) {
  return (
    <>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </>
  );
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 컴포넌트만 분리해줘도 리턴문 가독성이 좋아지는데요!
여기서 좀 더 나아가 로딩 처리 방식또한 개선해주면 더 명확해질것같습니다.

지금은 ProductCard 내부에서 data fetching을 시도하고있지않지만
나중에 ProductCard 내부에서도 data fetching을 시도하게 된다면?

데이터 패칭이 이뤄지고 나서야 관련된 컴포넌트를 렌더링하는 구조이다보니, 이런 패턴이 계층별로 반복된다면 당연히 렌더링 성능또한 나빠지겠죠?

그리고 로딩 상태에 대한 분기문이 필요해지다보니 지금과 같이 복잡도가 큰 컴포넌트에서는 가독성 문제가 다른 컴포넌트보다 더 커질 수 있고요.

이런 request waterfall과 가독성 문제를 해소해주려면 Suspense를 사용한 Render as you fetch 방식을 사용해주시는게 좋은데요! 이 과정은 fetch 렌더링 작업과 비동기 데이터 호출 과정을 동시에 이루어지게 만들어줍니다.

Suspense를 사용해 리팩토링한 최종 버전 예시를 보여드릴게요 :)

BEFORE

        {loading
          ? Array.from({ length: pageSize }).map((_, idx) => (
              <SkeletonCard key={idx} />
            ))
          : products.map((product) => (
              <ProductCard key={product.id} product={product} />
            ))}

AS-IS

        <Suspense fallback={<SkeletonList count={pageSize} />}>
          <ProductList products={products} />
        </Suspense>

Comment on lines +5 to +12
function MarketPage() {
return (
<div className={style['market-page']}>
<BestProducts />
<AllProducts />
</div>
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

위에서 말씀드린 Suspense를 사용한 로딩 처리는 여기서도 해주시는게 좀 더 효과적이겠죠? :)

@addiescode-sj
Copy link
Collaborator

질문에 대한 답변

멘토에게

  • 아직 srcset 적용을 잘 못하는 것 같습니다.
  • 다양한 디자인 패턴으로 최적화를 하고 싶은데 수정할 부분들이 있는지 궁금합니다.
  • AllProducts.tsx 컴포넌트에서 여러 상태를 관리하는데 최적화를 어떻게 진행해야 할지 궁금합니다.

우선 AllProducts의 경우 검색, 정렬, 페이지네이션, 로딩 처리 등 한 컴포넌트에서 너무 많은 역할을 맡고 있기때문에 복잡도가 올라갈 수 밖에 없습니다. 여기서 컴포넌트를 분리해 관심사를 분리하고 재사용 가능한 로직은 커스텀 훅으로 분리하고 꼭 필요한 props, state만 유지하도록 해보시면 개선에 도움이 되겠죠? 본문 내에 복잡도를 개선하는 방법에 대해 자세히 코멘트해드렸는데, 나머지 리팩토링도 개선의 여지가 보인다면 진행해보세요! :)

여러 상태들을 객체로 담아야 할지, useReducer 같은 커스텀 상태관리를 사용해도 되는지 궁금합니다.

상태를 객체로 관리하는건 대부분의 경우 좋지 않은 방법입니다.

우선 객체의 한 속성만 변경해도 전체 객체가 새로 생성되기때문에,
이로 인해 해당 상태를 의존하는 모든 컴포넌트가 불필요하게 리렌더링될 수 있습니다.

그리고 상태 업데이트 시 이전 상태를 스프레드 연산자로 복사해야하고
실수로 이전 상태를 복사하지 않으면 다른 상태 값들이 의도치않게 변경되기때문에 상태 관리의 복잡도가 올라갑니다.

useReducer의 경우에도 여러 상태가 서로 연관되어있거나(의존성 존재), 상태 업데이트 로직이 복잡할 경우 사용하시는게 좋습니다.

단지 코드 정리 및 가독성을 위해서만 상태를 객체로 만들거나 useReducer를 사용하시면 좋지않다는 점 참고해보세요 :)

  • /MarketPage 경로의 경우 레이아웃과 컴포넌트가 모여있는데 디렉터리 구조를 어떻게 관리하는게 직관적일지 궁금합니다.

pages > MarketPage > components > AllProduct > AllProducts.tsx, AllProducts.module.scss, index.ts 이런 경로로 관리하시는걸 추천드립니다.

  • AllProductHeader에서 요소들의 위치를 변경하는 반응형 css가 미흡한거같습니다.

    컨테이너에서 요소들을 flex로 담고 모바일 화면에서는 버튼만 absolute로 이동하는 식으로 구현했는데, 반응형 디자인에서 요소들의 배치 순서가 달라질때 어떻게 변경하는게 깔끔한지 궁금합니다.

엘리먼트의 순서를 조정하기 위해서였다면 Flexbox의 order 속성을 활용하시면됩니다.

  • 타입스크립트로 제작하였는데 타입 정의를 잘 하면서 제작했는지 잘 모르겠습니다.

이번 미션은 타입스크립트 활용이 주 목적이 아니었기때문에 타입스크립트 관련해서는 피드백을 최소로 드렸어요 :)
타입스크립트를 활용할때 필수적으로 정의해주셔야하는 부분, 개발자의 의도를 더 잘 표현하기위해 활용하는 부분, 타입 가드를 사용해 예외처리에 도움을 주는 부분 등 활용도가 크기 때문에 나중에 타입스크립트 미션하실때 천천히 공부해보시고 궁금한점 생기면 말씀주세요!

@addiescode-sj addiescode-sj merged commit 3674b05 into codeit-bootcamp-frontend:React-염휘건 May 28, 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