Skip to content

[김치영] sprint7 #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

Chiman2937
Copy link
Collaborator

요구사항

기본

체크리스트 [기본]
상품 상세

  • 상품 상세 페이지 주소는 "/items/{productId}" 입니다.

  • response 로 받은 아래의 데이터로 화면을 구현합니다.
    => favoriteCount : 하트 개수
    => images : 상품 이미지
    => tags : 상품태그
    => name : 상품 이름
    => description : 상품 설명

  • 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 "/items" 으로 이동합니다

상품 문의 댓글

  • 문의하기에 내용을 입력하면 등록 버튼의 색상은 "3692FF"로 변합니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다
    => image : 작성자 이미지
    => nickname : 작성자 닉네임
    => content : 작성자가 남긴 문구
    => description : 상품 설명
    => updatedAt : 문의글 마지막 업데이트 시간

심화

  • 모든 버튼에 자유롭게 Hover효과를 적용하세요.

배포

https://sprint-mission-kcy.netlify.app/

검토 요청 사항

✅ 커스텀 훅 useAsync 생성:
src\hooks\useAsync.jsx)

적용 대상:

  • BestItemsSection, CurrentItemSection
    src\pages\ItemsPage\sections)

  • ItemCommentsSection
    src\pages\ItemDetailsPage\sections)

✅ 커스텀 DropDown 생성:
KebabMenu
src\components\common\KebabMenu\KebabMenu.jsx
SelectDropDown
src\components\common\SelectDropdown\SelectDropdown.jsx

✅ 미션7 기능구현 -

  • 페이지:
    src/pages/ItemDetailsPage/ItemDetailsPage.jsx

  • sections:,
    src\pages\ItemDetailsPage\sections\ItemDetailsSection.jsx
    src\pages\ItemDetailsPage\sections\ItemCommentsSection.jsx

  • components:
    src\components\comments\CommentCard.jsx
    src\components\comments\CommentEditForm.jsx
    src\components\comments\CommentRequireForm.jsx
    src\components\comments\CommentsContainer.jsx
    src\components\comments\CommentView.jsx

  • utils:
    src\utils\format.js - formatTimeStamp

  • 로딩스피너 생성:
    src\components\layout\LoadingSpinner\LoadingSpinner.jsx

✅ 폴더구조 변경
기존에 components 폴더 안에 최상위 페이지를 제외한 모든 컴포넌트들을 관리하다 보니 구별이 어려워져 아래와 같이 폴더를 나눠봤습니다.
(1) 공통적으로 사용하는 기능이 있는 컴포넌트는 common, 단순 레이아웃을 담당하는 컴포넌트는 layout으로 분리
(2) 그 외 특정 페이지에서만 사용되는 컴포넌트는 각 페이지 이름 폴더에 관리

  • 변경 전
    ├─ src
    │ ├─ components
    │ │ ├─ BestItemsSection.jsx
    │ │ ├─ CommentCard.jsx
    │ │ ├─ CommentCard.module.css
    │ │ ├─ ...

  • 변경 후
    ├─ src
    │ ├─ App.jsx
    │ ├─ common.css
    │ ├─ components
    │ │ ├─ comments (2)
    │ │ ├─ common (1)
    │ │ ├─ ItemDetails (2)
    │ │ └─ layout (1)

✅ 그 외(단순 컴포넌트 분리)

  • ProfileCard 컴포넌트 분리
    src\components\layout\ProfileCard\ProfileCard.jsx

  • ItemImageViewer 컴포넌트 분리
    src\components\common\ItemImageViewer\ItemImageViewer.jsx

  • SearchInput 컴포넌트 분리
    src\components\common\SearchInput\SearchInput.jsx

  • 페이지네이션 / 페이지네이션 버튼 컴포넌트 분리
    src\components\common\Pagination\PaginationButton.jsx

  • FavoriteButton 컴포넌트 분리
    src\components\common\FavoriteButton\FavoriteButton.jsx


스크린샷

반응형 디자인(PC)

image

반응형 디자인(TABLET, MOBILE)

image

댓글 없는 페이지

image

문의하기 버튼 활성화

image

댓글 수정

image

로딩 스피너 적용

image

멘토에게

1. z-index 관리

케밥버튼 및 드롭다운 구현할 때 아래와 같이 다음 댓글에 의해 드롭다운이 가려지는 문제가 있었습니다. (예시)
image

z-index state를 만들어서 드롭다운이 활성화 된 댓글만 z-index 우선순위를 높이는 방식으로 해결하려고 했는데요, 현재 컴포넌트 구조는 이렇습니다.
(굳이 CommentView가 있는 이유는 CommentCard 안에서 CommentViewCommentEditForm 중 하나를 렌더링 하기 위해서 입니다)

├─ CommentsContainer
│ ├─ CommentCard
│ │ ├─ CommentView
│ │ │ ├─ KebabMenu(DropDown)

image

CommentCard가 댓글 한개의 최상위 컴포넌트여서 여기에 z-index를 적용하려면 KebabMenu의 isKebabSelected State를 Comment Card까지 끌어올려서 사용해야하는 문제가 있었고,
image

이 문제를 최대한 해결하기 위해 CommentCard에서 별도로 z-index state를 만들었고, handleKebabOpen, handleKebabClose 이벤트를 KebabButton에 props로 전달해서 z-index State를 변경하도록 만들었습니다.

image

  const handleKebabOpen = () => {
    setZIndex(1);
  };

  const handleKebabClose = () => {
    setZIndex(0);
  };

이런식으로 해결은 했는데 결과적으로 KebabButton의 props는 늘어났고.. 이게 최선의 방법인지 모르겠습니다. 더 좋은 방법이 있을까요?

2. DropDown 커스텀

DropDown을 커스텀하면서 아래와 같이 Button과 DropDown을 하나의 컴포넌트로 구현했는데요, 따로 나눠서 만드는게 더 확장성이 좋을까요?

return (
    <div className={styles["container"]}>
      //KebabButton
      <img
        name={id}
        className={styles["kebab-button"]}
        src={"/images/ic_kebab.png"}
        width={24}
        onClick={handleKebabClick}
        ref={kebabRef}
      />
      //DropDown
      {isKebabSelected && (
        <ul className={styles["dropdown-menu"]} ref={DropDownRef}>
          {menuItems.map(({ label, onClick }) => (
            <li className={styles["dropdown-button"]} key={label} onClick={onClick}>
              {label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );

3. 컴포넌트 분리

이번 미션7은 진행하면서 최대한 가독성/역할 분리에 초점을 맞추고 진행을 했는데 개선해야 할 점이 있는지 궁금합니다.

그 외

처음에 완전히 플랫하게 작업 한 후, 필요에 따라 컴포넌트를 분리하는 식으로 작업하고 있는데 이 부분에서 시간이 상당히 많이 걸리는 것 같습니다... 연습하다보면 빨라지겠죠? 처음부터 어느정도는 분리를 해주면서 작업을 해야할까요? 😂

  • sprint6 미션 수정에 대한 내용은 별도로 PR 올리겠습니다.

Chiman2937 added 24 commits May 19, 2025 21:20
@Chiman2937 Chiman2937 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label May 26, 2025
@Chiman2937 Chiman2937 requested a review from addiescode-sj May 26, 2025 03:56
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.

수고하셨습니다~!
아직 sprint 6 수정 관련한 PR은 안올리셨다고하셔서 저번 리뷰때 드린 피드백은 최대한 생략하고,
새롭게 변경되거나 추가된 부분에 대한 피드백 위주로 드려봤어요!

주요 리뷰 포인트

  • 폴더 구조 피드백
  • 네이밍 변경
  • z-index 위계 관리
  • 명확한 state, props만 남기기

│ │ │ ├─ CommentsContainer.module.css
│ │ │ ├─ CommentView.jsx
│ │ │ └─ CommentView.module.css
│ │ ├─ common
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 폴더로 따로 구분해놓을 필요없이, 여러 페이지에서 사용하는 컴포넌트인 경우에만 src > components에 모아두고, CommentsContainer 같이 특정 페이지 (아이템 상세 페이지) 에서 사용하는 컴포넌트의 경우 해당 페이지 컴포넌트 경로와 가까운곳에 components 폴더를 만들고 위치를 옮겨주시면 좋을것같아요.

직접적인 예시:
src > pages > ItemDetailsPage > components > Comments > CommentsContainer

import ProfileCard from "../layout/ProfileCard/ProfileCard";
import FavoriteButton from "../common/FavoriteButton/FavoriteButton";

const ItemMetaData = ({ ownerNickname, updatedAt, favoriteCount }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 metadata는 페이지별 메타정보 (Plain React app의 경우 CSR & SPA이기때문에 별도 설정 필요) 를 나타내는 의미로 많이 쓰이기때문에, 컴포넌트 네이밍을 metadata를 빼고 바꿔줄 필요가 있어보여요. ItemInfo, ItemSummary 등은 어떨까요?


const CommentCard = ({ comment }) => {
const [isEditing, setIsEditing] = useState(false);
const [zIndex, setZIndex] = useState(0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

z-index의 경우 위계를 정해두지않고 랜덤한 숫자를 쓰게되면,
다른 컴포넌트와의 상호작용을 처리할때 쉽게 z-index 이슈가 생길 수 있어요.

지금은 css modules를 쓰고계시니까

:root {
  /* Base layers */
  --z-base: 0;
  --z-above-base: 1;
  
  /* UI Components */
  --z-dropdown: 1000;
  --z-modal: 2000;
  --z-toast: 3000;
  --z-tooltip: 4000;
  
  /* Fixed elements */
  --z-header: 100;
  --z-footer: 100;
  --z-sidebar: 200;
  
  /* Overlay elements */
  --z-overlay: 1500;
  --z-overlay-above: 2500;
} 

이렇게 z-index 위계를 변수화해두고 사용하는걸 추천드립니다!
그리고 나중에 모달과 같이 최상위에 렌더링되어야하는 요소가 있다면, Portal을 사용해서 DOM 계층 구조의 최상위에 렌더링되게끔해 z-index나 overflow 문제를 쉽게 해결하실수도 있으니 참고해보세요 :)

참고

Copy link
Collaborator

Choose a reason for hiding this comment

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

그리고 직접적으로 z-index를 계산해서 상태로 관리하는것보다는 isKebabOpen 상태를 관리하고, 해당 상태에 따라 z-index를 골라쓰는 방식으로 수정하는게 코드 퀄리티 측면에서 도움이 될 것 같네요.

  const [isKebabOpen, setIsKebabOpen] = useState(false);

  const toggleIsKebabOpen = (isKebabOpen) => {
    setIsKebabOpen(isKebabOpen);
  };

};

return (
<div style={containerStyle} className={styles["container"]}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

위 코멘트처럼 컴포넌트 입장에서 중요한 데이터인 isKebabOpen에 따른 스타일링 분기를 만들어주면 이렇게 되겠죠?

Suggested change
<div style={containerStyle} className={styles["container"]}>
<div className={`${styles.container} ${isKebabOpen ? styles.open : ""}`}>

e.preventDefault();
};

const isInquireValid = !inquireText;
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 코드에서 isInquireValid는 inquireText가 빈 문자열일 때 true를 반환하고, 내용이 있을 때 false를 반환하고 있습니다.

이게 작성된 의도가 아니라 inquireText가 빈문자열인지 아닌지를 판단하고싶은거면,
이중 부정 연산자를 사용해 !!inquireText 로 바꿔주시면 됩니다.

값을 boolean으로 치환해서 아래와 같이 동작하게 됩니다.

  • inquireText가 빈 문자열("")일 때 → false
  • inquireText에 내용이 있을 때 → true

Comment on lines +6 to +17
<div className={styles["dot"]} style={{ "--i": 0 }}></div>
<div className={styles["dot"]} style={{ "--i": 1 }}></div>
<div className={styles["dot"]} style={{ "--i": 2 }}></div>
<div className={styles["dot"]} style={{ "--i": 3 }}></div>
<div className={styles["dot"]} style={{ "--i": 4 }}></div>
<div className={styles["dot"]} style={{ "--i": 5 }}></div>
<div className={styles["dot"]} style={{ "--i": 6 }}></div>
<div className={styles["dot"]} style={{ "--i": 7 }}></div>
<div className={styles["dot"]} style={{ "--i": 8 }}></div>
<div className={styles["dot"]} style={{ "--i": 9 }}></div>
<div className={styles["dot"]} style={{ "--i": 10 }}></div>
<div className={styles["dot"]} style={{ "--i": 11 }}></div>
Copy link
Collaborator

Choose a reason for hiding this comment

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

이거 갯수가 정해져있는 연산이라면 Array.from을 활용해서 코드를 간소화해보는건 어떨까요?

Suggested change
<div className={styles["dot"]} style={{ "--i": 0 }}></div>
<div className={styles["dot"]} style={{ "--i": 1 }}></div>
<div className={styles["dot"]} style={{ "--i": 2 }}></div>
<div className={styles["dot"]} style={{ "--i": 3 }}></div>
<div className={styles["dot"]} style={{ "--i": 4 }}></div>
<div className={styles["dot"]} style={{ "--i": 5 }}></div>
<div className={styles["dot"]} style={{ "--i": 6 }}></div>
<div className={styles["dot"]} style={{ "--i": 7 }}></div>
<div className={styles["dot"]} style={{ "--i": 8 }}></div>
<div className={styles["dot"]} style={{ "--i": 9 }}></div>
<div className={styles["dot"]} style={{ "--i": 10 }}></div>
<div className={styles["dot"]} style={{ "--i": 11 }}></div>
{Array.from({ length: 12 }, (_, i) => (
<div key={i} className={styles["dot"]} style={{ "--i": i }}></div>
))}

Comment on lines +23 to +34
{!comments && (
<div className={styles["comment-loading-container"]}>
<LoadingSpinner />
</div>
)}
{comments && comments?.length > 0 && <CommentsContainer comments={comments} />}
{comments && comments?.length === 0 && (
<div className={styles["comment-none-container"]}>
<img src={"/images/img_comment_none.png"} width={196} />
<span className={styles["comment-none-text"]}>아직 문의가 없어요</span>
</div>
)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

리턴문은 깔끔하게 유지되도록 신경써주시는게 좋습니다!
저번 리뷰때 얘기했던것처럼 조건식을 상수로 만들어주세요.

  const isLoading = !comments;
  const hasComments = comments && comments.length > 0;
  const hasNoComments = comments && comments.length === 0;

그러면 이런식으로 로직이 더 명확히 들어와요.

      {isLoading && (
        <div className={styles["comment-loading-container"]}>
          <LoadingSpinner />
        </div>
      )}
      {hasComments && <CommentsContainer comments={comments} />}
      {hasNoComments && (
        <div className={styles["comment-none-container"]}>
          <img src={"/images/img_comment_none.png"} width={196} />
          <span className={styles["comment-none-text"]}>
            아직 문의가 없어요
          </span>
        </div>
      )}

[]
);

const { result } = useAsync(getItems, options);
Copy link
Collaborator

Choose a reason for hiding this comment

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

options보다는 params정도가 더 네이밍이 괜찮을것같네요!
그리고 POST나 PATCH 처럼 request body가 필요한 요청의 경우 해당 useAsync 훅을 사용하고싶으면 어떻게 해줘야할지 고민해봅시다 :)

Comment on lines +35 to +48
const paginationHandler = {
onPageNumberClick: (e) => {
const nextPageNumber = Number(e.target.value);
setOffset((nextPageNumber - 1) * pageSize + 1);
},
onPagePrev: () => {
const nextPageNumber = visiblePageNumbers[0] - 1;
setOffset((nextPageNumber - 1) * pageSize + 1);
},
onPageNext: () => {
const nextPageNumber = visiblePageNumbers[visiblePageNumbers.length - 1] + 1;
setOffset((nextPageNumber - 1) * pageSize + 1);
},
};
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.

PR 올리실때 더이상 불필요한 파일은 제거해주세요!

@addiescode-sj
Copy link
Collaborator

질문에 대한 답변

멘토에게

1. z-index 관리

케밥버튼 및 드롭다운 구현할 때 아래와 같이 다음 댓글에 의해 드롭다운이 가려지는 문제가 있었습니다. (예시) image

z-index state를 만들어서 드롭다운이 활성화 된 댓글만 z-index 우선순위를 높이는 방식으로 해결하려고 했는데요, 현재 컴포넌트 구조는 이렇습니다. (굳이 CommentView가 있는 이유는 CommentCard 안에서 CommentViewCommentEditForm 중 하나를 렌더링 하기 위해서 입니다)

├─ CommentsContainer │ ├─ CommentCard │ │ ├─ CommentView │ │ │ ├─ KebabMenu(DropDown)

image

CommentCard가 댓글 한개의 최상위 컴포넌트여서 여기에 z-index를 적용하려면 KebabMenu의 isKebabSelected State를 Comment Card까지 끌어올려서 사용해야하는 문제가 있었고, image

이 문제를 최대한 해결하기 위해 CommentCard에서 별도로 z-index state를 만들었고, handleKebabOpen, handleKebabClose 이벤트를 KebabButton에 props로 전달해서 z-index State를 변경하도록 만들었습니다.

image

  const handleKebabOpen = () => {
    setZIndex(1);
  };

  const handleKebabClose = () => {
    setZIndex(0);
  };

이런식으로 해결은 했는데 결과적으로 KebabButton의 props는 늘어났고.. 이게 최선의 방법인지 모르겠습니다. 더 좋은 방법이 있을까요?

본문 안에서 답변 드렸습니다 :)
z-index의 위계를 따로 관리하는 방식으로 바꿔주세요!

2. DropDown 커스텀

DropDown을 커스텀하면서 아래와 같이 Button과 DropDown을 하나의 컴포넌트로 구현했는데요, 따로 나눠서 만드는게 더 확장성이 좋을까요?

return (
    <div className={styles["container"]}>
      //KebabButton
      <img
        name={id}
        className={styles["kebab-button"]}
        src={"/images/ic_kebab.png"}
        width={24}
        onClick={handleKebabClick}
        ref={kebabRef}
      />
      //DropDown
      {isKebabSelected && (
        <ul className={styles["dropdown-menu"]} ref={DropDownRef}>
          {menuItems.map(({ label, onClick }) => (
            <li className={styles["dropdown-button"]} key={label} onClick={onClick}>
              {label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );

네, 버튼과 드롭다운은 엄연히 다른 역할을 담당하니까, 컴포넌트로 따로 분리되어있는편이 유지보수 측면에서 낫겠네요 :)

3. 컴포넌트 분리

이번 미션7은 진행하면서 최대한 가독성/역할 분리에 초점을 맞추고 진행을 했는데 개선해야 할 점이 있는지 궁금합니다.

확실히 코드가 훨씬 읽기 수월하고 편해진것같아요! 굳굳 👍

그 외

처음에 완전히 플랫하게 작업 한 후, 필요에 따라 컴포넌트를 분리하는 식으로 작업하고 있는데 이 부분에서 시간이 상당히 많이 걸리는 것 같습니다... 연습하다보면 빨라지겠죠? 처음부터 어느정도는 분리를 해주면서 작업을 해야할까요? 😂

제가 코드리뷰 채널에서 공유드린, 리액트 설계 원칙을 공부해보시면 설계할때 도움이 되실것같아요 :)
https://discord.com/channels/1344520737691668561/1344520738299842595/1376489115725594684

@addiescode-sj addiescode-sj merged commit 6f6ea58 into codeit-bootcamp-frontend:React-김치영 Jun 19, 2025
1 check failed
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