Skip to content

[1단계 - 장바구니 미션] 에리얼(정예지) 미션 제출합니다. #327

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

Merged
merged 116 commits into from
Jun 6, 2025

Conversation

yeji0214
Copy link

@yeji0214 yeji0214 commented May 29, 2025

📦 장바구니 미션

이번 미션을 통해 다음과 같은 학습 경험들을 쌓는 것을 목표로 합니다.

1단계

  • 클라이언트 상태를 효과적으로 모델링하고 관리할 수 있다.
  • Jest, React Testing Library(RTL)를 활용하여 주요 기능에 대한 테스트를 작성할 수 있다.

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

  • 기능 요구 사항을 모두 구현했고, 정상적으로 동작하는지 확인했나요?

  • RTL 테스트 케이스를 모두 작성했나요?

  • 배포한 데모 페이지에 정상적으로 접근할 수 있나요?

  • 리뷰어가 장바구니 추가를 쉽게 할 수 있도록 Curl 명령어를 전달해주세요. (DM으로 보내겠습니다!)

🎥 시연 영상

2025-05-29.1.52.03.mov

리뷰 요청 & 논의하고 싶은 내용

안녕하세요 해먼드 ! 처음 뵙네요 🙇🏻‍♀️🙇🏻‍♀️
이번 미션은 세라랑 진행했습니다 ! 벌써 레벨2가 끝나간다니 시간이 너무 빠른 것 같아요 ,,
리뷰 잘 부탁드립니다 😀

git-pages 배포는 https만 허용 가능해 다음 절차 따라서 설정 해주셔야합니다!
크롬 설정 -> 개인 정보 보호 및 보안 -> 사이트 설정 -> (맨아래)추가 콘텐츠 설정 -> 안전하지 않은 콘텐츠 -> 안전하지 않은 콘텐츠 표시가 > 허용됨 탭에 배포한 사이트 주소 추가 (https://yeji0214.github.io/react-shopping-cart/)

1) 상태 설계 의도

서버에서 내려오는 상태를 제외한 대부분의 상태는 파생 상태로 보고, 가능하면 별도의 상태를 만들지 않는 방향으로 구현했습니다. 다만 체크 여부는 원본 상태의 유지가 필요하다고 판단해 별도로 상태로 관리하고 있습니다. 이때 각 항목의 고유성을 보장하고 효율적인 연산을 위해 Set 자료구조를 사용해 체크된 항목의 ID를 저장하고 있습니다. 또 이 데이터를 불러오는 useFetchData 를 구현했습니다. 이를 통해 API 호출에 대한 로딩, 에러, 데이터 상태를 통합적으로 관리하고, autoFetch 옵션을 활용해 초기 데이터를 자동으로 불러올 수 있도록 구성했습니다!

2) 이번 단계에서 가장 많이 고민했던 문제와 해결 과정에서 배운 점

1️⃣ MSW 미사용 이유와 테스트 선택

MSW를 사용하면 테스트 작성이 더 수월해질 수는 있지만, 테스트 문법이나 다양한 상황을 깊이 있게 학습하기에는 적합하지 않다고 판단했습니다. 또한 실제 API를 연동해야 하는 상황에서는 MSW 없이 테스트가 모두 깨지는 문제가 발생할 수 있기 때문에, 오히려 사용하지 않는 방향을 선택하게 되었습니다.

물론 MSW를 사용하지 않으면서 불편한 점도 있었습니다. 예를 들어, 테스트 코드에서 mockImplementation를 지속적으로 조작하거나, render를 매번 호출해야 하는 점이 번거로웠습니다. 하지만 이러한 조작은 MSW를 사용하는 경우에도 결국 필요하다는 점에서, 테스트 흐름을 명확히 파악할 수 있는 직접적인 조작 방식을 선택했습니다.

2️⃣ 사용자 경험 개선

요구사항에는 명시되지 않았지만, 현재 장바구니 기능을 구현하면서 “사용자가 어떻게 하면 실제 구매까지 이어질 수 있을까?“에 대해 고민해보았습니다. 그 과정에서 가장 눈에 띄는 요소는 무료 배송 혜택이었고, 이를 강조하기 위해 progress bar를 활용한 시각적 요소를 추가해 사용자에게 동기를 부여하고자 했습니다.

3️⃣ 퍼널 기반의 페이지 흐름 관리

현재는 페이지가 2개뿐이지만, 추후 결제 페이지 등 추가될 가능성을 고려해 퍼널 형태로 페이지 흐름을 관리하도록 구현했습니다. 라우터를 사용하지 않고 상태 기반으로 흐름을 전환함으로써, 페이지 전환 간의 복잡도를 낮추고 내부 상태 관리에 집중할 수 있도록 했습니다.
또한, 주문 확인 페이지에서 뒤로 가기 버튼을 눌러 장바구니로 돌아왔을 때에도 기존 데이터(상품 체크 상태 등)가 초기화되지 않고 유지되도록 처리하여 사용자 경험을 고려했습니다.

3) 이번 리뷰를 통해 논의하고 싶은 부분

1️⃣ isChecked 개선 방향

현재는 데이터를 불러온 이후, useEffect를 활용해 체크된 항목들을 1차적으로 동기화하고 있습니다. 이때 최초 렌더링 여부를 파악하기 위해 useRef를 사용하여 렌더링에 영향을 주지 않도록 처리했습니다. 이후 toggle이나 remove 등의 사용자 인터랙션에 대해서는 useState로 관리되는 checkedItems를 지속적으로 갱신하고 있는데, 이 구조가 최적의 방식인지 고민이 들었습니다.

특히 처음 데이터를 불러온 이후의 동기화 타이밍, 그리고 이후 상태 업데이트 방식이 적절한지 확신이 서지 않아 개선 방향에 대한 조언을 구하고 싶습니다! 혹시 더 효율적인 동기화 또는 상태 관리 방식이 있을까요??

2️⃣ Props를 통한 상태 전달 구조

현재 장바구니 관련 상태와 함수는 useCart에서 관리하고, 이를 CartPage에서 하위 컴포넌트로 prop을 통해 전달하고 있습니다.

아직은 상태가 복잡하지 않다고 판단하여 props 기반으로 구현했지만, 향후 컴포넌트가 많아지고 상태가 늘어날 경우 Context API를 도입해 props drilling을 줄이는 것도 고려해볼 수 있다고 생각했습니다.

지금 단계에서 Context API를 도입하는 것이 좋을지, 리뷰어의 의견이 궁금합니다!

3️⃣ 관심사 분리

현재 useCart 훅은 데이터 불러오기부터 항목 토글, 삭제, 수량 변경 등 거의 모든 장바구니 관련 로직을 포함하고 있습니다. 그런데 이렇게 다양한 기능이 하나의 훅에 모두 포함되다 보니, 데이터 로직과 사용자 인터랙션 로직이 분리되지 않아 유지보수 측면에서 아쉬움이 있습니다.

관심사를 명확히 분리하기 위해 훅을 역할별로 나누는 방식을 고려하고 있는데, 그렇게 했을 때 서로 다른 훅 간에 필요한 데이터를 props나 매개변수로 계속 전달해야 할 가능성이 생겨 오히려 구조가 복잡해지지는 않을까 하는 고민도 있습니다. 이러한 상황에서 더 나은 구조를 만들 수 있는 방법이 있을지 조언을 듣고 싶습니다!


✅ 리뷰어 체크 포인트

1. 클라이언트 상태관리

  • 원본상태/파생상태를 적절히 구분하여 선언하였나요?
  • React state 를 불필요하게 선언한 부분은 없나요?
  • 상태 관리 로직의 책임이 적절히 응집/분리되었나요? (ex. reducer, hook, Context)

2. MSW/Test

  • 주요 기능을 적절히 정의하였나요?
  • 주요 기능/예외에 대한 테스트가 충분히 이루어졌나요?

keemsebin and others added 30 commits May 27, 2025 14:27
Copy link

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

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

안녕하세요 에리얼!
장바구니 미션을 함께하게 된 리뷰어 해먼드입니다 ㅎㅎ
앞으로 잘 부탁드려요!


컴포넌트를 무척 잘 분리해주셨어요!
props도 어색함 없이 정의를 너무 잘 해주셨네요 ㅎㅎ
주로 제가 네이밍에 대한 리뷰를 많이 남기는 편인데, 에리얼의 리뷰에는 네이밍에 대한 리뷰를 거의 남기지 않았네요 ㅋㅋ

그리고 재활용할 수 있는 부분을 최대한 잘 분리해주셔서 무척 보기 좋았습니다.
고생하셨어요!


1️⃣ isChecked 개선 방향

상세 피드백에 남겨놓긴 했는데요, 상태가 아니라 값으로 관리를 하시면 좋답니다 ㅎㅎ

  const checkedItems = useMemo(() => new Set(cart.data.map((item) => item.id)), [cart.data])  

이러면 cart.data 의 값이 변경될 때 자동으로 값을 만들어주는거죠.

2️⃣ Props를 통한 상태 전달 구조

이것도 상세 피드백에 남겨놓았는데요, 재사용을 기준으로 판단하시면 좋을 것 같아요!
재사용 단위가 CartList 라고 치면 그냥 컨텍스트를 사용하는게 제일 편할 것 같긴 합니다 ㅋㅋ

3️⃣ 관심사 분리

지금 모습도 나쁘진 않다고 생각해요.
여기서 더 나아가자면, 각각의 함수를 훅으로 분리할 수 있지 않을까 싶네요!

const useCart = () => {
  const toggle = useCartToggle();
  const update = useCartUpdate();
  ...
}

요로코롬..

다만 지금은 각각의 함수와 상태가 서로 얽혀있다보니 분리하기가 쉽진 않을 것 같습니다.. ㅠㅠ


마지막으로 lint, tsc 등이 현재 실패하고 있어서 한 번 확인 부탁드려요!
테스트도 통합 테스트만 있고, 단위 테스트가 없어서 아쉽네요 ㅠㅠ
그리고 실패하는 케이스에 대한 테스트가 없어서 이에 대한 부분도 채워지면 좋겠어요!

테스트 커버리지를 항상 잘 측정해주세요!

src/api/cart.ts Outdated
Comment on lines 1 to 2
import { CartResponse } from '@/features/Cart/types/Cart.types';
import { ProductQuery, UpdateCartItem } from '@/features/Cart/types/Product.types';

Choose a reason for hiding this comment

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

api 폴더에서 feature를 의존하고 있네요..!
그렇다면 차라리 cart라는 이 api가 아예 feature 폴더 내부에 있어야 하는게 아닐까요!?

Copy link
Author

Choose a reason for hiding this comment

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

cart 관련 api를 features/Cart 내부로 이동하여 응집도를 향상시켰습니다.

b128244

Comment on lines 59 to 64
const url = new URL(ENV.BASE_URL + path);
Object.entries(query || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && String(value)) {
url.searchParams.append(key, String(value));
}
});

Choose a reason for hiding this comment

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

쿼리를 관리하는 부분을 유틸 함수로 분리한 다음 테스트로 검증해도 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

쿼리 문자열 생성 로직을 buildQueryParams 유틸로 분리했습니다.
위치는 src/shared/utils/url.ts 입니다.

1dd5cd7

Comment on lines 66 to 69
const headers = {
'Content-Type': 'application/json',
Authorization: `Basic ${ENV.TOKEN}`,
};

Choose a reason for hiding this comment

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

이런건 DEFAT_HEADER 라는 상수로 만들어서 관리해도 좋아보입니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

반영했습니다!

725348c

Comment on lines 82 to 86
if (!response.ok) {
// const errorData = await response.json().catch(() => null);
// throw new HttpError(response.status, errorData);
throw new Error();
}

Choose a reason for hiding this comment

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

어땐 에러인지 명시해주면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

fetcher 내부에서의 에러 상황을 throw new Error() 대신, 커스텀 HttpError 클래스를 활용해 명확하게 처리하도록 개선했습니다.

01c15d5

Comment on lines +88 to +95
if (response.status === 204 || response.headers.get('content-length') === '0') {
if (returnOriginalOnNoContent && body) {
return body as unknown as T;
}
return returnOriginalOnNoContent as unknown as T;
}

return await response.json();

Choose a reason for hiding this comment

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

Suggested change
if (response.status === 204 || response.headers.get('content-length') === '0') {
if (returnOriginalOnNoContent && body) {
return body as unknown as T;
}
return returnOriginalOnNoContent as unknown as T;
}
return await response.json();
return await response.json();

어차피 응답이 빈 값일꺼라서, await response.json() 으로 바로 넘어가도 차이가 없지 않을까요!?

Copy link
Author

Choose a reason for hiding this comment

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

의견 주신 부분과 관련해서 궁금한 점이 있어 질문 드립니다!

바로 await response.json()을 호출하게 되면,
응답 본문이 {} 같은 빈 객체가 아니라 아예 없을 경우(204 No Contentcontent-length: 0),
SyntaxError: Unexpected end of JSON input 같은 에러가 발생하진 않을까요?

Choose a reason for hiding this comment

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

앗.. 그렇네요 ㅎㅎ
제가 잘못알고 있었네요 ㅠㅠ 죄송합니다.

Comment on lines 17 to 25
const hasCheckCartLength = cartItems?.filter((item) => item.isChecked).length;
const totalQuantity = cartItems?.reduce(
(acc, item) => acc + (item.isChecked ? item.quantity : 0),
0
);
const totalPrice = cartItems?.reduce(
(acc, item) => acc + (item.isChecked ? item.product.price * item.quantity : 0),
0
);

Choose a reason for hiding this comment

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

이런 것들도 훅으로 묶어줄 수 있겠네요!

Copy link
Author

Choose a reason for hiding this comment

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

OrderConfirm 컴포넌트 내 선택된 상품 수량, 총 수량, 총 결제 금액 계산 로직을 useOrderInfo 훅으로 분리했습니다.

a42e2e7

Comment on lines 13 to 20
const orderPrice = cartItems
.filter((item) => item.quantity > 0 && item.isChecked)
.reduce((acc, cart) => {
return acc + Number(cart.product.price) * Number(cart.quantity);
}, 0);

const deliveryFee = orderPrice >= 100000 ? 0 : 3000;
const totalPrice = orderPrice + deliveryFee;

Choose a reason for hiding this comment

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

앞선 리뷰와 동일합니다!

Copy link
Author

Choose a reason for hiding this comment

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

PriceSummary 컴포넌트 내부의 가격 관련 계산 로직을 usePriceInfo 훅으로 분리했습니다.

157a185

Comment on lines 7 to 30
export const CartPage = () => {
const { Funnel, setStep } = useFunnel<STEPS>('장바구니');
const { cartItems, toggleCheck, toggleAllCheck, removeCartItem, updateQuantity } = useCart();

return (
<>
<Funnel>
<Funnel.Step name="장바구니">
<CartInfo
cartItems={cartItems ?? []}
onToggle={toggleCheck}
onToggleAll={toggleAllCheck}
onRemove={removeCartItem}
onUpdateQuantity={updateQuantity}
onNext={() => setStep('주문정보')}
/>
</Funnel.Step>
<Funnel.Step name="주문정보">
<OrderConfirm cartItems={cartItems ?? []} onPrev={() => setStep('장바구니')} />
</Funnel.Step>
</Funnel>
</>
);
};

Choose a reason for hiding this comment

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

결국 cart에 대한 값이 전부 하위 컴포넌트로 props를 통해 전파되고 있어요.
이 때 고민할 수 있는 포인트는 이렇게 두 가지라고 생각합니다.

  1. cart ui 를 재활용 할 것인가? -> 그렇다면 현재 모습 유지
  2. cart ui 에 대한 재활용 의사가 없는가? -> 그렇다면 핵심 로직을 전부 context로 묶어서 전파하기. 전체적인 흐름을 이해하기가 더 수월함.

Copy link
Author

Choose a reason for hiding this comment

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

cart ui는 재사용 가능성이 낮을 것 같아 핵심 로직을 context로 묶었습니다!

7432bde

Comment on lines 19 to 32
const deliveryFee = orderPrice >= 100000 ? 0 : 3000;
const totalPrice = orderPrice + deliveryFee;

return (
<Flex
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
gap="10px"
width="100%"
padding="20px"
>
<Text type="Caption" weight="regular">
🛍️ 총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다.

Choose a reason for hiding this comment

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

100000 은 상수로 만들어서 관리해도 좋을 것 같네요!

Copy link
Author

Choose a reason for hiding this comment

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

반영했습니다!

37e78e3

Comment on lines 15 to 20
useEffect(() => {
if (cart.data && cart.data.length > 0 && !hasInitialized.current) {
setCheckedItems(new Set(cart.data.map((item) => item.id)));
hasInitialized.current = true;
}
}, [cart.data, checkedItems.size]);

Choose a reason for hiding this comment

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

checkdItems를 상태가 아니라 값으로 관리하면 어떨까요?

Suggested change
useEffect(() => {
if (cart.data && cart.data.length > 0 && !hasInitialized.current) {
setCheckedItems(new Set(cart.data.map((item) => item.id)));
hasInitialized.current = true;
}
}, [cart.data, checkedItems.size]);
const checkedItems = useMemo(() => new Set(cart.data.map((item) => item.id)), [cart.data])

이러면 useEffect를 사용할 필요가 없답니다 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

현재 체크 상태는 사용자 상호작용(개별/전체 토글 등)에 따라 동적으로 변경되기 때문에,
상태로 관리해야 유연하게 처리할 수 있다고 판단했습니다!

말씀해주신 useMemo 방식은 cart.data 변화에만 반응하기 때문에 사용자 조작이 반영되기는 힘들 것 같은데 맞을까요 ??

yeji0214 added 26 commits June 1, 2025 00:00
@yeji0214
Copy link
Author

yeji0214 commented Jun 4, 2025

안녕하세요 해먼드!
꼼꼼한 리뷰 감사합니다 ㅎㅎ
주신 피드백을 토대로 리팩토링을 진행했어요 🙂

코멘트로도 커밋 주소 달아두겠습니다 !

📦 리팩토링 내역 정리

🗂 폴더 구조 개선

  • cart 관련 API를 features/Cart/api 폴더로 이동하여 응집도 향상

🧩 로직 분리 및 커스텀 훅화

  • URL 쿼리 생성 로직을 buildQueryParams 유틸로 분리 (shared/utils/url.ts)
  • 계산 로직을 별도 커스텀 훅으로 추출
    • useCartInfo: 장바구니 선택/전체 개수, 합계 계산
    • useOrderInfo: 선택된 상품 수량, 총 금액 계산
    • usePriceInfo: 주문 금액, 배송비, 총 결제 금액 계산

📝 네이밍 및 구조 개선

  • Toast 메시지 전달 방식 개선 → children: ReactNode로 변경하여 유연성 확보
  • 로컬 상수(STEPS)는 외부 파일에서 제거하고 사용되는 컴포넌트 내부로 이동
  • index.tsx 대신 명확한 컴포넌트 이름으로 파일명을 수정

⚠️ 에러 처리 개선

  • fetcher 내부 에러를 커스텀 HttpError 클래스를 통해 명확하게 처리
  • mutate에서 fetch 대신 refetchFn를 직접 호출하고 성공 시 setData로 갱신하는 방식으로 변경

🧱 코드 구조 개선

  • createPortal의 두 번째 인자인 렌더 타겟을 parent prop으로 외부 주입 가능하도록 개선
  • showToast 함수에 duration 파라미터를 추가하여 유지 시간 설정 가능
  • 체크박스 컴포넌트에 input type="checkbox"요소 포함
  • StyledHeader의 정렬 방식은 외부에서 스타일 주입받도록 개선
  • 컴포넌트 props 주석을 제거하여 가독성 향상
  • alias(@) 기준으로 모든 import 경로를 통일

🧪 테스트

  • 실패 케이스 추가
  • 핵심 계산 로직 및 커스텀 훅에 대한 단위 테스트 추가

🧷 context API

  • Context.Provider에 넘기는 객체를 useMemo로 감싸서 불필요한 리렌더링 방지
  • 장바구니 UI 재사용 가능성이 낮다고 판단되어, 관련 로직을 Context에 통합하여 전역 상태로 관리

Copy link

@JunilHwang JunilHwang left a comment

Choose a reason for hiding this comment

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

안녕하세요 에리얼!
리뷰가 늦었네요 ㅠㅠ 죄송합니다.
전체적으로 잘 개선해주셔서 이번 단계는 여기서 마무리하고, 이어서 다음 단계 진행해주시면 될 것 같아요!

몇 가지 자잘한 리뷰 남겼으니 한 번 확인 부탁드립니다!

Comment on lines +88 to +95
if (response.status === 204 || response.headers.get('content-length') === '0') {
if (returnOriginalOnNoContent && body) {
return body as unknown as T;
}
return returnOriginalOnNoContent as unknown as T;
}

return await response.json();

Choose a reason for hiding this comment

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

앗.. 그렇네요 ㅎㅎ
제가 잘못알고 있었네요 ㅠㅠ 죄송합니다.

Comment on lines +5 to +9
constructor(status: number, data?: unknown) {
super(`HTTP Error ${status}`);
this.name = 'HttpError';
this.status = status;
this.data = data;

Choose a reason for hiding this comment

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

Suggested change
constructor(status: number, data?: unknown) {
super(`HTTP Error ${status}`);
this.name = 'HttpError';
this.status = status;
this.data = data;
constructor(public status: number, public data?: unknown) {
super(`HTTP Error ${status}`);
this.name = 'HttpError';

아마 이렇게 정의하면 자동으로 할당될 것 같아요!
javascript에서는 안 되고, typescript 에서만 가능한 문법이랍니다 ㅎㅎ

}, duration);
};

const contextValue = useMemo(() => ({ showToast }), [showToast]);

Choose a reason for hiding this comment

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

지금 여기서 contextValue에 메모를 하는게 의미가 없어요.
어차피 showToast는 항상 새로운 값일테니까요..!
어째서 그런걸까요~?

Comment on lines +48 to +55
direction={direction}
justifyContent={justifyContent}
alignItems={alignItems}
gap={gap}
margin={margin}
padding={padding}
width={width}
height={height}

Choose a reason for hiding this comment

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

gap 빼고 다 props로 그대로 넘겨줘도 될 것 같네요!

Comment on lines +31 to +35
useEffect(() => {
if (cart.error && isError(cart.error)) {
showToast('장바구니 정보를 불러올 수 없습니다.');
}
}, [cart.error, showToast]);

Choose a reason for hiding this comment

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

showToast를 의존하고 있는데요,
앞선 리뷰에 남긴 것 처럼 toast의 값이 변경 될 때 마다 showToast가 매번 변경되고,
showToast가 변경되면 ToastContext를 사용하는 구간이 다 렌더링되고,
이 useEffect 에서도 showToast를 의존하고 있어서 불필요하게 계속 실행될 가능성이 있어요..!

Comment on lines +30 to +33
const imgUrl =
!product.imageUrl || product.imageUrl === '' || product.imageUrl.includes('kream')
? NoImage
: product.imageUrl;

Choose a reason for hiding this comment

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

이런 부분도 유틸 함수로 만들어주면 어떨까 싶어요!

css={css`
margin-top: 8px;
`}
onClick={() => onRemove(id)}

Choose a reason for hiding this comment

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

삭제를 클릭하는거라
onRemove가 아니라
onRemoveClick 처럼 표현해야 적절할 것 같네요!

Comment on lines +98 to +99
onIncrease={() => onUpdateQuantity(id, quantity + 1)}
onDecrease={() => onUpdateQuantity(id, quantity - 1)}

Choose a reason for hiding this comment

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

onUpdateQuantity 라는 이름이 내포하는건 이렇습니다.

  1. 현재 컴포넌트 내부에 quantity에 대한 상태가 정의되어 있고
  2. 해당 quantity를 변경하는 함수가 있고,
  3. 그 함수를 실행하여 quantity를 변경할 때 onUpdateQuantity 가 실행됨
const [quantity, setQuantity] = useState(0);
const updateQuantity = (newQuantity) => {
  setState(newQuantity);
  onUpdateQuantity(id, newQuantity);
}

이런 모습이랄까..

input의 이벤트에 대해 잘 고민해보시면
input은 자체적으로 value를 가지고 있어요
그리고 onChange 같은 이벤트가 발생하면 자신의 value를 event에 담아서 전달한답니다 ㅎㅎ

그래서 이벤트란 무엇일지 한 번 고민해보시면 좋아요!

이 상황에서는 어떻게 네이밍을 해야 좋을까요?

padding="20px"
>
<Text type="Caption" weight="regular">
🛍️ 총 주문 금액이 100,000원 이상일 경우 무료 배송됩니다.

Choose a reason for hiding this comment

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

FREE_DELIVERY_THRESHOLD 을 여기서도 사용할 수 있지 않을까요!?

Comment on lines +121 to +127
<CartItemDetail
key={item.id}
onToggle={onToggle}
onRemove={onRemove}
onUpdateQuantity={onUpdateQuantity}
{...item}
/>

Choose a reason for hiding this comment

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

각각의 CartItemDetail 이 지금은 매번 렌더링 될 수 밖에 없는 상황인데요,
필요할 때만 렌더링 되도록 만들어주려면 어떤식으로 개선이 되어야 좋을까요!?

@JunilHwang JunilHwang merged commit aca118d into woowacourse:yeji0214 Jun 6, 2025
1 check passed
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.

3 participants