Skip to content

Conversation

@SanginJeong
Copy link
Collaborator

@SanginJeong SanginJeong commented Sep 4, 2025

요구사항

배포링크

기본

  • 상품 상세 페이지 주소는 "/items/{productId}" 입니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다.
  • 목록으로 돌아가기 버튼을 클릭하면 중고마켓 페이지 주소인 "/items" 으로 이동합니다.
  • 문의하기에 내용을 입력하면 등록 버튼의 색상은 "3692FF"로 변합니다.
  • response 로 받은 아래의 데이터로 화면을 구현합니다.

심화

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

주요 변경사항

  • 상품 디테일 페이지를 구현했습니다.
  • 추상화 수준을 동일하게 맞추려고 신경썼습니다.
  • 핸들러 함수들 리팩토링을 진행했습니다.

스크린샷

localhost_5173_items_226 (1) localhost_5173_items_226 (3) localhost_5173_items_226 (2) localhost_5173_items_226 (4) localhost_5173_items_226 (5)

멘토에게

  • URL parameter 를 가져오고 각 컴포넌트의 props로 넘겨주는 것과, 각 컴포넌트들에서 URL parameter를 가져오는 것에 대해서 고민을 많이했습니다. 의견이 궁금합니다.

변경 전

const ProductDetailPage = () => {
  const params = useParams();
  const { productId } = params;
  return (
    <div className="productDetail-page-layout">
      <ProductDetail productId={productId}/>
      <div className="productDetail-page-comments-area">
        <ProductQuestion productId={productId}/>
        <ProductComments productId={productId}/>
      </div>
      <div className="productDetail-page-back-btn-area">
        <ProductBackButton />
      </div>
    </div>
  );
};

변경 후 : 각 컴포넌트에서 직접 가져옵니다.

const ProductDetailPage = () => {
  return (
    <div className="productDetail-page-layout">
      <ProductDetail />
      <div className="productDetail-page-comments-area">
        <ProductQuestion />
        <ProductComments />
      </div>
      <div className="productDetail-page-back-btn-area">
        <ProductBackButton />
      </div>
    </div>
  );
};
  • sprint 6 미션 PR당시 답변 주신게 이해가 안돼서 comment를 남겼었습니다.

(질문) 태그 인풋은 엔터를 누를 때 등록되게 해서 따로 form을 구성했는데 이 방법이 어색한 느낌이 들어서 어떤지 궁금합니다.
PR 링크

@SanginJeong SanginJeong requested a review from kiJu2 September 4, 2025 12:57
@SanginJeong SanginJeong self-assigned this Sep 4, 2025
@SanginJeong SanginJeong added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Sep 4, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 5, 2025

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

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 5, 2025

역시 상인님... 질문을 '읽는 사람'을 고려하여 정말 잘 전달주셨네요. 커뮤니케이션 스킬이 남다르십니다. 🥺

URL parameter 를 가져오고 각 컴포넌트의 props로 넘겨주는 것과, 각 컴포넌트들에서 URL parameter를 가져오는 것에 대해서 고민을 많이했습니다. 의견이 궁금합니다.

제 생각에서는 컴포넌트가 props로 받는게 낫지 않을까 싶어요.
productId는 URL에서 읽을 수도 있지만, 받아온 데이터로도 읽을 수 있을거예요.
즉 컴포넌트가 필요로 하는 것은 productId이지, "URL에서 읽은 productId"가 아니지 않을까요?

다른 출처에서 받은 productId를 매개체로 사용될 수도 있으므로 props로 전달하는 것에 한 표 던집니다 😉

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 5, 2025

(질문) 태그 인풋은 엔터를 누를 때 등록되게 해서 따로 form을 구성했는데 이 방법이 어색한 느낌이 들어서 어떤지 궁금합니다.

헐... 죄송합니다 제가 코드를 잘못 읽고 질문에 대한 답변을 잘 못했네요 ㅠㅠㅠ

HTML 표준 위반이 아니며, 편하고 유용한 방법일 것으로 사료되기는 하나 일반적으로 하나의 제출 단위로 필요한 form을 하나로 만드는게 일반적이기는 합니다 !

다음과 같이 해볼 수도 있겠네요.

<form className="addItem-form" onSubmit={handleSubmitAddItem}>
  <fieldset>
    <legend className="sr-only">상품 기본 정보</legend>
    <AddItemFormHeader formData={formData} />
    <AddItemImage ... />
    <AddItemName value={formData.name} onChange={(e)=>handleChange(e,"name")} />
    <AddItemDescription ... />
    <AddItemPrice ... />
  </fieldset>

  <fieldset>
    <AddItemTag
      value={inputValueTag}
      onChange={(e) => setInputValueTag(e.target.value)}
      onKeyDown={(e) => {
        if (e.key === "Enter") {
          e.preventDefault();
          handleSubmitTag();
        }
      }}
    />
    <button type="button" onClick={handleSubmitTag}>태그 추가</button>
  </fieldset>

  <div className="actions">
    <button type="submit">상품 등록</button>
  </div>
</form>

다만, '일반적' 이라는 말이 참 모호하죠. 하핳.. 지금 상인님께서 작성하신 코드는 그렇다고 해서 HTML의 표준을 위배하고 있지는 않기에 그대로 두셔도 무방할 것으로 보입니다.

Comment on lines +5 to +6
DropDown.header = ({ children }) => {
return <div>{children}</div>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으.. 상인님 코드를 볼 때마다 느끼는거지만 유지보수에 신경을 많이 쓰시는 것 같아요.

그냥 Dorpdown children<div>로처리하셨을 수도 있었을텐데, 드롭다운이 공통적으로 변경되는 것도 고려하여 이렇게 설계하신게 느껴집니다.. 👍

Comment on lines +4 to +12
const ScrollToTop = () => {
const { pathname } = useLocation();

useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);

return null;
};
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 ScrollToTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
};
const useFixTop = () => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
};

컴포넌트는 보통 실체하는 ui가 있을 때 사용되는건데 이건 컴포넌트로 보긴 어렵겠네요.

<TagBadge name={tag} onDelete={() => onDelete(index)} />
<TagBadge
name={tag}
onDelete={onDelete ? () => onDelete(index) : undefined}
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
onDelete={onDelete ? () => onDelete(index) : undefined}
onDelete={onDelete && () => onDelete(index)}

어떻게 이게 가능할까요?

  • 자바스크립트에서 && 연산자는 앞의 값이 truthy일 때만 뒤의 값을 평가합니다.
  • 즉, onDelete가 존재하지 않으면 false가 반환되어 onDelete props에는 false가 들어갑니다.
  • React는 false, null, undefined를 렌더링하지 않는 값으로 처리하므로, 결과적으로 onDelete가 전달되지 않은 것과 동일하게 동작합니다.

Comment on lines +1 to +2
export const QUESTION_PLACEHOLDER =
"개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다.";
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 +16
export const useGetProductCommentsQuery = ({ productId, limit, cursor }) => {
return useQuery({
queryKey: ["getProductComments", productId, limit],
queryFn: () => getProductComments({ productId, limit, cursor }),
staleTime: 300000,
select: (response) => response.data,
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으.. 기초 프로젝트 때 익히신 react-query를 사용해보셨군요 ! 😊

배우신 것을 바로 응용해보시는 상인님의 학습 열정 리스펙합니다 👍👍


export const useGetProductCommentsQuery = ({ productId, limit, cursor }) => {
return useQuery({
queryKey: ["getProductComments", productId, limit],
Copy link
Collaborator

Choose a reason for hiding this comment

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

그런데, cursor도 함께 쿼리키에 포함하여야 하지 않을까요?

Suggested change
queryKey: ["getProductComments", productId, limit],
queryKey: ["getProductComments", productId, limit, cursor],

두 번째 커서, 세 번째 커서, 각기 다른 결과값을 가져올 것으로 보여서요 !

Comment on lines +12 to +25
const { data, isLoading, isError, error } = useGetProductCommentsQuery({
productId,
limit: 3,
});

if (isLoading) {
return <LoadingSpinner />;
}

if (isError) {
return <ErrorMessage errorMessage={error.message} />;
}

const comments = data.list;
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 { data, isLoading, isError, error } = useGetProductCommentsQuery({
productId,
limit: 3,
});
if (isLoading) {
return <LoadingSpinner />;
}
if (isError) {
return <ErrorMessage errorMessage={error.message} />;
}
const comments = data.list;
const { data: {
list: comments
}, isLoading, isError, error } = useGetProductCommentsQuery({
productId,
limit: 3,
});
if (isLoading) {
return <LoadingSpinner />;
}
if (isError) {
return <ErrorMessage errorMessage={error.message} />;
}

const comments = data.list;를 따로 선언하지 않고 받아올 때 구조분해 할당으로 선언과 동시에 별칭도 붙일 수 있어요 😉

Comment on lines +13 to +24
const {
data: productInfo,
isLoading,
isError,
error,
} = useGetProductDetailQuery({
productId,
});

if (isLoading) return <LoadingSpinner />;

if (isError) return <ErrorMessage errorMessage={error.message} />;
Copy link
Collaborator

Choose a reason for hiding this comment

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

아으.. 너무 깔끔합니다 !

loading, error 처리. 리액트 쿼리의 기본적인 상태들을 사용하시니까 코드가 훨씬 간결해졌네요 👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Sep 5, 2025

크으 ~ 너무 좋습니다 상인님.
상인님 코드를 보고 있으면 학습에 대한 저항감이 없고 의욕이 넘치시는게 느껴져서 강사로서 정말 뿌듯해요.
언제나 도전적이고 열정적이신 상인님께서 근미래에 멋진 개발자로 성장될 것이 기대됩니다 👍

@kiJu2 kiJu2 merged commit d7fef37 into codeit-bootcamp-frontend:React-정상인 Sep 5, 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