Skip to content

[8팀 박창준] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#40

Open
ckdwns9121 wants to merge 65 commits intohanghae-plus:mainfrom
ckdwns9121:main
Open

[8팀 박창준] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#40
ckdwns9121 wants to merge 65 commits intohanghae-plus:mainfrom
ckdwns9121:main

Conversation

@ckdwns9121
Copy link
Member

@ckdwns9121 ckdwns9121 commented Aug 5, 2025

배포링크

배포링크

과제의 핵심취지

  • 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과 라이브러리 훅과의 계층의 성격이 다르다는 것을 이해하고 적용했는가?

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

과제 셀프 회고

이번 과제는 개인적으로 시간 투자를 많이 하지 못한 점이 아쉽습니다. 최근 취업 준비에 더 많은 시간을 할애해야겠다는 필요성을 느끼고 있었고, 특히 오프코치님의 멘토링을 들으면서 시간 분배의 중요성에 대해 더욱 체감하게 되었습니다. 요즘은 거의 과제 2, 취업 준비 8 정도의 비율로 시간을 쓰고 있는 것 같아요.

과제는 수요일 저녁쯤 시작했는데, 다행히 리액트에는 익숙해서 진입 장벽은 높지 않았습니다. 특히 수많은 props drilling을 경험하면서 이를 어떻게 개선할 수 있을지 고민해보는 과정이 나름 재미있었고, 리팩토링을 통해 코드 구조를 조금씩 개선해보는 시도도 흥미로웠습니다.

아직 부족한 점이 많지만, 이런 과정을 통해 실제 현업에서의 코드 구조나 설계 방식에 대해 더 깊이 고민해볼 수 있었던 점이 의미 있었던 것 같습니다. 다음 과제는 더 넉넉하게 시간을 투자해서, 조금 더 완성도 있는 결과물을 만들어보고 싶습니다.

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

1. 디자인패턴을 도입하여 요구사항 변화에 유연하게 대응해보기

이번 과제에서 가장 신경 썼던 부분은 디자인 패턴을 어떻게 적용해볼 수 있을까에 대한 고민이었습니다.

준일 코치님의 요구사항의 변화로 알아가는 클린코드 글을 굉장히 감명깊게 읽었는데요. 그 관점을 가지고 해당 부분을 신경써서 개발해보고 싶었습니다. (작은 태스크라도 한번 시도해보고 싶었어요)

우선 요구사항을 한번 정의해보면

  1. 어드민 페이지에서 탭으로 “상품 관리” 탭과 “쿠폰 관리” 탭이 있다.
  2. “상품 관리”탭을 누르면 상품관리 할 수 있는 콘텐츠가, “쿠폰 관리”를 누르면 쿠폰 관리를 할 수 있는 콘텐츠 가 나온다.

그래서 초기에는 아래와 같이 조건부 렌더링을 사용해서 구현했습니다.

{tab === "상품관리" && <상품관리탭 />}
{tab === "쿠폰관리" && <쿠폰관리탭 />}

만약 여기서 유저관리, 재고관리, 리뷰관리 등등 새로운 탭을 추가해달라는 요구사항이 발생하면 어떻게 될까요

{tab === "상품관리" && <상품관리탭 />}
{tab === "쿠폰관리" && <쿠폰관리탭 />}
{tab === "유저관리" && <유저관리탭 />}
{tab === "재고관리" && <재고관리탭 />}
{tab === "리뷰관리" && <리뷰관리탭 />}

아마 이런식으로 구현해볼 것 같았습니다.
이 방식도 간단하고 직관적이지만, 탭이 많아질수록 컴포넌트가 길어지고 복잡도가 증가할 수 있다는 점에서 구조적으로 불만족스러웠습니다.

그래서 리팩토링을 통해 합성 컴포넌트(Compound Components) 패턴을 적용해보았습니다. Tabs라는 추상화된 UI 컴포넌트를 중심으로, 각 탭의 TriggerPanel을 독립적인 방식으로 구성할 수 있도록 구조를 바꿨습니다.

<Tabs defaultValue={ADMIN_TABS.PRODUCTS}>
  <Tabs.List>
    <Tabs.Trigger value={ADMIN_TABS.PRODUCTS}>상품 관리</Tabs.Trigger>
    <Tabs.Trigger value={ADMIN_TABS.COUPONS}>쿠폰 관리</Tabs.Trigger>
  </Tabs.List>

  <Tabs.Content>
    <Tabs.Panel value={ADMIN_TABS.PRODUCTS}>
      <ProductManagement ...props />
    </Tabs.Panel>

    <Tabs.Panel value={ADMIN_TABS.COUPONS}>
      <CouponManagement ...props />
    </Tabs.Panel>
  </Tabs.Content>
</Tabs>

이 방식의 장점은 무엇보다도 구조가 명확하다는 점입니다.

탭의 구성 요소(List, Trigger, Panel)가 명확히 분리되어 있어 코드의 가독성과 유지보수성이 뛰어나고, 새로운 탭을 추가하거나 수정할 때도 전체 구조를 쉽게 파악할 수 있습니다.

또한 Tabs 컴포넌트 내부에서 상태를 관리하기 때문에, 별도의 상태 선언 없이도 UI 전환이 가능해지고, 기능별 컴포넌트를 따로 나눠서 독립적으로 관리할 수 있어 관심사 분리 관점에서도 큰 장점이었습니다.

하지만 이것 역시 탭이 많아지면 유연하게 잘 처리할 수 있는 구조일까? 고민했습니다..

제 결론은 컴포넌트가 굉장히 길어지고 가독성이 매우 안좋아질 것 같았어요. 더 나아가 Router를 추가해서 탭이 변경되면 URL도 변경되는 요구사항을 받으면 어떻게 해야할까.. 이런 고민도 했습니다.

이 둘을 유연하게 처리할 수 있는 방식을 고민해보다가 기능 목록을 객체 배열추상화하는 것이였습니다.

const adminFeatures: AdminFeature[] = [
  {
    id: ADMIN_TABS.PRODUCTS,
    label: "상품 관리",
    component: ProductManagementFeature,
  },
  {
    id: ADMIN_TABS.COUPONS,
    label: "쿠폰 관리",
    component: CouponManagementFeature,
  },
  {
    id: ADMIN_TABS.INVENTORY,
    label: "재고 관리",
    component: InventoryManagementFeature,
  },
];

이런식으로요. 이렇게 추상화하면, 각 기능을 새로운 탭에 추가하거나 재배치할 때 페이지 컴포넌트 구조를 건드리지 않아도 되기 때문에 매우 유연하게 관리할 수 있습니다. 또 각 component는 독립적인 컴포넌트로 관리되기 때문에 재사용성과 가독성 모두 좋아졌습니다.

한가지 발생한 문제점은 props가 다른 경우였는데요. 이 부분은 각 Feature 레이어에서 jotai를 활용해 전역상태를 다루고 있었기 때문에 생각보다 쉽게 해결할 수 있었습니다. (직접적인 해결방법은 아닌데 객체에서 render 함수 명시하고 직접 넘기는 방식으로 해결해볼수 있을 것 같아요)

<Tabs defaultValue={adminFeatures[0].id}>
  <Tabs.List>
    {adminFeatures.map((feature) => (
      <Tabs.Trigger key={feature.id} value={feature.id}>
        {feature.label}
      </Tabs.Trigger>
    ))}
  </Tabs.List>

  <Tabs.Content>
    {adminFeatures.map((feature) => (
      <Tabs.Panel key={feature.id} value={feature.id}>
        <feature.component />
      </Tabs.Panel>
    ))}
  </Tabs.Content>
</Tabs>

또하나 궁금했던 건 UI 구조를 Router 기반으로 전환하고 싶을 때, 이 패턴이 얼마나 확장성이 좋을까? 였습니다. 다행히도 adminFeatures 형태의 구조는 라우팅 전환에도 유리했습니다.

예를 들어 아래와 같이 Route 배열을 뽑아내는 것도 어렵지 않았습니다.

// React Router와 연동
const AdminRouter = () => {
  return (
    <Routes>
      {adminFeatures.map((feature) => (
        <Route
          key={feature.id}
          path={`/admin/${feature.id}`}
          element={<feature.component />}
        />
      ))}
    </Routes>
  );
};

각 feature의 id를 path로 활용할 수 있어서 라우트를 자동으로 생성할 수 있었고, 이렇게 구성하면 나중에 탭 기반 UI에서 라우터 기반 UI로 전환하거나, 탭과 라우터를 동시에 사용하는 방식(예: ?tab=products)도 유연하게 적용 가능했습니다.

이처럼 요구사항 변화로 알아가는 클린코드에 대해서 직접 체험해보며 요구사항의 변화를 고려하면서 유연하게 대응하는 코드를 작성해본 좋은 경험이였습니다.

2. 에러클래스+ 고차함수로 유연하게 에러핸들링하기.

이번 과제를 진행하며 코드를 컴포넌트와 훅으로 나누는 과정에서, 에러와 알림 처리코드의 가독성과 유지보수성을 해치는 주된 원인이 될 수 있다는 점을 깨달았습니다. 초기에는 기능 구현에 집중하다 보니, 이러한 부수적인 로직이 반복적으로 작성되거나 비즈니스 로직과 뒤섞이는 경우가 많았습니다. 이 부분을 놓치고 싶지 않아서, 더 좋은 구조로 개선해보고자 리팩토링을 시도하게 되었습니다.

<비즈니스 로직과 UI 로직의 관심사 분리>

처음 코드를 작성했을 때는 재고 확인, 수량 변경 같은 비즈니스 로직 안에 addNotification("재고가 부족합니다!", "error")와 같은 UI 관련 로직이 함께 존재했습니다. 이러한 방식은 다음과 같은 문제들을 야기했습니다.

  • 코드의 비순수성: 상품을 장바구니에 담는 핵심 로직이 '알림을 띄우는' 부수 효과에 의존하게 되었습니다. 만약 알림이 아닌 다른 방식으로 에러를 처리해야 한다면, 비즈니스 로직이 담긴 함수를 직접 수정해야만 했습니다.
  • 재사용성 저하: addToCart 함수는 오직 알림을 띄우는 환경에서만 제대로 동작합니다. 만약 서버 API를 통해 장바구니에 상품을 추가하는 로직을 재사용해야 한다면, UI 관련 코드를 제거하거나 새롭게 작성해야 하는 불편함이 있었습니다.

저는 "어떻게 하면 순수한 비즈니스 로직만 담고 있는 함수를 만들 수 있을까?"라는 근본적인 질문에 대해 고민을 했습니다..

<문제 해결: throw와 고차 함수의 역할 분담>

제가 생각한 해결책은 비즈니스 로직에서는 에러가 발생했을 때 예외를 throw하고, 그 예외를 외부에서 처리하는 것이었습니다. 비즈니스 로직의 핵심은 '어떤 조건에서 어떤 상태 변화가 일어나야 하는지'를 정의하는 것이라고 생각했어요.

1. 비즈니스 로직에 집중하는 Atom

jotaiatom을 활용하여 장바구니 관련 비즈니스 로직을 구현했습니다. 여기서 핵심은 각 atomset 함수는 오직 상태 변경이라는 순수한 역할에만 집중하도록 설계했습니다.

예를 들어, addToCartAtom은 장바구니에 상품을 추가하는 과정에서 재고가 부족하거나 수량이 초과되면 UI 알림을 띄우는 대신 명확한 에러 클래스를 throw 하게끔 구현했습니다.

// 장바구니에 상품 추가하는 atom
export const addToCartAtom = atom(null, (get, set, product: Product) => {
  const cart = get(cartAtom);
  const remainingStock = getRemainingStockModel(product, cart);

  // 비즈니스 로직: 재고가 0 이하면 예외를 던짐
  if (remainingStock <= 0) {
    throw new InsufficientStockError(product.name, remainingStock);
  }
  // ...
});
// 재고 부족 에러
export class InsufficientStockError extends CartError {
  constructor(productName: string, availableStock: number) {
    super(`${productName}의 재고가 부족합니다. (가용 재고: ${availableStock}개)`);
    this.name = "InsufficientStockError";
  }
}

// 재고 초과 에러
export class StockExceededError extends CartError {
  constructor(productName: string, maxStock: number, requestedQuantity: number) {
    super(`${productName}의 재고는 ${maxStock}개까지만 있습니다. (요청 수량: ${requestedQuantity}개)`);
    this.name = "StockExceededError";
  }
}

// 장바구니가 비어있는 에러
export class EmptyCartError extends CartError {
  constructor() {
    super("장바구니가 비어있습니다.");
    this.name = "EmptyCartError";
  }
}

// 수량 유효성 검증 에러
export class InvalidQuantityError extends CartError {
  constructor(quantity: number) {
    super(`유효하지 않은 수량입니다: ${quantity} (1 이상이어야 합니다)`);
    this.name = "InvalidQuantityError";
  }
}

마찬가지로, updateQuantityAtom 역시 수량이 재고를 초과하면 StockExceededError를 던집니다. 이렇게 함으로써 비즈니스 로직은 어떤 에러가 발생했는지 '알려주는' 역할만 담당하게 만들었습니다.

2. 고차 함수로 에러 책임을 분리

던져진 에러를 처리하는 역할은 비즈니스 로직의 바깥인 고차 함수의 책임으로 분리했습니다.

export const withTryNotifySuccess = <T extends readonly unknown[], R>(
  action: (...args: T) => R,
  successMessage: string,
  addNotification: (message: string, type: "success" | "error") => void
) => {
  return (...args: T): R | undefined => {
    try {
      const result = action(...args); // ✨ 비즈니스 로직(atom) 실행
      addNotification(successMessage, "success"); // 성공 시 알림 처리
      return result;
    } catch (error) {
      // ✨ 비즈니스 로직에서 던진 에러를 여기서 잡음
      const errorMessage = error instanceof Error ? error.message : "오류가 발생했습니다";
      addNotification(errorMessage, "error"); // 에러 메시지를 알림으로 처리
      return undefined;
    }
  };
};

UI 컴포넌트에서는 이 고차 함수를 이용해 비즈니스 로직을 감싸기만 하면 되게끔 설계했습니다.

// UI 레이어에서 비즈니스 로직을 호출하는 예시
const handleAddProduct = useAutoCallback(
  withTryNotifySuccess(addProduct, "상품이 추가되었습니다.", addNotification)
);

이러한 구조를 통해 비즈니스 로직오직 상태 변경과 유효성 검증에 집중하고, UI 알림 처리고차 함수라는 얇은 레이어를 통해 일관되게 처리할 수 있게 리팩토링 하였습니다.

비즈니스 로직의 '순수성'을 지키고, 알림처리는 고차 함수를 통해 명확하게 관심사를 분리할 수 있었습니다. 또한 반복되는 에러 처리 로직을 추상화하여 코드의 일관성, 재사용성을 동시에 확보할 수 있었습니다.

3. Props drliing 최대한 줄여보기

이번 과제의 핵심인 Props drliling을 경험해보고 이걸 최대한 줄여봐야지 고민했습니다. 처음엔 전형적으로 App컴포넌트에서 모든 상태를 다 정의한뒤 props로 내려줬고, 이렇게 하고나니 거의 4레벨 5레벨 수준의 props drliling이 발생했고 컴포넌트간 결합도도 매우 강했습니다.

예를들어서 ProductMagagement (관리자에서 쓰는 상품 탭) 컴포넌트는 12개의 props를 받고 있었고, ShopPage는 페이지단 컴포넌트인데도 상위에서 9개정도 받고있었습니다.

이러한 문제를 분명 해결할 수 있을것 같았는데 마침 로컬스토리지에 저장되어있던 상태에서 아이디어를 떠올랐습니다. 어차피 참조가 달라도 키가 같으면 훅으로 동기화 되지 않을까? 라고 접근을 했고 최대한 상태를 격시시켜가며 리팩토링 했습니다.

ProductManagement부터 useProducts, useCart, useProductForm 훅을 직접 사용하도록 변경했고, 이렇게 하니 12개던 props가 1개(addNotification)로 줄어들었습니다.

다음으로 ShopPage도 useCart, useCoupons 훅을 직접 사용하도록 변경하니 9개던 props가 1개로 줄어들었습니다.

const App = () => {
  // 알림 시스템
  const { notifications, addNotification, removeNotification } = useNotification();

  // 앱 UI 상태 관리
  const { isAdmin, toggleAdmin, totalItemCount, setTotalItemCount } = useAppState();

  // 검색 상태 관리
  const [searchTerm, setSearchTerm] = useState("");

  return (
    <div className="min-h-screen bg-gray-50">
      <Notification notifications={notifications} onRemove={removeNotification} />

      <Header
        isAdmin={isAdmin}
        onToggleAdmin={toggleAdmin}
        totalItemCount={totalItemCount}
        searchTerm={searchTerm}
        onSearchChange={setSearchTerm}
      />

      <main className="max-w-7xl mx-auto px-4 py-8">
        {isAdmin ? (
          <AdminPage addNotification={addNotification} />
        ) : (
          <ShopPage
            addNotification={addNotification}
            onTotalItemCountChange={setTotalItemCount}
            searchTerm={searchTerm}
          />
        )}
      </main>
    </div>
  );
};

export default App;

최종적으로 App 컴포넌트는 UI 상태(isAdmin, toggleAdmin)만 관리하게 되었고, 각 컴포넌트가 필요한 데이터를 직접 localStorage에서 가져오도록 개선할 수 있었습니다.

다만 아쉬운 점은 ShopPage에서 쓰는 장바구니의 개수는 Header가 알지 못하는 문제가 있어서 별도로 따로 상태를 만들어서 Header한테 넘겨주어야 했는데 이부분은 좀 개선할 필요가 있습니다.

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

  • jotai에 대해서 학습시간이 부족했는데 좀 더 학습하고 구현했다면 더 좋은 구조로 설계할 수 있지 않았을까 하는 아쉬움이 남습니다.

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

Q1. 코치님은 zustand와 jotai 중 어떤 전역 상태 관리 라이브러리를 선호하시나요? 그리고 최근에는 서버 상태와 클라이언트 상태를 분리하여 관리하는 아키텍처가 선호되는데, 클라이언트 전역 상태로는 주로 어떤 상태들을 관리하는 것이 좋을까요?

Q2. 함수형 프로그래밍에 대해서 공부해보고싶어요. 코치님은 함수형 프로그래밍을 공부하신 적이 있다면 어떤식으로 공부하셨는지 책이나 방법을 추천해주실 수 있나요?

…artContainer, CartItem, CartCoupon, CartSummary, and CartEmpty
…rt, and coupons management, enhancing code organization and functionality
…g, including final total calculation, item discount evaluation, and various price formatting methods
…ging notifications and product state, enhancing user experience and data handling
- 상품 관리 기능을 ProductManagement 컴포넌트로 분리
- AdminPage에 관리자 기능 통합 및 탭 관리 구현
- App.tsx 코드 간소화 (473줄 → 305줄)
- types/admin.ts 생성으로 중앙 집중식 타입 관리
- any 타입 제거 및 타입 안전성 향상
- CouponManagement 컴포넌트 별도 파일로 분리
- import type 활용으로 번들 최적화
- 매직 스트링을 상수로 교체하여 유지보수성 개선
- Context API와 Object.assign을 활용한 합성 컴포넌트 구현
- 제어/비제어 컴포넌트 패턴 지원으로 유연한 사용법 제공
- AdminPage 탭 네비게이션을 새로운 Tabs 컴포넌트로 교체
- useState 기반 상태 관리에서 컴포넌트 내장 상태로 이관
- 타입 안전성과 확장성을 고려한 인터페이스 설계
- ShopPage 컴포넌트를 새로 추가하여 상품 목록과 장바구니 기능 통합
- App.tsx에서 ProductList와 CartContainer를 ShopPage로 대체
- getFinalTotal 함수 최적화 및 useCallback 사용으로 성능 개선
- useSearch 훅에서 ProductWithUI 타입을 Product로 변경하여 타입 일관성 유지
- getMaxApplicableDiscount 함수와 관련된 로직을 discounts.ts 파일로 이동
- calculateItemTotal 함수에서 할인 적용 로직을 calculateItemTotalWithDiscount로 변경
- App.tsx에서 getRemainingStock 제거 및 getMaxApplicableDiscount를 래핑하여 사용
- withTryNotifySuccess 및 withTryNotifyError 함수를 추가하여 에러 처리 및 성공 알림 로직을 통합
- App.tsx에서 장바구니, 쿠폰, 상품 추가/수정/삭제 관련 함수에 새로운 에러 처리 로직 적용
- 코드 중복 제거 및 가독성 향상
- 상품 추가 및 편집 기능을 관리하는 useProductForm 훅을 새로 생성
- App.tsx에서 상품 폼 관련 상태 및 로직을 useProductForm으로 통합하여 코드 간소화
- 상품 제출 처리 로직을 useProductForm의 handleProductSubmit으로 위임하여 가독성 향상
- App.tsx에서 상품 관련 상태 및 로직을 AdminPage로 이동하여 코드 구조 개선
- useProductForm 훅을 통해 상품 추가 및 편집 기능을 통합하여 가독성 향상
- AdminPage에서 상품 제출 처리 로직을 handleProductFormSubmit으로 위임하여 코드 간소화
- App.tsx에서 쿠폰 관련 상태 및 로직을 useCouponForm 훅으로 이동하여 코드 구조 개선
- AdminPage에서 쿠폰 제출 처리 로직을 handleCouponFormSubmit으로 위임하여 가독성 향상
- 쿠폰 폼 표시 및 숨김 기능을 useCouponForm 훅에서 관리하도록 변경
- CouponManagement 컴포넌트에서 setShowCouponForm을 showForm 및 hideForm으로 변경하여 가독성 향상
- ProductManagement 및 useCouponForm 훅에서 ProductWithUI 타입을 Product로 수정하여 일관성 유지
- AdminPage에서 쿠폰 폼 관련 상태 및 함수 통합으로 코드 구조 개선
- CouponManagement 컴포넌트에서 setCouponForm을 updateField로 변경하여 상태 업데이트 방식을 통일
- useCouponForm 훅에서 범용 필드 업데이트 함수 추가
- AdminPage에서 쿠폰 폼 관련 상태 및 함수 통합으로 코드 구조 개선
- CouponManagement 컴포넌트를 리팩토링하여 CouponGrid와 CouponForm을 사용하여 코드 구조 개선
- CouponCard, AddCouponButton, CouponGrid, CouponForm, Button, Input, Select 컴포넌트를 새로 추가하여 재사용성 향상
- useCouponForm 훅에서 쿠폰 코드 입력 시 대문자로 변환하는 로직 추가
- ProductManagement 컴포넌트를 리팩토링하여 ProductTable 및 ProductForm을 사용하여 코드 구조 개선
- DiscountItem 컴포넌트를 추가하여 할인 정책 관리 기능을 분리 및 재사용성 향상
- useProductForm 훅에서 범용 필드 업데이트 함수 추가로 상태 관리 방식 통일
- AdminPage에서 상품 관련 상태 및 함수 통합으로 코드 가독성 향상
- App.tsx에서 useCallback을 useAutoCallback으로 변경하여 콜백 함수의 메모리 관리를 개선
- ProductTable.tsx에서 불필요한 줄을 제거하여 코드 가독성 향상
- errorHandler.ts에서 제네릭 타입을 unknown으로 변경하여 타입 안정성 강화
- useAutoCallbak.ts 파일을 새로 추가하여 자동 콜백 기능 구현
- useCart, useCoupons, useProducts 훅에서 localStorage 접근 방식을 useLocalStorage 훅으로 통합하여 코드 중복 제거 및 가독성 향상
- useLocalStorage 훅을 새로 추가하여 상태 초기화 및 저장 로직을 간소화
- errorHandler.ts에서 주석 정리 및 불필요한 코드 제거
- Removed the useAppHandlers hook to simplify the App component's logic.
- Integrated notification handling directly within the cart, coupon, and product hooks for improved user feedback.
- Updated the App component to utilize the new notification system for actions like adding, updating, and deleting products, coupons, and cart items.
- Enhanced the clarity and maintainability of the code by directly passing functions to UI components.
- Removed redundant total calculations from App component, streamlining the logic.
- Moved item total calculation directly into CartContainer for better encapsulation.
- Updated onApplyCoupon function to pass the current total directly, enhancing clarity.
- Cleaned up imports and type definitions for improved maintainability.
- Moved cart and coupon calculation logic to dedicated models for better organization and reusability.
- Updated CartContainer and related hooks to utilize new model functions, enhancing clarity and maintainability.
- Simplified the calculation of item totals and discounts by leveraging centralized functions, improving overall code structure.
…ization

- Added new constants for product and coupon forms, validation limits, and admin tab information in a dedicated admin.ts file.
- Refactored useCouponForm and useProductForm hooks to utilize the new constants for initial form states.
- Updated useProducts and useNotification hooks to implement a unique ID generation utility for better ID management.
- Cleaned up imports in AdminPage to reference the new constants file, enhancing code clarity and maintainability.
…tion and product hooks

- Removed the useUniqueId utility and replaced it with a random string generation method for creating unique IDs in useNotification and useProducts hooks.
- Cleaned up imports to enhance code clarity and maintainability.
- Added jotai as a dependency for state management.
- Refactored cart handling in useCart hook to utilize jotai atoms for managing cart state, item counts, and stock.
- Introduced a new cartStore file to define atoms for cart operations, enhancing modularity and maintainability.
- Wrapped the App component with a Provider to enable jotai's context.
…omponent

- Integrated jotai for managing product state through atoms, enhancing modularity and maintainability.
- Refactored useProducts hook to utilize jotai atoms for product operations, including addition, update, and deletion.
- Renamed App component to AppContent for clarity and wrapped it with a Provider to enable jotai's context.
- Introduced a new productStore file to define product-related atoms, streamlining state management.
- Replaced local state management in useCoupons hook with jotai atoms for better modularity and maintainability.
- Introduced couponStore to define atoms for managing coupons, including addition, deletion, and application.
- Streamlined coupon operations by utilizing jotai's state management capabilities, enhancing overall code clarity.
… management

- Introduced a comprehensive troubleshooting guide for migrating React state management to Jotai, addressing common issues and solutions.
- Refactored components and hooks to utilize Jotai atoms for improved state handling, reducing props drilling and enhancing modularity.
- Implemented a centralized notification system using Jotai for better user feedback and error handling.
- Streamlined cart and product management by integrating Jotai, resulting in optimized performance and maintainability.
…reamlining ShopPage

- Removed unused props from Header and AdminPage components to enhance clarity.
- Simplified ShopPage by reducing the number of props passed, focusing on essential data for rendering.
- Improved overall structure of AppContent for better maintainability.
…nality

- Replaced useSearch with useProductSearch to improve product search handling using Jotai atoms.
- Simplified props in ShopPage by directly using filtered products and search information.
- Removed unnecessary hooks and props in AppContent for better clarity and maintainability.
- Updated Header component to remove unused imports, enhancing code cleanliness.
- Refactored App component to utilize Jotai atoms for managing admin state and notifications, improving modularity.
- Simplified Header and Notification components by removing unnecessary props and directly using Jotai for state management.
- Updated AdminPage to handle product and coupon operations with Jotai setters, enhancing clarity and maintainability.
- Removed redundant local state management, resulting in cleaner and more efficient code.
- Refactored App component to separate content into AppContent for improved modularity.
- Introduced useProductSearch hook to streamline product search functionality using Jotai atoms.
- Updated useSearch to accept a search function, enhancing flexibility and reusability.
- Cleaned up imports and improved overall code structure for better maintainability.
…nd stores

- Removed unnecessary imports from useCoupons, useNotification, and useProducts hooks for improved clarity.
- Simplified atom definitions in cartStore and couponStore by eliminating unused parameters, enhancing code readability.
- Improved overall structure and maintainability of the codebase by reducing redundancy in state management.
- Modified package.json to streamline build and deployment scripts, introducing a preview command.
- Enhanced Vite configuration to rename the advanced HTML file post-build for better output management.
- Refactored ProductCard to utilize a new Button component for improved UI consistency.
- Updated ProductTable and ProductTableRow to use a revised stock handling method, enhancing clarity.
- Cleaned up formatters by removing unused functions and optimizing stock-related logic.
- Improved useCart and useProducts hooks to wrap operations with notification handling for better user feedback.
- Introduced InventoryManagementFeature for managing stock levels and displaying inventory status.
- Updated AdminPage to dynamically register management features, including Products, Coupons, and Inventory.
- Refactored CouponManagement and ProductManagement into separate feature components for improved modularity.
- Enhanced admin tab labels to include inventory management, providing a comprehensive admin dashboard.
…management

- Added useAppState hook to manage application-wide state, consolidating product, cart, and coupon functionalities.
- Introduced useOrder hook to handle order completion and notification, enhancing order management.
- Refactored App component to utilize the new hooks, simplifying state handling and improving modularity.
- Cleaned up imports and streamlined the overall structure for better maintainability.
- Removed unused product-related props and state from App, simplifying state management.
- Updated AdminPage to directly utilize product management hooks, improving modularity.
- Refactored ShopPage to integrate search functionality, enhancing user experience.
- Cleaned up Header component by eliminating unnecessary props, improving clarity.
…ndling

- Removed unused props and state from ProductManagement, streamlining its functionality.
- Integrated product management hooks for better modularity and improved notification handling.
- Updated AdminPage to utilize the refactored ProductManagement, enhancing clarity and maintainability.
- Removed unused coupon-related props and state from App, simplifying state management.
- Updated AdminPage and CouponManagement to utilize hooks for better modularity and notification handling.
- Refactored ShopPage to manage coupon logic directly, improving user experience and code clarity.
- Cleaned up related components to enhance maintainability and reduce redundancy.
- Removed unused props and state related to cart management from App, streamlining its functionality.
- Introduced local state for total item count in App, improving clarity and modularity.
- Updated Header and ShopPage components to reflect changes in props, enhancing maintainability.
- Refactored useAppState to focus solely on UI state management, improving overall structure.
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.

안녕하세요, 잘 정리된 과제와 리팩토링 설명 잘 읽었습니다. 요구사항 변화(탭 증가, 라우터 전환, 에러/알림 분리 등)를 고민하며 Compound Components, 고차 함수 기반 에러 처리, Props drilling 제거 같은 여러 좋은 시도들이 보입니다. 아래는 PR 파일(특히 src/advanced/* 변경사항)을 바탕으로 한 종합 피드백과 상세 피드백입니다. 이 내용을 그대로 GitHub PR Review로 붙여도 무방하도록 마크다운 형식으로 정리했습니다.

주의: 분석은 PR에 포함된 변경파일들(src/advanced/* + package/pnpm 변경 등)을 기준으로 했습니다. 일부 내부함수/atom 구현(예: stores 내부)은 PR에 포함되지 않아 추론하며 코멘트했습니다.


종합 피드백

  1. 주요 키워드 (PullRequestFiles 전체 분석 결과)
  • 모듈화 / 컴포넌트 분리: App.tsx 모놀리식 → 많은 UI/도메인 컴포넌트로 분해 (ProductCard, CartContainer, Header, Tabs 등)
  • 상태관리 종속성: jotai 사용(Provider, useAtomValue 등)이 UI/컴포넌트에 직접 노출됨 → 상태 라이브러리 교체 부담
  • 계층(응집도) 개선 시도: models, components, ui, hooks 분리 시도(README, components/* 파일 추가)
  • 결합도 문제 잔존: 컴포넌트들이 직접 atom을 읽거나 store 로직을 호출하는 곳이 있어 교체 시 영향 범위 확장 가능
  • 테스트 안정성/취약점: UI 문자열/DOM 구조에 강하게 의존하는 테스트(텍스트, 순서, 태그 위치) — DOM 변동에 취약
  • 재사용성: UI 컴포넌트(Button, Input, Tabs 등) 잘 정리되어 재사용성 좋아짐
  • 비즈니스 로직 분리: cart 관련 계산(model)과 UI 컴포넌트 분리(새 models/cart 사용)
  1. PullRequestBody의 '과제 셀프회고'와 '신경 쓴 부분'에 대한 인사이트
  • 매우 좋은 접근입니다. 요구사항 변화(탭 추가, 라우터 전환 등)를 실제로 고민하고 Compound Component + feature 목록(adminFeatures)으로 해결한 점은 실무에 맞는 사고입니다.
  • 고차함수(withTryNotifySuccess)로 에러/알림 책임을 분리한 점도 우수합니다. 비즈니스 로직을 순수하게 유지하려는 시도는 재사용성과 테스트 용이성을 크게 높입니다.
  • 추가 질문/심화 인사이트:
    • Tabs + adminFeatures 패턴을 확장할 때 feature별 props가 다른 문제를 해결하기 위해 "feature adapter"나 "feature wrapper"를 생각해볼 수 있습니다. (각 feature에 공통 props 인터페이스를 제공하는 래퍼)
    • 에러 고차함수 패턴에서 비동기 액션(서버 호출)과 동기 액션(로컬 상태 변경)을 통합 처리할 때의 전략(비동기 에러 전파, 스로잉 vs rejected promise)을 고민해보면 좋습니다.
    • 테스트에서 도메인 로직(할인 계산, 재고 계산 등)을 UI 테스트로만 검증하고 있는데, 이 부분은 단위 테스트로 분리하면 더 튼튼한 테스트가 됩니다.
  1. PullRequestBody의 "리뷰 받고 싶은 질문" 답변 (Q1, Q2)
    Q1. zustand vs jotai — 어떤걸 선호하느냐, 그리고 클라이언트 전역 상태로 무엇을 관리할지?
  • 간단 정리:
    • jotai: atom 기반으로 세분화된 상태(작은 단위별 구독)가 필요하고, React와 자연스럽게 결합되는 경우에 편함. 컴포넌트가 특정 atom에만 반응하도록 만들기 쉽고, 리액티브한 파생(atom) 관리에 유리.
    • zustand: 전역 스토어(단일/복수 스토어)로 단순하고 직관적인 API, 미들웨어(immer, persist) 활용이 쉬움. 서버 상태와의 조합이나 독립적 store 생성이 편리.
    • tanstack-query (react-query): 주로 서버 상태(캐시, stale-while-revalidate, 비동기 패칭)에 사용. 로컬 UI 상태와는 분리해서 사용하는 것이 추천.
  • 제안: 서버 상태는 tanstack-query로, 클라이언트 전역 상태는 UI state와 로컬 영속 상태(persist)가 필요한 것들을 구분하여 관리.
    • 추천으로 다루는 전역 상태 종류:
      • UI state (모달 open, toast, isAdmin, 탭 선택 등) — 짧은 수명, UI 중심: jotai / zustand 허용
      • 영속 로컬 상태 (장바구니 로컬 저장, 유저 세팅) — zustand + persist 또는 jotai with localStorage effect
      • 서버 캐시/페칭/동기화: tanstack-query
      • 파생 계산/비즈니스 로직은 순수 함수로 분리해 unit 테스트
  • 어떤걸 선호하냐: 팀/프로젝트 성격에 따라 다릅니다. 전반적으로 "간단한 전역 상태 + 로컬 persist"라면 zustand가 빠르게 구축 가능하고, "세분화된 리액티브 atom" 필요하면 jotai가 좋습니다. 둘 다 추상화 계층(useCart, useNotification 등)를 만들면 교체가 쉽습니다.

Q2. 함수형 프로그래밍 공부법/자료

  • 실전 추천 순서:
    1. 기본: 불변성, 순수함수, higher-order functions (map/filter/reduce), composition
    2. 책/자료:
      • Mostly Adequate Guide to Functional Programming (Brian Lonsdorf) — JS로 FP 개념을 체득
      • Functional-Light JavaScript (Kyle Simpson) — 실용적인 접근
      • "Learn You a Haskell" (문법은 다르지만 FP 개념 이해에 도움)
      • TypeScript와 함께: "Practical FP in TypeScript" (온라인 자료들) 또는 fp-ts 튜토리얼들
    3. 실습: 작은 유틸(할인 계산, 재고 계산, 포맷터)를 순수함수로 작성 + 테스트
    4. 점진적 도입: 프로젝트에 reduce/compose 사용, side-effect는 명확히 분리(try/catch, 에러 클래스로 처리)

상세 피드백 (파일별 / 개념별)

먼저 개념 정의(요청하신 대로).

  1. 개념 정의
  • 응집도(cohesion): 한 모듈(파일/컴포넌트/패키지)이 동일한 책임(또는 매우 관련된 책임)을 얼마나 잘 모아두었는지의 정도.
    • 실무 규칙으로 재정의: "변경을 위해 수정해야 하는 파일/라인의 이동 동선이 얼마나 짧은가"와 "해당 모듈을 라이브러리로 떼어낼 수 있는지(분리 용이성)".
  • 결합도(coupling): 모듈들(함수/컴포넌트/훅)이 얼마나 직접적으로 서로의 내부에 의존하는가.
    • 낮은 결합도: 인터페이스(함수/훅/props)를 통해 의존성 축소. 예: useAddProduct({ onSuccess, onError }) vs useAddProduct(addNotification)
  1. 문제 정의 (PR에서 관찰된 주요 문제)
  • 상태 라이브러리(jotai)가 UI 컴포넌트 내부에 직접적으로 노출되어 있음
    • 예: Header 컴포넌트가 useAtomValue(totalItemCountAtom) 등으로 직접 읽음
    • 문제: jotai → zustand/Redux로 바꿀 때 컴포넌트마다 수정 필요 → 결합도 높음
  • 일부 비즈니스 로직과 UI 로직이 완전히 분리되지 않음(이미 분리 노력이 보이지만 일부 컴포넌트/테스트에서 결합 발견)
  • 패키지로 분리(packaging)할 때 엔티티(도메인 모델)와 UI가 완전히 독립적이지 않은 파일들 존재
  • 테스트가 DOM/문구 의존성이 강해 리팩토링 시 깨질 가능성 큼
  1. 문제 상황(AS-IS) + 근거 코드 스니펫
    아래에 실제 PR의 AS-IS 예시와 어떤 문제가 발생하는지 정리합니다.

AS-IS 1 — Header가 jotai에 직접 의존

  • 문제: Header 컴포넌트가 상태 라이브러리에 강하게 결합되어 있어 교체가 어려움.

AS-IS 코드 (PR에 존재)

// src/advanced/components/ui/header/Header.tsx (발췌)
// 내부에서 직접 jotai atom 사용
import { useAtomValue } from "jotai";
import { totalItemCountAtom } from "../../../stores/cartStore";
...
const ShopHeader = ({ searchTerm, onSearchChange, onToggleAdmin }: ShopHeaderProps) => {
  const totalItemCount = useAtomValue(totalItemCountAtom);
  ...
  return (
    <header> ... {totalItemCount} ... </header>
  );
};

문제:

  • swap state library 시 useAtomValue, totalItemCountAtom를 모두 변경해야 함.
  • Header는 "UI 컴포넌트"여야 하지 "상태 관리 라이브러리 세부"를 알아야 할 필요가 없음(관심사의 분리 위반).

TO-BE 1 — 추상화된 훅을 사용하여 결합도 낮추기

  • 해결책: components는 "어떤 데이터/행위"가 필요한지만 선언하고 실제 구현은 훅(useCart, useHeaderState, useNotification 등)으로 숨김.

TO-BE 코드 (권장)

// src/advanced/stores/cartFacade.ts (새 파일)
export const useCartFacade = () => {
  // 내부적으로 jotai/ zustand/ redux 등 자유롭게 사용
  const count = useAtomValue(totalItemCountAtom); // jotai 기반 구현 예시
  const remove = useSetAtom(removeFromCartAtom);
  return { count, remove };
};

// src/advanced/components/ui/header/Header.tsx (수정)
import { useCartFacade } from "../../stores/cartFacade";

const ShopHeader = ({ searchTerm, onSearchChange, onToggleAdmin }: ShopHeaderProps) => {
  const { count: totalItemCount } = useCartFacade();
  return <header> ... {totalItemCount} ... </header>;
};

효과:

  • 상태라이브러리 변경 시 cartFacade 안의 구현만 바꾸면 되고 UI 컴포넌트는 변경 불필요.
  • 응집도는 "cart 관련 접근"으로 모이고, 결합도는 낮아짐.

AS-IS 2 — 컴포넌트가 비즈니스 로직을 직접 호출/다루는 경우

  • PR에서는 개선되어 many components moved logic into models and hooks, 하지만 일부 컴포넌트가 여전히 비즈니스 상세(재고 계산) 로직을 직접 호출하거나 setState를 관리.

예 (PR의 원래 App.tsx 안에 있던 로직 일부)

const addToCart = useCallback((product) => {
  const remainingStock = getRemainingStock(product);
  if (remainingStock <= 0) {
    addNotification('재고가 부족합니다!', 'error');
    return;
  }
  // setCart update...
  addNotification('장바구니에 담았습니다', 'success');
}, [ /* ... */ ]);

문제:

  • UI(컴포넌트)에서 비즈니스 유효성검증 + 알림 처리 + 상태 변경이 뒤섞여 있음 → 재사용성/테스트성 저하.

TO-BE 2 — 비즈니스 로직은 순수한 훅/모델로 분리, UI는 훅으로 래핑

  • 구현 아이디어: cart model은 가능한 한 "throw 기반"의 에러 전파(또는 반환값 형태의 Result)를 사용하고, UI는 withTryNotify wrapper로 에러/알림을 처리.

TO-BE 코드 (권장)

// src/advanced/models/cart.ts
export function addToCartModel(cart, product) {
  const remaining = getRemainingStockModel(product, cart);
  if (remaining <= 0) throw new InsufficientStockError(...);
  return updatedCart; // 순수: 기존 cart를 인자로 받고 새 cart 리턴
}

// src/advanced/hooks/useCart.ts
export const useCart = () => {
  const [cart, setCart] = useAtom(cartAtom);
  const add = (product) => {
    try {
      const next = addToCartModel(cart, product);
      setCart(next);
      return { ok: true };
    } catch (e) {
      throw e; // 에러를 호출자(UI)에게 위임
    }
  };
  return { cart, add, ... };
};

// UI 레이어
const { add } = useCart();
const handleAdd = withTryNotifySuccess(
  (product) => add(product),
  "장바구니에 담았습니다.",
  addNotification
);

효과:

  • addToCartModel은 단위 테스트로 검증 가능(순수 함수).
  • useCart는 상태 업데이트 정책만 노출.
  • UI는 에러/알림 정책만 담당 → 관심사가 분리됨.

AS-IS 3 — 상태 라이브러리 교체 시 영향 범위 (시나리오)

  • 현 상황: 많은 컴포넌트가 jotai 훅(useAtomValue/useSetAtom)을 직접 사용.
  • 교체 시 문제: 수십 개의 컴포넌트에서 jotai API 호출을 zustand/Redux 호환 API로 변경해야 함.

TO-BE 3 — 어댑터 계층 도입 (권장 전략)

  • 모든 상태 접근을 중앙화된 "facade" 훅/파일로 추상화합니다. (예: src/stores/cart.ts, src/stores/notification.ts)
  • 컴포넌트는 절대 useAtom*를 직접 import 하지 않고 facade 훅을 사용.
  • 상태 라이브러리 교체는 facade 파일들 내부만 변경하면 됩니다.

예시:

// src/advanced/stores/index.ts (public surface)
export { useCart } from "./cartFacade";
export { useNotifications } from "./notificationFacade";

facade 내부는 jotai로 구현했다가 zustand로 교체하면 바로 바꿀 수 있습니다.


AS-IS 4 — 패키지화(모듈화) 관점: 어떤 코드를 떼어내기 쉬운가?

  • 현재 구조 장점:
    • UI 컴포넌트(Button, Tabs, Input), admin/product/cart 컴포넌트가 명확히 분리되어 있음 → UI 라이브러리 패키지로 떼어내기 쉬움.
    • models 및 utils(계산 함수들)이 곳곳에 추가되어 있음 → 순수 로직은 라이브러리로 묶기 용이.
  • 남아있는 문제(떼어내기 방해 요소):
    • components에서 상태 접근을 직접 함(직접 atom 사용). 패키지(라이브러리)로 배포하려면 상태 추상화가 필요함(스스로 상태를 관리하거나 prop으로 주입 가능한 인터페이스 필요).
    • types와 constants가 루트에 흩어져 있음(패키징 시 타입 의존성 복잡).
  • 결론: UI 컴포넌트 패키지화는 비교적 쉬우나 "엔티티 훅/스토어"를 같이 묶어 패키지로 만들려면 facade/DI 계층이 필요.

권장 패키징 구조(예)

  • packages/ui -> Button, Input, Tabs, Icons, Header(순수 UI, props 기반)
  • packages/domain-cart -> models/cart.ts (순수 함수), types/cart.ts, hooks/useCartFacade.ts (facade만)
  • packages/hooks -> useProductSearch, useLocalStorage 등
  • packages/stores-adapter -> jotai impl / zustand impl (각 스토어에 대해 adapter export)

추가로: 배포 가능한 패키지라면 UI 컴포넌트는 상태 접근을 전혀 몰라야 합니다(모두 props로 주입).


응집도/결합도 정리 및 평가(요청하신 정의 기준으로)

  • 응집도(변경 이동 경로 짧음, 잘 떼어낼 수 있는가)
    • 긍정적: components/ui/, components/product/ 등 UI 컴포넌트 계층이 응집되어 있음(떼어내기 좋음).
    • 개선필요: stores/hooks 접근이 UI에 흩어져 있어서 특정 도메인(feature)을 떼어낼 때 연쇄수정이 필요. 예: Header는 UI이지만 jotai atom 의존 → UI 패키지로 떼어내려면 atom 의존 제거 필요.
  • 결합도(인터페이스 통한 결합 여부)
    • 일부 좋은 사례: Product components accept product props, ProductForm uses updateField callback → 낮은 결합
    • 안 좋은 사례: Header, Notification 등에서 직접 useAtomValue 사용 → 높은 결합
    • 권장: 컴포넌트는 "onX, props" 인터페이스로만 결합하고 내부 state 라이브러리 참조는 facade/hook으로 숨기기

구체적 권장 개선 (우선순위)

  1. 상태 접근을 가로채는 facade 훅을 만들어 모든 컴포넌트가 이를 사용하도록 마이그레이션
    • useCart, useNotifications, useProducts, useAdminMode 등
  2. UI 컴포넌트는 절대 store API를 직접 import 하지 않도록 규칙화
    • Header는 props 기반 UI로 만들거나 facade 훅만 사용
  3. 비즈니스 로직(할인, 재고 등)은 models/ 폴더에 순수함수로 두고 단위 테스트 추가
  4. 테스트 분리: 핵심 로직(할인, 재고)은 unit test, UI 흐름은 통합/E2E로 축소 — DOM에 의존적이지 않게 테스트 분리
  5. 에러 핸들링 전략 문서화: domain 함수는 명확한 Error 클래스를 throw, UI는 withTryNotify 같은 handler로 래핑

구체적 AS-IS -> TO-BE (코드 비교 샘플들)
(1) 상태 접근 추상화 예시

AS-IS:

// components directly import jotai atom
import { useAtomValue } from "jotai";
import { totalItemCountAtom } from "../../../stores/cartStore";

const Header = () => {
  const total = useAtomValue(totalItemCountAtom);
  return <div>{total}</div>;
};

TO-BE:

// stores/cartFacade.ts
export const useCart = () => {
  const total = useAtomValue(totalItemCountAtom); // jotai 내부 사용
  const add = useSetAtom(addToCartAtom);
  return { total, add };
};

// components use facade only
import { useCart } from "../../stores/cartFacade";
const Header = () => {
  const { total } = useCart();
  return <div>{total}</div>;
};

(2) 비즈니스 로직 분리 + 에러 처리

AS-IS:

// 컴포넌트 내부에서 재고 체크 + 알림
if (remainingStock <= 0) {
  addNotification('재고가 부족합니다!', 'error');
  return;
}
setCart(...); // 상태 변경
addNotification('장바구니에 담았습니다', 'success');

TO-BE:

// models/cart.ts (순수)
export function addToCartModel(cart, product) {
  const remaining = getRemainingStockModel(product, cart);
  if (remaining <= 0) throw new InsufficientStockError(...);
  return updatedCart;
}

// hooks/useCart.ts
const add = (product) => {
  try {
    setCart(prev => addToCartModel(prev, product));
    return { ok: true };
  } catch (e) {
    throw e;
  }
};

// UI
const handleAdd = withTryNotifySuccess(
  (product) => cart.add(product),
  "장바구니에 담았습니다.",
  addNotification
);

(3) 상태 라이브러리 교체를 쉽게 만드는 adapter 예시

TO-BE (추상화 인터페이스):

// src/advanced/stores/index.ts
export type CartAPI = {
  getItems: () => CartItem[];
  addItem: (product: Product) => void;
  updateQuantity: (productId: string, qty: number) => void;
  getTotals: () => { totalBefore: number; totalAfter: number };
};

let impl: CartAPI;

export const registerCartImpl = (impl_: CartAPI) => { impl = impl_; };

export const useCartAPI = () => impl;
  • jotai impl 등록:
// src/advanced/stores/jotaiCartImpl.ts
import { useAtom } from "jotai";
// implement CartAPI using atom hooks and register via registerCartImpl
  • zustand로 바꿀 때는 jotai impl 대신 zustand impl을 만들어 register만 바꿔주면 됨.

테스트 관련 코멘트

  • 현재 테스트(origin.test.tsx)는 좋은 통합 시나리오를 많이 다루지만, 다음을 권장합니다:
    • 핵심 도메인 로직(할인 정책, 재고 계산)은 유닛 테스트로 분리(빠르고 덜 깨짐).
    • UI 테스트는 경로(flow) 중심으로 최소화(핵심 시나리오만).
    • 테스트에서 텍스트 매칭을 할 때 일부 문구(예: '장바구니에 담았습니다' vs '장바구니에 담았습니다.')의 점(dot/마침표/공백 차이에 민감함 -> 정확한 문구를 상수로 관리하거나 메시지 키를 사용).

마무리 요약 / 실행 가능한 체크리스트

  • 단기(빠르게 적용 가능한) 개선사항

    1. 모든 컴포넌트에서 직접 사용하는 atom 호출을 찾아 facade 훅(useCart, useNotifications, useProducts 등)으로 래핑
    2. models/*에 있는 순수 함수(계산 함수)에 unit tests 추가
    3. UI 컴포넌트(lib)에서는 상태 라이브러리 직접 참조 금지 규칙 적용
  • 중기(리팩토링/패키징) 개선사항

    1. packages/ui, packages/domain-* 형태로 패키징 준비(종속성 최소화)
    2. stores adapter 계층 구현(register 기반)으로 라이브러리 전환 용이성 확보
    3. 에러 처리 정책(도메인 에러 클래스 + withTryNotify 패턴 문서화) 완비
  • 장기(아키텍처) 제안

    1. 서버 상태는 tanstack-query로 집중, 로컬 유저 UI 상태는 zustand/jotai 중 하나로 관리(팀 스타일에 맞게 선택)
    2. FP 관점에서 비즈니스 로직(모델)은 순수함수로 유지, side-effect(알림, 네트워크)는 얇은 레이어에서 담당

마지막으로 칭찬할 부분

  • Compound Components(Tabs) 도입, components 계층 정리, UI 컴포넌트 디자인(버튼, input, select) 정리는 아주 훌륭합니다.
  • withTryNotifySuccess로 에러/알림 책임 분리 시도도 실무적으로 매우 유용한 패턴입니다.
  • README에 구조 개선 제안도 포함한 점은 팀 협업/리팩토링을 위한 좋은 출발입니다.

필요하시면:

  • repo 기준으로 "facade 훅" 예제 파일 1~2개(예: useCartFacade, useNotificationFacade)를 실제 코드로 만들어 드릴게요.
  • jotai→zustand 마이그레이션 스텝별 패치(예: 적용해야 할 파일 목록 + 변경 코드)도 만들어 드릴 수 있습니다.

원하시는 우선순위를 알려주세요.

Copy link

@angielxx angielxx left a comment

Choose a reason for hiding this comment

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

오 창준님도 알림을 에러로 throw하는 방식으로 처리하셨군요...! 저도 알림을 NotificationError 커스텀 에러로 구현했는데, 고차함수로 try-catch문으로 감싸서 처리하신 부분이 저랑 달라서 흥미롭습니다. PR에서 확장이 가능한 코드를 작성하기 위해 고민하신 부분이 인상깊네요.

Copy link

@unseoJang unseoJang left a comment

Choose a reason for hiding this comment

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

안녕하세요 창준님~
디자인 패턴과 함수형 프로그래밍 관련 코드 살펴봤습니다.
BP받을만한 과제 내용이라고 생각이 드네요
도메인 별로 디렉토리도 잘 나눠주신것같고 유틸함수도 전에 제가 말씀 드린데로 인자값에 명확히 무슨 값이 들어가고 가장 최소 단위 반환값으로 잘 나눠주신것 같습니다.

중간 중간 보완해야 될 부분들만 살짝살짝 보이는데 큰 문제는 없을 것같아요!
try catch 문을 저렇게 쓰는 것도 신기하게 잘 봤어요!

Choose a reason for hiding this comment

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

AI가 제안해준 개선 방안일까요?정리가 깔끔하게 되어있네요

}

// 관리 기능들을 여기에 등록
const adminFeatures: AdminFeature[] = [

Choose a reason for hiding this comment

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

이 부분이 유열님이 고민해본 유연하게 처리할 수 있는 관리 기능들을 추상화한 부분이네요

Choose a reason for hiding this comment

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

이전보다 코드가 훨씬 클린해진 느낌이 들어요, 각 함수의 성격을 주석으로 작성해놓은 부분들도 놓고 타입스크립트라서 어떤 값을 뱉는지 명확성이 확실해보입니다.

};

// 할인율 계산
export const calculateDiscountRate = (originalPrice: number, discountedPrice: number): number => {

Choose a reason for hiding this comment

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

할인율 계산이지만 정수를 반환하는 부분이죠? 혼동을 줄이려면 내부: 소수 비율(0~1), **표시/노출: 정수 %**로 고정해놓는 게 명확성에 훨씬 더 도움을 줄 것 같아요

// 아이템별 할인 정보 계산
export const calculateItemDiscount = (
item: CartItem,
itemTotal: number

Choose a reason for hiding this comment

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

여기 있는 itemTotal: number은 itemTotal이 할인적용 후 금액일까요?
함수명만 보면 내부에서 원가→할인→총액을 계산할 것처럼 느껴지네요

import { calculateItemTotal, calculateCartTotal, calculateItemDiscount } from "../../models/cart";

// 장바구니 아이콘 컴포넌트
const CartIcon = () => (

Choose a reason for hiding this comment

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

아이콘 컴포넌트는 외부에서 아예 따로 빼서 가져와도 될것같아요

let totalBeforeDiscount = 0;
let totalAfterDiscount = 0;

cart.forEach((item) => {

Choose a reason for hiding this comment

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

forEach + 외부 변수 변경 대신, reduce로 합계를 만들면 불변/순수한 계산 파이프라인이 돼서 테스트/가독성↑. 이라고 하는데... reduce는 저도 잘 못써서...ㅜㅜ

if (value < COUPON_LIMITS.MIN_VALUE) {
return {
isValid: false,
errorMessage: "할인값은 0 이상이어야 합니다",

Choose a reason for hiding this comment

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

여기 메시지 상수값으로 저장해두면 관리하기 더 편할 것 같아요

} catch (error) {
const errorMessage = error instanceof Error ? error.message : "오류가 발생했습니다";
addNotification(errorMessage, "error");
return undefined;

Choose a reason for hiding this comment

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

catch 값에서 error 표시 해주고 나서는 자동으로 undefined 반환 되지 않나요? undefined 안넣어줘도 되지 않나 싶긴합니다!

const ProductImage = ({ product }: { product: Product }) => (
<div className="relative">
<div className="aspect-square bg-gray-100 flex items-center justify-center">
<svg className="w-24 h-24 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">

Choose a reason for hiding this comment

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

svg는 따로 빼서 관리하면 전체 코드가 가독성 면에서 좋아질것같아요

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.

4 participants