Skip to content

[1단계 - 장바구니] 클레어(안은소) 미션 제출합니다. #364

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 57 commits into from
Jun 5, 2025

Conversation

eunsoA
Copy link

@eunsoA eunsoA commented May 29, 2025

안녕하세요 쵸파! 🥸
이번 상품 목록 미션의 리뷰이, 클레어입니다! 🍀

이번 미션은 써밋과 함께 페어로 진행했어요 👻
마지막 미션인 만큼 잘 하고 싶었는데 몇가지 아쉬움도 남는 것 같네요 ㅎㅎ

그럼 이번 미션 잘 부탁드립니다! 🙇‍♂️

📦 장바구니 미션

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

1단계

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

🕵️ 셀프 리뷰(Self-Review)

제출 전 체크 리스트

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

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

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

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

1) 상태 설계 의도

  • 페프를 시작하기 전에 우선 요구사항에서 필요한 상태를 아래와 같이 설계했습니다!

    변하는 UI 요소 이 UI가 언제 변하나요? UI 반영을 위해서 어떤 데이터가 필요한가요? (ex. 장바구니 상품 목록의 개수, 배송비) 상태 이름 (변수명) 자료구조 (타입) 종류 (원본/파생)
    장바구니 상품 리스트 페이지를 처음 열었을 때 장바구니 상품 목록 cartItems CartItem[] 원본
    장바구니 상품 목록의 종류(개수) 상품 목록의 종류가 변할 때(삭제 버튼 클릭시, 상품빼기로 상품의 개수가 0이 되었을 때) 장바구니 상품 목록의 종류 cartItemsLength number 파생
    전체 선택 체크박스 페이지를 처음 열었을 때, 상품이 전체 선택되었을 때 주문 리스트 isAllChecked boolean 파생
    장바구니 상품 체크 박스 체크박스를 클릭했을 때, 전체 선택 체크박스가 클릭됐을 때 장바구니 상품 목록 isChecked boolean 원본
    장바구니 상품 개수 상품 빼기, 추가 버튼 클릭했을 때 장바구니 상품 개수 cartItemQuantity number 파생
    주문 금액 체크 박스 클릭, 상품 빼기, 추가 버튼 클릭 시 장바구니 상품 목록과 각 상품의 개수 orderPrice number 파생
    배송비 주문금액이 10만원 이상 또는 미만일 때 주문 금액 정보 deliveryPrice number 파생
    총 결제 금액 주문금액 또는 배송비 값에 변동이 있을 때 주문 금액, 배송비 totalPrice number 파생
    주문 확인 버튼 주문 할 상품이 있거나 없을 때 주문 리스트 isOrderActive boolean 파생
    • Custom Hook 기반 상태 관리 계층화

      • useFetchData: 서버 상태 관리 (API 호출, 로딩, 에러 처리)
      • useMutation: 서버 상태 변경 작업 관리 (생성, 수정, 삭제)
      • useCartItem: 장바구니 비즈니스 로직 통합 관리 (클라이언트 + 서버 상태)
    • 상태 흐름 설계

      • 서버 데이터 (cartItems) -> 클라이언트 상태 (orderList) -> 파생 상태 (isAllChecked, orderTotalPrice 등) -> UI 렌더링
  • 위 상태 설계에 맞춰서 아래와 같이 컴포넌트를 분리해서 설계했습니다!

    • CartPage
      image
    • OrderSuccessPage
      image

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

🌀 컴포넌트 설계

  • 이번 미션에서는 요구사항에서 컴포넌트를 먼저 설계하고 페프를 진행했지만, 구현을 진행하면서 CartItems을 api 요청으로 받아오는 로직으로 인해, 컴포넌트 초기 설계에서 수정되었어요.
  • bottom 에 있는 OrderConfirmButtonCartContent 내부로 넣은 것이 가장 큰 변동이예요.
  • 현재 컴포넌트 설계에 대한 가독성이 좋은지, 구조에 대해 아쉬운 점은 어떤 것인지 쵸파의 의견이 궁금해요 !

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

🌀 상태 관리

  • 현재는 CartContent에서 useCartItem라는 훅으로 CartPage의 모든 상태와 이벤트 핸들러가 관리되고 있는데요, 너무 비대하다는 생각도 들어요. 이를 분리하는 것에 대해. 어떻게 생각하시는지, 쵸파의 의견이 궁금해요!
  • 또한 현재 이런 상태 관리에서 단점이나 문제 상황이 존재할지도 좀 더 알고 싶어요. 우선 제 눈에는 props drilling이 제일 큰 문제인 것 같아보여요.

🌀 isAllChecked의 상태 관리

  • 아마 이게 이번 미션에서 모두가 공통적으로 고민했던 내용인 것 같아요.
  • 주문 목록의 전체 선택을 관리하는 상태인 isAllCheckedorderList라는 주문 목록 리스트가 cartItems와 일치하는지로 판단하는 파생 상태로 구현했어요. 그러나 orderListcartItems와 중복되는 정보를 들고 있기 때문에 SSOT를 위배된다고도 보여져요.
  • 지금 생각해보면 isAllCheckedorderListcartItems로 비교하는게 아니라
    api 요청으로 받은 cartItemsisChecked라는 필드를 추가하고,
    cartItems내부 item들을 돌면서 모두 isChecked가 true일때 isAllChecked가 true가 될 수 있도록 상태 관리를 했을 것 같아요 .. ! (현재는 첫번째 방식으로 구현했어요.)
  • 이에 대한 쵸파의 의견을 듣고 수정하고 싶어요.

그럼 이번 미션 잘 부탁드립니다!
좋은 하루 보내세요 : >


✅ 리뷰어 체크 포인트

1. 클라이언트 상태관리

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

2. MSW/Test

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

woowa-euijinkk and others added 30 commits May 27, 2025 14:21
@eunsoA eunsoA changed the title sdf [1단계 - 장바구니] 클레어(안은소) 미션 제출합니다. May 29, 2025
@bassyu bassyu self-assigned this May 31, 2025
Copy link
Member

@bassyu bassyu left a comment

Choose a reason for hiding this comment

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

안녕하세요 클레어! 만나서 반가워요!
리뷰가 늦어져서 죄송합니다! 🙇

현재 컴포넌트 설계에 대한 가독성이 좋은지, 구조에 대해 아쉬운 점은 어떤 것인지 쵸파의 의견이 궁금해요 !

코드 전반적으로 적절하게 분리, 응집되어있다고 느꼈어요!
가독성이 좋아서 필요한 코드를 찾고 이해하기에 좋았습니다! 👍 👍

테스트도 적절하게 작성 해주셨네요!

고민해주신 부분도 코멘트로 답변 드렸습니다!


1단계 수고 많으셨습니다!
코멘트 확인 후 다시 요청해주세요~! 💪 💪


return (
<S.ButtonWrapper>
{quantity === 1 ? (
Copy link
Member

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.

으하핫 페어의 의견이었는데 좋게 봐주셔서 감사합니다! :>

errorMessage,
refetch: refetchCartItems,
} = useFetchData({ fetchFn: getCartItems });
const [orderList, setOrderList] = useState<CartItemType[]>([]);
Copy link
Member

Choose a reason for hiding this comment

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

주문 목록의 전체 선택을 관리하는 상태인 isAllChecked를 orderList라는 주문 목록 리스트가 cartItems와 일치하는지로 판단하는 파생 상태로 구현했어요. 그러나 orderList는 cartItems와 중복되는 정보를 들고 있기 때문에 SSOT를 위배된다고도 보여져요.

상품 중에서, 주문 하고 싶은 일부만 선택하는 것이기 때문에, 새로운 상태나 필드가 필요한 것은 어쩔 수 없는 것 같아요!

다만 CartItemType을 전부 관리하진 않아도 될 것 같은데요, 선택한 id만 상태로 관리해보면 어떨까요?

api 요청으로 받은 cartItems에 isChecked라는 필드를 추가하고,

이 방법도 좋을 것 같아요! 조금 조심해야 할 부분이 있는데요.

만약 또 다른 곳에서 서버 상태를 사용할 때 isChecked가 서버 타입인지 아닌지 헷갈릴 수도 있습니다.
서버 타입과 클라이언트 타입을 엄밀하게 구분해서 관리하는 것이 중요할 것 같아요!

또는 여러 곳에서 모두 isChecked가 필요하긴 한데, 서로 다른 지역 상태를 유지해야 한다면 isChecked만 SSOT에 어울리지 않을 수도 있습니다!

Copy link
Author

Choose a reason for hiding this comment

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

말씀해주신 내용을 정리해보면, 두가지 방식이 있을것 같아용.

  1. orderIdList로 주문할 목록의 id만 상태로 관리
  2. clientCartItemsisChecked 필드 포함

두가지 방식 중에 저는 전자를 택해서 수정했어요. ( commit : 88dfaee )

🌀 여기서 한가지 궁금한 점이 있는데요!
이렇게 되면 현재 useCartItem.ts에서 서버 데이터인 cartItems와 클라이언트 데이터인 orderIdList가 함께 쓰이고 있어요. 제 생각에 서버 데이터와 클라이언트 데이터는 분리되어 있어야 할 것 같은데요.
같은 파일 내에서 쓰인다면 네이밍으로 둘을 분리하는 것도 방법일 것 같아요.

이에 대해 쵸파만의 주관이나 의견이 있는지 궁금해요! 🙋‍♂️🙋‍♂️🙋‍♂️

Copy link
Member

Choose a reason for hiding this comment

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

넵! 1번 방법에서 상태 관리 관심사가 크지 않다면,
저도 그냥 같은 파일 안에서 네이밍으로 분리하는 것이 좋아보입니다~!

Comment on lines 14 to 21
const isFirstLoad = useRef(true);

useEffect(() => {
if (cartItems && isFirstLoad.current) {
setOrderList(cartItems);
isFirstLoad.current = false;
}
}, [cartItems]);
Copy link
Member

Choose a reason for hiding this comment

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

지금처럼 ref를 사용하거나, 다른 방법으로는 useFetchData 에게 직접 onFetch 같은 콜백을 넘겨줄 수도 있겠네요~!

Copy link
Author

@eunsoA eunsoA Jun 4, 2025

Choose a reason for hiding this comment

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

음 이 피드백 질문주신 내용이 잘 이해가 안돼요.

기존에 있던 useCartItem이 사라지면서 현재 useOrderSelection에서 useRef(true)로 첫 로드를 감지하는 방식을 사용하고 있는데요! isFirstLoad을 useRef로 관리하는 건 똑같아요.

export const useOrderSelection = (cartItems: CartItemType[] | undefined) => {
  const [orderIdList, setOrderIdList] = useState<OrderItemType>([]);
  const isFirstLoad = useRef(true);

  useEffect(() => {
    if (cartItems && cartItems.length > 0 && isFirstLoad.current) {
      setOrderIdList(cartItems.map((item) => item.id));
      isFirstLoad.current = false;
    }
  }, [cartItems]);

지금처럼 ref를 쓰지않고 useFetchData에 직접 onFetch 콜백을 넘겨주는 방법은 뭔지 궁금해욥
어떻게 개선하면 좋을지 조금만 힌트를 주실 수 있을까요 ? 🥺

Copy link
Member

Choose a reason for hiding this comment

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

이런식으로 onFetch가 있다면, 추가적인 useEffect 없이 상태를 초기화 할 수 있겠다고 생각했어요~!

const fetchCartItems = async () => {
  try {
    const data = await getCartItems();
    setCartItems(data);

    onFetch?.(data);
    // onFetch: (cartItems) => { setOrderIdList(cartItems.map((item) => item.id)); }
  } catch (error) {
  }
};

Comment on lines 21 to 25
useEffect(() => {
if (!location.state) {
navigate(ROUTES.CART);
}
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

이런 디테일도 좋네요! 조금 더 해보자면,

특정 페이지의 state를 관리하고, 필요한 state만 줄 수 있도록 유도하는 장치가 있으면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

페이지의 state 관리를 위해 아래와 같이 수정해봤어요! (commit : b273ff0)

// 수정 전
const navigateToOrderSuccess = (state: OrderSuccessState) => {
  navigate(ROUTES.ORDER_SUCCESS, { state });
};

// 수정 후
const navigateTo = <T extends RouteState>(route: string, state?: T) => {
  navigate(route, state ? { state } : undefined);
};
  • usePageNavigation 커스텀 훅 추가
  • 페이지 이동 시 state 타입 검증 로직 통합
  • CartContent, OrderSuccessPage 네비게이션 처리 방식 통일

이에 대한 쵸파의 의견이 궁금해요..! 너무 과도한 공통화일까요?

Copy link
Member

Choose a reason for hiding this comment

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

앗 원래 수정 전 처럼 추상화 되어있었나요? 수정 전이 안전해보입니다!

저는 특정 페이지에서 쓰이는 state 필드가 존제한다면, navigate를 할 때 그 페이지에만 필요한 state만 줄 수 있으면 좋겠다는 생각이 들었어요.
나중에 페이지가 많아지면 헷갈리기도 하고, state를 줄 때 오타가 날 수도 있어서요~!

그래서 수정 전의 모습처럼, 각각의 페이지마다 navigate 훅이 있으면 어떨지? 제안 드려봤습니다!

Comment on lines 59 to 71
return {
cartItems,
isLoading,
errorMessage,
refetchCartItems,
orderList,
isAllChecked,
orderTotalPrice,
toggleAllCheckBox,
addOrderItem,
removeOrderItem,
updateOrderItem,
};
Copy link
Member

Choose a reason for hiding this comment

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

또한 현재 이런 상태 관리에서 단점이나 문제 상황이 존재할지도 좀 더 알고 싶어요. 우선 제 눈에는 props drilling이 제일 큰 문제인 것 같아보여요.

좋은 고민이네요! 서버 데이터 관련해서 적용 해보면 어떨까요?
어디서든 api 호출이 가능하지만, 그 호출이 특정 상태에 전반적으로 영향을 주니까요~!

이전 미션에서도 context를 넣을지 고민해보셨을 것 같은데요, 이전 미션에서는 어떻게 구분했었는지도 궁금합니다!

Copy link
Author

@eunsoA eunsoA Jun 4, 2025

Choose a reason for hiding this comment

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

우선 문제상황을 분석하고 아래와 같이 해결해봤어요.


🌀 문제 상황

  1. Props Drilling 문제

    // 현재: 너무 많은 props 전달
    <CartList
      cartItems={cartItems}
      orderIdList={orderIdList}
      refetchCartItems={refetchCartItems}
      addOrderItemId={addOrderItemId}
      removeOrderItemId={removeOrderItemId}
    />
  2. 서버 데이터가 전반적으로 영향을 미침
    : 개별 아이템 변경 시마다 전체 목록을 다시 가져오고, 이것이 전체 상태를 초기화시킴

    // 🔄 CartItem.tsx - 아이템 삭제할 때
    const removeCartItem = async () => {
      await removeCartItemMutate(undefined);  // 서버에서 1개 아이템 삭제
      refetchCartItems();                     // 👈 전체 목록을 다시 가져옴!
    };
    
    // 🔄 QuantityButton.tsx - 수량 변경할 때  
    const updateQuantity = async () => {
      await updateCartItemMutate({id, quantity});  // 서버에서 1개 아이템 수량 변경
      refetchCartItems();                          // 👈 전체 목록을 다시 가져옴!
    };
    • 불필요한 네트워크 요청: 1개만 바뀌어도 전체를 다시 가져옴
    • 상태 초기화: 선택된 아이템들(orderIdList)이 리셋될 수 있음
  3. useCartItem이 4가지 책임을 갖음. 책임과 역할을 적절히 분리하는것이 필요하겠다고 생각됐어요.

    • 서버 데이터 관리 (cartItems, isLoading, errorMessage, refetchCartItems)
    • 선택 상태 관리 (orderIdList, addOrderItemId, removeOrderItemId)
    • 계산 로직 (orderTotalPrice, isAllChecked)
    • UI 로직 (toggleAllCheckBox)

🌀 해결 방안

폴더구조는 아래와 같이 구성하고

// 폴더 구조
src/pages/cart/
├── CartPage.tsx
├── contexts/
   └── CartServerContext.tsx    🆕 서버 상태 관리
├── hooks/
   ├── useOrderSelection.ts     🆕 주문 선택 상태
   └── useOrderCalculation.ts   🆕 가격 계산 로직
└── CartContent/

데이터 흐름을 아래와 같이 수정했어요.

// 변경 전
아이템 삭제  서버 삭제  전체 목록 다시 가져오기  모든 컴포넌트 리렌더링

// 변경 후
아이템 삭제  서버 삭제  로컬 상태만 업데이트  필요한 컴포넌트만 리렌더링

서버 상태와 클라이언트 상태를 분리하고, 각자의 책임을 명확히 하고자 아래와 같이 상태를 분리했어요.

📦 장바구니 상태 관리
├── 🌐 CartContext (cartItems, isLoading, errorMessage)  서버 데이터 (전역 상태로 어디서든 접근 가능, 실시간 동기화 필요)
├──  useOrderSelection (orderIdList, isAllChecked) 클라이언트상태 (로컬)  
└── 🧮 useOrderCalculation (orderTotalPrice, paymentAmount)  계산 로직 (순수함수)

결과적으로 서버 상태는 Context로, 클라이언트 상태는 명시적 props로 관리하여서 위에 언급한 문제를 해결하고자 했어요!

Copy link
Author

Choose a reason for hiding this comment

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

여기서 드는 궁금점은 위에 orderIdList 관련하여 질문했던 것처럼 서버 상태와 클라이언트 상태를 이렇게 분리하는 것이 괜찮은지, 쵸파의 의견이 궁금해요.

두번째로 useOrderCalculation로 계산로직을 분리했는데 shared/utils로 계산로직을 빼고, 이걸 사용하고 있어요. 이 로직에 대해서도 쵸파의 의견이 궁금해요.

// 현재 : useOrderCalculation로 계산 로직 분리
const { orderTotalPrice, deliveryFee, paymentAmount } = useOrderCalculation(
    cartItems,
    orderIdList,
  );

// shared/utils/orderPricing의 계산 로직 함수를 사용
const { deliveryFee, paymentAmount } = calculatePaymentInfo(orderTotalPrice);

Copy link
Member

Choose a reason for hiding this comment

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

  1. 적절한 이유로 context 사용해주신 것 같습니다! 👍 👍
  2. 이전에 SSOT 관련해서 말씀 드린 것처럼, 지금은 사용처가 1곳밖에 없지만, 로컬 상태를 따로 정의해주는 방법도 좋다고 생각합니다!
  3. 계산 부분을 utils로 뺀 것 자체는 좋아보입니다! (domain으로 봐도 괜찮겠네요)
    갠적으론 함수 크기는 덜 쪼개봐도 좋아보이네요~!

Comment on lines 26 to 32
if (isLoading) {
return <div>로딩중</div>;
}

if (errorMessage) {
return <div>에러남</div>;
}
Copy link
Member

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.

네! 아래와 같이 반영해서 수정했어요! 🫡🫡 (commit : c15f537)

  • 로딩 화면을 위한 로딩컴포넌트 (commit : 25f6336)
  • 에러 화면을 위한 에러 컴포넌트 (commit : 9da7e57)

} = useCartItem();
const navigate = useNavigate();

if (isLoading) {
Copy link
Member

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.

네! 반영해서 수정했어요! (commit : c814464)

  if (isLoading && !cartItems?.length) {
    return <div>로딩중</div>;
  }

그런데 피드백 요청한 목적에 맞는 수정방안인지 모르겠어요.
쵸파는 어떻게 생각하시나요?

Copy link
Member

Choose a reason for hiding this comment

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

충분히 좋습니다! 👍 👍

다른 방법으로는, 첫 로딩시에만 변하는 isFirstLoading상태를 만들고, 항상 변하는 isLoading 상태도 만들 수 있겠네요!
아니면 아예 요청을 보내지 않도록 캐싱하는 방법도 생각납니다!

} finally {
setIsLoading(false);
}
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

어떤 상태에 의해 fetchFn의 동작이 달라진다고 해도 괜찮을까요?

Copy link
Author

@eunsoA eunsoA Jun 4, 2025

Choose a reason for hiding this comment

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

말씀해주신 피드백 관련해서 현재 상황이 변경되었는데요!

프로젝트 구조가 변경되어 useFetchData 훅 대신 CartContext를 사용하는 방식으로 리팩토링했어요. 현재는 CartContext에서 데이터 페칭과 상태 관리를 모두 담당하고 있어서, useFetchData 훅은 더 이상 사용되지 않고 있어요.

혹시 useFetchData 훅을 다시 활용하는 방향으로 검토해보면 좋을지, 아니면 현재처럼 Context 기반으로 유지하는 게 좋을지 쵸파의 의견이 궁금합니다! 😊

Copy link
Member

Choose a reason for hiding this comment

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

이전엔 deps에 fetchFn을 넣어주지 않아서 걱정되는 상황이었는데, 지금은 괜찮겠네요!

refetchCartItems,
removeOrderItem,
}: RemoveCartItemButtonProps) {
const { mutate: removeCartItemMutate } = useMutation(() => deleteCartItem(cartItemId));
Copy link
Member

Choose a reason for hiding this comment

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

훅으로 정의 해주신 만큼, isLoading 상태도 활용해보면 좋을 것 같아요~!

Copy link
Author

Choose a reason for hiding this comment

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

아 윗 코멘트를 반영하면서 현재 RemoveCartItemButton 컴포넌트는 useMutation 훅을 사용하는 방식에서 CartContext를 사용하는 방식으로 변경됐어요.

// 수정 전
const { mutate: removeCartItemMutate } = useMutation(() => deleteCartItem(cartItemId));

// 수전 후
const { removeItem } = useCartContext();

useMutation 훅도 useFetchData와 마찬가지로 프로젝트 구조가 Context 기반으로 변경되면서 더 이상 사용되지 않고 있어요. 따라서 isLoading 상태를 활용하는 것에 대한 피드백을 반영하기 위해서는 현재 Context 구조에 맞는 다른 접근이 필요할 것 같습니다.
혹시 Context 기반의 현재 구조에서 로딩 상태를 어떻게 관리하면 좋을지 힌트를 주실 수 있을까요? 👀👀

Copy link
Member

Choose a reason for hiding this comment

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

메서드 + url을 키로 활용해서 로딩 상태 Map을 관리하는 방법이 생각나네요~!

아니면 context안에서 useMutation같은 범용적인 훅을 활용하는 방법도 생각납니다!

const { mutate: update, isLoading: isLoadingUpdate } = useMutation(updateCartItem)
const { mutate: remove, isLoading: isLoadingRemove } = useMutation(deleteCartItem)

return <Provider value={{ update, isLoadingUpdate, remove, isLoadingRemove }}>{children}</Provider>

Copy link

coderabbitai bot commented Jun 4, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@eunsoA
Copy link
Author

eunsoA commented Jun 4, 2025

안녕하세요 쵸파!
휴일 잘 보내고 계신가요?

이것저것 생각해볼거리가 많은 피드백을 주셔서 생각해보고 .. 반영이 조금 늦어졌습니닷

그리고 사실 저는 코멘트를 다 써놓고 한번에 우르르 코멘트 작성 버튼을 클릭하는데요 ... 코멘트를 다 써놓은 상태에서 새로고침했더니 코멘트가 전부 날아가버려서 .. 다시 쓰느라 시간이 더 걸렸네요 .. 🥲🥲🥲 (앞으로는 코멘트는 작성하면 바로바로 보내야겠다는 걸 .. 배웠습니닷 ... 🥺)


🌀 type vs interface

  • 이번 페어프로그래밍을 하면서 문득 type과 interface의 차이가 뭔지 모르고 쓰고있다는걸 알았어요. 그래서 step1 피드백을 반영하면서 관련해서 공부해봤어요! type vs interface
  • API response와 같이 객체 구조가 명확하고, 추후 확장 가능성이 있다면 interface를 사용하고, 복잡한 타입 조합이 필요하다면 type을 사용하고자 했어요.
  • 페프 당시에는 type으로 모두 통일했어서 interface와 type을 적절하게 목적에 맞도록 사용하고자 수정했어요! (commit : b6b4dc8)
  • 적절하게 수정한 것이 맞는지 이에 대한 쵸파의 의견이 궁금합니다!

🌀 orderIdList로 주문할 목록의 id만 상태로 관리 vs clientCartItems에 isChecked 필드 포함

  • 두가지 방식중에 저는 전자를 택해서 수정했어요.
  • 위 댓글에 수정 사안과 함께 질문도 남겨놓았습니다!

🌀 서버 상태와 클라이언트 상태로 분리

  • 기존에는 아래와 같이 useCartItem이라는 커스텀훅에서 모두 관리됐었어요.
// 하나의 훅에서 모든 것을 담당
export const useCartItem = () => {
  // 서버 상태
  const { data: cartItems, refetch: refetchCartItems } = useFetchData({ fetchFn: getCartItems });
  
  // 클라이언트 상태  
  const [orderIdList, setOrderIdList] = useState([]);
  
  // 주문 금액 계산 로직
  const orderTotalPrice = cartItems?.reduce(...);
  const isAllChecked = [...cartIds].every(...);
  
  // UI 로직
  const toggleAllCheckBox = () => {...};

  // 모든 것을 반환 😰
  return {
    cartItems, isLoading, errorMessage, refetchCartItems,  // 서버 상태
    orderIdList, isAllChecked, toggleAllCheckBox,          // 클라이언트 상태  
    orderTotalPrice, addOrderItemId, removeOrderItemId     // 계산 + UI
  };
};
  • 현재 CartContext에서는 서버 상태만 관리하도록 분리했어요.
// Before: 모든 것을 useCartItem이 담당
const everything = useCartItem(); // 10개 이상의 반환값

// After: 역할별로 분리
const { cartItems } = useCartContext();           // 서버 상태만
const { orderIdList } = useOrderSelection();      // 클라이언트 상태만  
const { totalPrice } = useOrderCalculation();     // 계산만
  • 위 코멘트에서도 썼지만, 결과적으로 서버 상태는 Context로, 클라이언트 상태는 명시적 props로 전달하여 분리했어요. (commit : 2b04ab1) 이에 대한 쵸파의 의견이 궁금해요 ! 👀

  • 또한 이와 함께 수량 조절에 대해 매번 전체 refactch하지 않도록 수정했어요.

// Before: 매번 전체 refetch
const updateQuantity = async () => {
  await updateAPI();
  refetchCartItems(); // 🐌 전체 데이터 다시 가져오기
};

// After: 스마트 로컬 업데이트  
const updateItemQuantity = async (id, quantity) => {
  await updateAPI(id, quantity);
  setCartItems(prev => prev.map(...)); // ⚡ 해당 아이템만 수정
};

🌀 낙관적 업데이트

  • 이건 따로 피드백 내용은 아닌데 다른 크루들이 낙관적 업데이트에 관해서 이야기하는 것을 듣고 궁금해서 적용해봤어요!
  • 낙관적 업데이트가 실제로 사용되는지, 제가 사용한 방식이 맞는지 궁금해요
  const updateItemQuantity = async (id: number, quantity: number) => {
    // 원본 백업
    const originalItems = cartItems;
   
    // 즉시 UI 업데이트
    setCartItems((prev) => prev.map((item) => (item.id === id ? { ...item, quantity } : item)));
    try {
      await updateCartItemQuantity({ id, quantity });
    } catch (error) {
      // 실패시 되돌리기
      setCartItems(originalItems);
      setErrorMessage(error instanceof Error ? error.message : '수량 변경에 실패했습니다.');
    }
  };

🌀 페이지 네비게이션 로직 공통화

  • usePageNavigation 커스텀 훅 추가하여 페이지 이동 시 state 타입 검증 로직 통합했어요. (commit : b273ff0)

이외에도 추가로 위에서 언급해주신 내용 모두 반영하였습니다!
요즘 캠퍼스 내에 감기가 계속 돌고 있어서 인지 저도 감기 몸살로 컨디션이 메롱이네요 .. 🤒
쵸파는 건강유의하시길 바랍니닷 💪💪💪

그럼 좋은 하루 보내세요! 감사합니다! :>

Copy link
Member

@bassyu bassyu left a comment

Choose a reason for hiding this comment

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

안녕하세요 클레어~!

고민 많이 해주시고, 리팩터링 잘 해주셨네요! 👍 👍

적절하게 수정한 것이 맞는지 이에 대한 쵸파의 의견이 궁금합니다!

취향에 따라 다를 것 같아요~! 클레어가 정리해주신 것처럼 휴리스틱하게 interface를 사용할 수도 있죠!
저는 갠적으로, API 타입 정의는 확장성 보다는 엄격한 것이 안전하다고 봐서, type을 좋아합니다!

낙관적 업데이트

에러까지 고려해서 적절하게 구현 해주셨네요! 👍

최근 개발한 기억으로는, 즉각 반영이 중요한 케이스가 아니라면
기본적으론 백엔드와 sync가 일치하는 것에 집중하고 있습니다!

서비스와 그 기능에 따라서 많이 다를 것 같아요~!


다른 질문들도 코멘트로 답변 남겼습니다~!
남긴 코멘트는 2단계에서 이어서 고민해주세요!

1단계는 이만 머지하겠습니다!
2단계도 파이팅입니다~! 💪 💪

errorMessage,
refetch: refetchCartItems,
} = useFetchData({ fetchFn: getCartItems });
const [orderList, setOrderList] = useState<CartItemType[]>([]);
Copy link
Member

Choose a reason for hiding this comment

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

넵! 1번 방법에서 상태 관리 관심사가 크지 않다면,
저도 그냥 같은 파일 안에서 네이밍으로 분리하는 것이 좋아보입니다~!

Comment on lines 21 to 25
useEffect(() => {
if (!location.state) {
navigate(ROUTES.CART);
}
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

앗 원래 수정 전 처럼 추상화 되어있었나요? 수정 전이 안전해보입니다!

저는 특정 페이지에서 쓰이는 state 필드가 존제한다면, navigate를 할 때 그 페이지에만 필요한 state만 줄 수 있으면 좋겠다는 생각이 들었어요.
나중에 페이지가 많아지면 헷갈리기도 하고, state를 줄 때 오타가 날 수도 있어서요~!

그래서 수정 전의 모습처럼, 각각의 페이지마다 navigate 훅이 있으면 어떨지? 제안 드려봤습니다!

} finally {
setIsLoading(false);
}
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

이전엔 deps에 fetchFn을 넣어주지 않아서 걱정되는 상황이었는데, 지금은 괜찮겠네요!

refetchCartItems,
removeOrderItem,
}: RemoveCartItemButtonProps) {
const { mutate: removeCartItemMutate } = useMutation(() => deleteCartItem(cartItemId));
Copy link
Member

Choose a reason for hiding this comment

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

메서드 + url을 키로 활용해서 로딩 상태 Map을 관리하는 방법이 생각나네요~!

아니면 context안에서 useMutation같은 범용적인 훅을 활용하는 방법도 생각납니다!

const { mutate: update, isLoading: isLoadingUpdate } = useMutation(updateCartItem)
const { mutate: remove, isLoading: isLoadingRemove } = useMutation(deleteCartItem)

return <Provider value={{ update, isLoadingUpdate, remove, isLoadingRemove }}>{children}</Provider>

} = useCartItem();
const navigate = useNavigate();

if (isLoading) {
Copy link
Member

Choose a reason for hiding this comment

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

충분히 좋습니다! 👍 👍

다른 방법으로는, 첫 로딩시에만 변하는 isFirstLoading상태를 만들고, 항상 변하는 isLoading 상태도 만들 수 있겠네요!
아니면 아예 요청을 보내지 않도록 캐싱하는 방법도 생각납니다!

Comment on lines 14 to 21
const isFirstLoad = useRef(true);

useEffect(() => {
if (cartItems && isFirstLoad.current) {
setOrderList(cartItems);
isFirstLoad.current = false;
}
}, [cartItems]);
Copy link
Member

Choose a reason for hiding this comment

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

이런식으로 onFetch가 있다면, 추가적인 useEffect 없이 상태를 초기화 할 수 있겠다고 생각했어요~!

const fetchCartItems = async () => {
  try {
    const data = await getCartItems();
    setCartItems(data);

    onFetch?.(data);
    // onFetch: (cartItems) => { setOrderIdList(cartItems.map((item) => item.id)); }
  } catch (error) {
  }
};

Comment on lines 59 to 71
return {
cartItems,
isLoading,
errorMessage,
refetchCartItems,
orderList,
isAllChecked,
orderTotalPrice,
toggleAllCheckBox,
addOrderItem,
removeOrderItem,
updateOrderItem,
};
Copy link
Member

Choose a reason for hiding this comment

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

  1. 적절한 이유로 context 사용해주신 것 같습니다! 👍 👍
  2. 이전에 SSOT 관련해서 말씀 드린 것처럼, 지금은 사용처가 1곳밖에 없지만, 로컬 상태를 따로 정의해주는 방법도 좋다고 생각합니다!
  3. 계산 부분을 utils로 뺀 것 자체는 좋아보입니다! (domain으로 봐도 괜찮겠네요)
    갠적으론 함수 크기는 덜 쪼개봐도 좋아보이네요~!

@bassyu bassyu merged commit 640c022 into woowacourse:eunsoa Jun 5, 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