Skip to content

[6팀 조영민] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #34

Open
0miiii wants to merge 49 commits intohanghae-plus:mainfrom
0miiii:main
Open

[6팀 조영민] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #34
0miiii wants to merge 49 commits intohanghae-plus:mainfrom
0miiii:main

Conversation

@0miiii
Copy link

@0miiii 0miiii commented Aug 4, 2025

배포링크: https://0miiii.github.io/front_6th_chapter2-2/

과제의 핵심취지

  • React의 hook 이해하기
  • 함수형 프로그래밍에 대한 이해
  • 액션과 순수함수의 분리

과제에서 꼭 알아가길 바라는 점

  • 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
  • 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
  • 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
  • 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)

기본과제

  • Component에서 비즈니스 로직을 분리하기

  • 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기

  • 뷰데이터와 엔티티데이터의 분리에 대한 이해

  • entities -> features -> UI 계층에 대한 이해

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • 특정 Entitiy만 다루는 함수는 분리되어 있나요?

  • 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?

  • 데이터 흐름에 맞는 계층구조를 이루고 의존성이 맞게 작성이 되었나요?

심화과제

  • 재사용 가능한 Custom UI 컴포넌트를 만들어 보기

  • 재사용 가능한 Custom 라이브러리 Hook을 만들어 보기

  • 재사용 가능한 Custom 유틸 함수를 만들어 보기

  • 그래서 엔티티와는 어떤 다른 계층적 특징을 가지는지 이해하기

  • UI 컴포넌트 계층과 엔티티 컴포넌트의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 Hook과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

  • 엔티티 순수함수와 유틸리티 함수의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

과제 셀프회고

과제를 하면서 내가 제일 신경 쓴 부분은 무엇인가요?

과제를 다시 해보면 더 잘 할 수 있었겠다 아쉬운 점이 있다면 무엇인가요?

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)

아래와 같이 설계된 컴포넌트에 대해서 어떻게 생각하시는지 궁금합니다.
Cart 컴포넌트는 장바구니에 담긴 상품 목록을 렌더링하는 컴포넌트입니다.

처음에는 Cart 내부에서 상품제거, 상품증가, 감소 처리를 했었는데, 이것을 컴포넌트 내에서 직접 처리하지 않고 장바구니에 담긴 상품만 보여주는 역할만 하도록 구현해봤습니다. 그래서 props로 cart 데이터를 전달하고 이벤트는 선택한 상품을 인자로 전달해주면서 실행시키도록만 하고 아무 기능을 하지 않도록 했습니다.

이렇게 구현하면 제거, 증가, 감소 버튼을 클릭했을 때 모달을 표시한다든지, 값을 1증감이 아닌 2증감으로 변경한다든지 이런 추가 작업이 발생할 때 Cart 컴포넌트는 수정할일이 없을거라고 생각했습니다.

신경쓰이는 부분은 onRemove를 동작시켰을 때, 외부에서 삭제시키는 로직을 포함한 함수를 전달하지 않으면, 이벤트 이름에 맞는 기능을 동작하지 않게 됩니다.

이처럼 cart 데이터를 보여주고 기능은 전달받은 핸들러를 통해 실행시키는 방법과, Cart 컴포넌트 내부에서 삭제, 증가, 감소를 구현해놓는 방법 중 어떤것이 더 좋은 설계인지 받고싶습니다.

interface Props {
  cart: CartItem[];
  onRemove: (product: CartItem) => void;
  onIncrease: (product: CartItem) => void;
  onDecrease: (product: CartItem) => void;
}

const Cart = ({ cart, onRemove, onDecrease, onIncrease }: Props) => {
  return (
    <section className="bg-white rounded-lg border border-gray-200 p-4">
      <h2 className="text-lg font-semibold mb-4 flex items-center">
        <svg
          className="w-5 h-5 mr-2"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
          />
        </svg>
        장바구니
      </h2>
      {cart.length === 0 ? (
        <div className="text-center py-8">
          <svg
            className="w-16 h-16 text-gray-300 mx-auto mb-4"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={1}
              d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
            />
          </svg>
          <p className="text-gray-500 text-sm">장바구니가 비어있습니다</p>
        </div>
      ) : (
        <div className="space-y-3">
          {cart.map((item) => {
            const itemTotal = calculateItemTotal(item);
            const originalPrice = item.product.price * item.quantity;
            const hasDiscount = itemTotal < originalPrice;
            const discountRate = hasDiscount
              ? Math.round((1 - itemTotal / originalPrice) * 100)
              : 0;

            return (
              <div
                key={item.product.id}
                className="border-b pb-3 last:border-b-0"
              >
                <div className="flex justify-between items-start mb-2">
                  <h4 className="text-sm font-medium text-gray-900 flex-1">
                    {item.product.name}
                  </h4>
                  <button
                    onClick={() => onRemove(item)}
                    className="text-gray-400 hover:text-red-500 ml-2"
                  >
                    <svg
                      className="w-4 h-4"
                      fill="none"
                      stroke="currentColor"
                      viewBox="0 0 24 24"
                    >
                      <path
                        strokeLinecap="round"
                        strokeLinejoin="round"
                        strokeWidth={2}
                        d="M6 18L18 6M6 6l12 12"
                      />
                    </svg>
                  </button>
                </div>
                <div className="flex items-center justify-between">
                  <div className="flex items-center">
                    <button
                      onClick={() => onDecrease(item)}
                      className="w-6 h-6 rounded border border-gray-300 flex items-center justify-center hover:bg-gray-100"
                    >
                      <span className="text-xs"></span>
                    </button>
                    <span className="mx-3 text-sm font-medium w-8 text-center">
                      {item.quantity}
                    </span>
                    <button
                      onClick={() => onIncrease(item)}
                      className="w-6 h-6 rounded border border-gray-300 flex items-center justify-center hover:bg-gray-100"
                    >
                      <span className="text-xs">+</span>
                    </button>
                  </div>
                  <div className="text-right">
                    {hasDiscount && (
                      <span className="text-xs text-red-500 font-medium block">
                        -{discountRate}%
                      </span>
                    )}
                    <p className="text-sm font-medium text-gray-900">
                      {Math.round(itemTotal).toLocaleString()}
                    </p>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </section>
  );
};

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

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.

안녕하세요! PR 잘 정리하셨네요. 리팩토링 방향(비즈니스 로직 ↔ UI 분리, models/hooks/pages/components 분리)이 명확하게 반영되어 있습니다. 아래는 요청하신 형식(시나리오 · 변화 예상 · 응집도/결합도 평가 · AS-IS/TO-BE 코드 예시)을 반영한 종합 피드백과 상세 피드백입니다. PR을 Github 리뷰로 바로 붙여넣을 수 있게 마크다운 형식으로 작성했습니다.


안내: 이 피드백은 코드 가독성/구조/아키텍처 중심의 리뷰입니다. 구현 세부 동작(버그 리포트)은 일부 예시로만 지적했습니다.

질문(간단 답변)

  • 과제 셀프회고 / 내가 신경 쓴 부분 / 리뷰 받고 싶은 내용이 비어있습니다. 아래 종합 피드백(2)에서 '셀프회고를 쓸 때 도움이 되는 포인트'와 '리뷰 받을 만한 항목 추천'을 적어두었습니다. 참고하셔서 PR 본문에 채우시면 리뷰 품질과 의사소통이 좋아집니다.

종합 피드백

  1. PullRequestFiles의 핵심 키워드 (요약)
  • 모듈화: models/ (cart/product/discount/coupon), hooks/, utils/, pages/, components/
  • 책임 분리: UI(Page/Component) ↔ 순수 로직(models) ↔ 상태·부가기능(hooks)
  • 로컬 퍼시스트: useLocalStorage + utils/storage
  • 알림 시스템: useNotifications + NotificationContainer
  • 검색 디바운스: useDebounce
  • 아직 남아있는 결합 지점: 상태(장바구니, products, coupons)를 props로 전달하는 방식 / addNotification을 props로 전달
  1. PullRequestBody의 '과제 셀프회고' / '내가 제일 신경 쓴 부분'에 대한 피드백 (인사이트 + 추가 질문)
  • 인사이트
    • 리팩토링의 핵심 가치는 '변경에 대한 비용(파일 수정 범위)을 줄이는 것'과 '재사용성/테스트 가능성'입니다. 이번 PR은 models로 비즈니스 로직을 모아 '순수함수'로 바꾼 점, hooks로 자주 쓰는 행동(로컬 스토리지, 알림, 디바운스)을 분리한 점이 우수합니다.
    • UI 계층(ShopPage/AdminPage/Header/NotificationContainer)이 비즈니스 로직에 직접 접근하지 않고 props/훅으로 의존을 주입받도록 바뀐 것도 좋은 방향입니다.
  • 이어 생각해볼 질문
    • "어떤 단위(모듈)를 npm 패키지로 떼어내면 유지보수·테스트·배포 측면에서 가치가 클까?" — models(비즈니스 로직)과 utils는 후보입니다. UI는 앱 주변부로 남겨두기 좋습니다.
    • "현재 props로 주입하는 패턴과 전역 상태(store)를 쓰는 패턴 중 어떤 변화가 더 적은 비용으로 전환될까?" — 아래 세부 섹션에서 시나리오로 설명합니다.
    • "알림/로컬스토리지 같은 cross-cutting concern은 전역으로 관리할까, 훅으로 각 모듈에서 사용하게 할까?" — trade-off(적용 범위·테스트성) 를 고민해보세요.
  1. PullRequestBody의 '리뷰 받고 싶은 내용'에 대한 권장 답변 (PR 본문에 적으면 좋은 항목)
  • "이 모듈 API(예: models/cart.ts의 addToCart/updateQuantity)가 충분히 직관적인가요?" — 함수 시그니처(입력/출력)가 명확한지 의견을 구하세요.
  • "useNotifications를 전역(컨텍스트)으로 바꾸는 게 나을까요?" — 스케일(앱 전체에서 같은 알림을 쓰는 경우)과 테스트성(단위테스트에서 목(mock) 주입)을 고려해 물어보세요.
  • "pages 컴포넌트가 아직 props로 너무 많은 것을 받는데, 커스텀 훅으로 추출하면 더 좋아질까요?" — 동의 여부와 코드 예시 요청.

상세 피드백

먼저 피드백에 사용할 개념 정의 (요청하신 정의 기반)

  • 응집도(cohesion) — 여기서는 "변경에 대한 파일/코드의 추가/수정/삭제 등에 대한 동선이 얼마나 짧은가" + "라이브러리(패키지)로 만들 때 매끄럽게 떼어낼 수 있는가" 로 정의합니다.

    • 높은 응집도: 특정 역할(예: 장바구니 비즈니스 로직)과 관련된 코드가 한 곳(models/cart)에 모여 있어 관련 변경 시 한 파일(또는 한 모듈)만 수정하면 된다.
    • 낮은 응집도: 동일 도메인 로직이 여러 파일에 흩어져 있어 작은 변경에도 여러 파일을 수정해야 하는 구조.
  • 결합도(coupling) — "함수와 함수, 컴포넌트와 컴포넌트가 인터페이스(추상화)를 통해 결합되어 있는가"를 봅니다.

    • 낮은 결합도: 명확한 함수/훅 API로 의존성이 주입(주입된 인터페이스로 대체 가능)되어 교체가 쉬움.
    • 높은 결합도: 구체 구현(예: addNotification 이름을 직접 요구)이나 전역 상태 구현에 강하게 의존.

아래 각 문제 항목에 대해: 개념정의 → 문제 정의 → AS-IS → TO-BE(코드 포함) 형태로 정리합니다.

1) 상태관리 라이브러리(예: jotai/zinzustand/tanstack-query)로 바꿔야 하는 경우 — 시나리오·영향·개선안

정의

  • 문제: 현재 상태(cart/products/coupons/selectedCoupon)는 App에서 useLocalStorage/useState로 관리한 뒤 여러 컴포넌트(ShopPage/AdminPage/Header 등)에 props로 전달하고 있음. 전역 상태 라이브러리로 바꾸면 '상태 소스'가 바뀌므로 props 주입부/상태를 사용하는 컴포넌트가 영향을 받음.

AS-IS (핵심)

  • ShopPage/AdminPage가 setCart/setProducts/setCoupons 같은 setState를 props로 받음.
  • addNotification도 props로 내려감.

예: ShopPage 컴포넌트 일부 시그니처 (AS-IS)

interface Props {
  products: Product[];
  cart: CartItem[];
  coupons: Coupon[];
  setCart: React.Dispatch<React.SetStateAction<CartItem[]>>;
  setProducts: React.Dispatch<React.SetStateAction<ProductWithUI[]>>;
  addNotification: (message: string, type: 'error'|'success') => void;
  ...
}

문제(영향)

  • 전역 상태로 바꾸면 모든 컴포넌트에서 props로 넘기던 부분을 제거하거나 대체해야 함.
  • 현재는 props 주입이 많아 바꾸는 범위가 넓음(여러 컴포넌트/페이지 수정 필요).

TO-BE (권장)

  • '상태를 사용하는 측'을 바꾸지 않으려면 중간 훅(useCart/useProducts/useCoupons)을 만들고 App에서 전역 스토어를 래핑시킴. 컴포넌트는 기존 훅을 통해 상태를 소비 → 전환 비용 최소화.
  • 이상적으로는 components/pages는 상태를 직접 다루지 않고, custom hooks를 통해 추상화. 그러면 상태 저장소 교체는 hook 내부만 수정하면 됨.

예: useCart 훅 인터페이스 (TO-BE)

// useCart.ts
export const useCart = () => {
  return {
    items,                // CartItem[]
    totalCount,           // number
    totals,               // { totalBeforeDiscount, totalAfterDiscount }
    add: (product) => { /* add */ },
    remove: (productId) => { /* remove */ },
    updateQuantity: (product, qty) => { /* update */ },
    applyCoupon: (coupon) => { /* apply */ },
  };
};
  • 위 훅 내부 구현만 바꾸면(예: useState+localStorage → Zustand store) 컴포넌트 측(ShopPage 등)은 바꿀 필요 없음.

예: Zustand로 전환 예시 (간단)

// store/cartStore.ts (Zustand)
import create from 'zustand';
import { addToCart as addToCartPure, removeFromCart as removeFromCartPure } from '../models/cart';

type CartState = {
  cart: CartItem[];
  add: (product: Product) => { success: boolean; reason?: string };
  remove: (productId: string) => void;
  setCart: (cart: CartItem[]) => void;
};

export const useCartStore = create<CartState>((set, get) => ({
  cart: [],
  add: (product) => {
    const result = addToCartPure(get().cart, product);
    if (result.success) set({ cart: result.cart });
    return result;
  },
  remove: (productId) => set({ cart: removeFromCartPure(get().cart, productId) }),
  setCart: (cart) => set({ cart }),
}));
  • 이후 useCart 훅은 useCartStore를 래핑하여 동일 API를 제공하면 앱 전체 변경 최소화.

결론(비용)

  • 현재 구조는 models가 "순수 함수"로 작성되어 있어 상태 저장소를 바꾸는 데 실제 로직 변경 필요는 적음(낮은 비용). 다만 현재 props로 주입하는 부분을 hook으로 묶어두면 전환 비용은 더욱 낮아집니다.

2) 모듈화를 하여 패키지로 배포해야 하는 경우 — 응집도/결합도 평가

정의(재언급)

  • 응집도: 관련 코드가 한 모듈에 모여있어 변경시 영향을 줄이는 정도.
  • 결합도: 모듈간 의존 관계의 강도.

문제 정의

  • 어떤 단위(모듈)를 떼어낼지 판단해야 함. 제품 도메인(models) / hooks / UI 중 어느 것이 "독립적으로 재사용 가능"한가?

AS-IS

  • models 폴더: cart/product/coupon/discount 등 도메인 로직이 잘 분리되어 있고, 대부분 순수 함수. -> 응집도 높음, 패키징 후보 우수.
  • hooks: useLocalStorage/useDebounce/useNotifications — 범용 훅들로 재사용 가능. 응집도도 괜찮음.
  • utils: storage/validation/notification utils — 훅과 함께 묶어 배포 가능.
  • components/pages: UI에 강하게 의존(스타일/클래스 등), 앱 특화(테마/레이아웃) → 패키징 시 분리 권장.

TO-BE (패키징 제안)

  • shop-core (패키지 A)
    • 포함: src/models/* (cart/product/discount/coupon), types (엔티티 타입), 순수 유틸(applyCoupon 등)
    • 이유: 순수 비즈니스 로직만 포함하면 다른 프로젝트에서도 재사용 가능.
  • ui-hooks (패키지 B)
    • 포함: useLocalStorage, useDebounce, useNotifications, NotificationContainer? (NotificationContainer는 UI 의존이 있으므로 별도 패키지로 분리할지 결정)
  • ui-components (패키지 C) — 선택적
    • Header, NotificationContainer 같은 재사용 가능한 컴포넌트만 분리.

포장 시 고려사항

  • public API 명확화: 예) export { addToCart, calculateCartTotal, calculateItemTotal } from 'shop-core'
  • 사이드 이펙트 최소화: 패키지에 window/localStorage 접근을 직접 넣지 말고, 주입 가능한 저장소 추상화 제공(예: StorageAdapter 인터페이스)
  • 타입 보장: types를 명확히 export

응집도 판단(요약)

  • models: 응집도 높음 → 패키지 난이도 낮음
  • hooks/utils: 응집도 보통 — 범용성 높으므로 분리 가치 있음
  • UI: 응집도 낮음(앱-특화) → 패키지화 시 많은 커스터마이징 필요

3) 구체적 결합도 문제(알림/에러처리 패턴) — 개선 제안

정의

  • "onSuccess/onError" 패턴을 통해 결합도를 낮추자(요구사항에도 명시된 예).

문제

  • 지금 코드에서는 addNotification을 props로 전달하는 경우가 있음. 이는 사용하는 쪽이 알림 구현에 의존함.
    • 예: AdminPage has addNotification prop; ShopPage too.

AS-IS 예

// App -> AdminPage
<AdminPage addNotification={addNotification} ... />

문제점

  • addNotification 함수명이 하드코딩되어 호출자에게 '알림을 어떻게 보여주는지'를 강제(높은 결합도).
  • useNotifications hook을 전역화하거나, useCart 훅 내부에서 addNotification을 직접 호출하면 재사용성이 떨어짐.

TO-BE 제안 1: 명확한 콜백을 사용하여 낮은 결합도로 변경

  • API: useCart.add accepts optional callbacks rather than assuming notification system.

사용 예 (TO-BE)

// useCart 내부/호출부
const { add } = useCart();
add(product, {
  onSuccess: () => onSuccess?.('장바구니에 담았습니다'),
  onError: (reason) => onError?.(reason),
});
  • 이렇게 하면 알림을 전달하는 책임은 호출자(페이지)가 가짐. 반대로 별도 전역 알림 사용 시엔 useNotifications를 직접 훅에서 사용.

TO-BE 제안 2: useNotifications는 전역 훅 또는 컨텍스트로 제공

  • 장점: 앱 전체에서 동일한 알림 동작 보장, 호출부 단순화
  • 단점: 유닛테스트 시 알림 훅 모킹 필요

권장: 둘 다 지원

  • useCart API는 콜백(onSuccess/onError)을 옵션으로 제공하고, 내부적으로는 useNotifications를 호출하지 않음. 그러면 test/패키징 시 순수함수처럼 유지 가능.

4) Good: 순수함수화 및 모델 분리 — 칭찬 포인트

  • models/ 폴더에 비즈니스 로직(장바구니 로직, 쿠폰 로직, 상품 관련 유틸)이 잘 응집되어 있습니다.
  • calculateItemTotal / calculateCartTotal / addToCart 등 함수는 입력(상태)을 받아 출력(새 상태 or 금액)을 반환하는 형태로 작성되어 테스트하기 좋습니다.
  • utils/storage의 isEmptyValue 처리를 통한 localStorage 저장 전략(빈값 제거)도 일관된 정책으로 좋습니다.

5) 개선 권장 사항(우선순위 순서)

  1. Introduce abstracted hooks for state groups

    • useCart, useProducts, useCoupons를 만들어 App/Pages에서 직접 setState props를 다루지 않게 변경.
    • Benefit: 상태관리 라이브러리 전환 시 훅 내부만 수정하면 됨.
  2. Reduce prop drilling for addNotification

    • 두 옵션: (A) useNotifications를 전역(컨텍스트)으로 제공 → 컴포넌트 내부에서 바로 사용 (B) useCart/useProducts 같은 훅이 onSuccess/onError 콜백을 인자로 받도록 API 설계
    • 권장: 훅 API에서 콜백을 옵션으로 제공 + 앱 레벨에서 useNotifications를 사용하는 조합.
  3. Storage adapter 추상화

    • setStorageItem/getStorageItem는 현재 localStorage를 직접 사용. 패키징 시 대체 가능하도록 StorageAdapter 인터페이스를 만들면 유용(예: localStorage | indexedDB | server).
  4. 문서화 & 테스트

    • models 함수마다 간단한 유닛 테스트 추가 (계산 함수, 쿠폰 적용, addToCart 경계조건 등).
    • public API 문서(README) 작성.
  5. 타입 엄격성 & null-safe 처리

    • formatPrice(productId?: string) → product 찾기 로직의 undefined 처리(이미 잘 되어있음). 하지만 일부 컴포넌트에서 product 목록과 타입이 혼합되어 있으니 타입 일관성을 재검토.

구체적 AS-IS / TO-BE 코드 스니펫 (직접 적용 가능한 예시)

  1. AS-IS: App이 상태와 setState를 props로 내려주는 패턴
// App.tsx (AS-IS - 일부)
const [cart, setCart] = useLocalStorage<CartItem[]>("cart", []);
...
<ShopPage
  products={products}
  coupons={coupons}
  cart={cart}
  setCart={setCart}
  addNotification={addNotification}
/>

TO-BE: useCart 훅을 만들고 컴포넌트에서 훅을 직접 사용 (props 제거)

// hooks/useCart.ts
import { useLocalStorage } from './useLocalStorage';
import * as cartModel from '../models/cart';

export const useCart = () => {
  const [cart, setCart] = useLocalStorage<CartItem[]>('cart', []);

  const add = (product: Product) => {
    const result = cartModel.addToCart(cart, product);
    if (result.success) setCart(result.cart);
    return result;
  };

  const remove = (productId: string) => setCart(cartModel.removeFromCart(cart, productId));
  const updateQuantity = (product: Product, qty: number) => {
    const result = cartModel.updateQuantity(cart, product, qty);
    if (result.success) setCart(result.cart);
    return result;
  };

  const totals = cartModel.calculateCartTotal(cart, null);

  return { cart, add, remove, updateQuantity, totals };
};

그리고 ShopPage에서는

// ShopPage.tsx (TO-BE)
const { cart, add, remove, updateQuantity, totals } = useCart();
  • 이로써 App에서 setCart prop을 빼고 ShopPage는 전혀 변경 없이 useCart를 사용하게 할 수 있음.
  1. AS-IS: addNotification을 props로 전달
// AdminPage receives addNotification prop and calls it directly
addNotification('상품이 수정되었습니다.', 'success');

TO-BE: 명시적 콜백 또는 전역 훅 사용

  • 방법 A (콜백 제공)
// useCart.add 사용 시
add(product, {
  onSuccess: () => addNotification('장바구니에 담겼습니다', 'success'),
  onError: (reason) => addNotification(reason, 'error')
});
  • 방법 B (전역 훅)
// useCart 내부에서 useNotifications를 쓰지 않고, 컴포넌트에서 직접 훅을 사용
const { addNotification } = useNotifications();
const { add } = useCart();

const handleAdd = (product) => {
  const result = add(product);
  if (result.success) addNotification('장바구니에 담았습니다', 'success');
  else addNotification(result.reason, 'error');
};

결론 요약 (한 문장)

  • 전반적으로 좋은 리팩토링: 모델(비즈니스 로직)과 UI 분리가 잘 되었고, 순수 함수로 작성되어 테스트/패키징에 적합합니다. 다음 단계는 "상태 접근을 hook으로 추상화"하여 전역 상태 라이브러리 교체 비용을 더 낮추고, 알림/저장소 같은 cross-cutting concern을 추상화하여 결합도를 더 낮추는 것입니다.

체크리스트(권장 작업)

  • useCart / useProducts / useCoupons 훅 추가로 props 드릴링 제거
  • addNotification 전달 방식 통일(콜백 옵션 또는 전역 훅)
  • StorageAdapter(추상화) 설계: localStorage 교체를 쉽게
  • models에 대한 단위 테스트 추가 (calculateItemTotal, addToCart edge case 등)
  • 패키징 범위 결정: shop-core(models+types), hooks, ui-components 분리 제안

필요하시면 다음을 도와드리겠습니다.

  • useCart/useProducts 훅의 구체 구현 템플릿(현재 코드 기반) 제공
  • Zustand / Jotai 전환 예시 코드(현재 구조에서 변경해야 할 라인 요약)
  • shop-core 패키지 구조(파일 목록 + export API 샘플)

원하시는 것을 알려주세요.

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