Skip to content

[6팀 정용준] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#45

Open
dkile wants to merge 18 commits into
hanghae-plus:mainfrom
dkile:main
Open

[6팀 정용준] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#45
dkile wants to merge 18 commits into
hanghae-plus:mainfrom
dkile:main

Conversation

@dkile
Copy link
Copy Markdown

@dkile dkile commented Aug 6, 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는 분리되어 있나요?

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

심화과제

  • 이번 심화과제는 Context나 Jotai를 사용해서 Props drilling을 없애는 것입니다.

  • 어떤 props는 남겨야 하는지, 어떤 props는 제거해야 하는지에 대한 기준을 세워보세요.

  • Context나 Jotai를 사용하여 상태를 관리하는 방법을 익히고, 이를 통해 컴포넌트 간의 데이터 전달을 효율적으로 처리할 수 있습니다.

  • Context나 Jotai를 사용해서 전역상태관리를 구축했나요?

  • 전역상태관리를 통해 domain custom hook을 적절하게 리팩토링 했나요?

  • 도메인 컴포넌트에 도메인 props는 남기고 props drilling을 유발하는 불필요한 props는 잘 제거했나요?

  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?

이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?

이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!

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

@dkile dkile changed the title [6팀 정용준] [6팀 정용준] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 Aug 6, 2025
Copy link
Copy Markdown

@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(리팩토링 + 기능 분리) 잘 진행하셨습니다. 전반적으로 도메인(상품, 쿠폰, 알림)을 feature 단위로 분리하고 UI/도메인 책임을 분명히 하려는 시도가 잘 보입니다. 다만 React API(특히 Context/useContext 사용), 훅의 클로저/의존성, 추상화 경계 등에서 치명적인 버그/유지보수 위험이 보입니다. 아래는 요청하신 흐름(시나리오 → 변화 예측 → 잘 작성되었는지 검증)에 맞춰 정리한 피드백입니다.

요약(한눈에)

  • 핵심 키워드: Context Provider 오용, React use() 오용, useCallback/의존성(클로저) 문제, 기능별 모듈화(응집도 양호), 결합도(인터페이스로 분리 필요), domain vs UI 경계, 전역 상태관리 교체 시 어댑터 필요, 패키지화 시 추출 단위 제안
  • 즉시 고쳐야 할 치명적 문제: React의 use()와 Context 사용법 오용, Provider 태그 사용 실수(App에서 AdminContext를 Provider로 쓰지 않음), useCallback dependency 누락으로 인한 stale closure / 런타임 버그
  • 구조적 제안: features(product/coupon/notification)은 잘 모아져 있어 패키지로 빼기 용이. 하지만 public API(타입 · 훅 시그니처)를 깔끔하게 정의하고 state 접근을 추상화해야 전역 상태관리 교체가 쉬움.

아래는 PR에 남길 Github PR Review 형식(마크다운)으로 정리한 종합 피드백 + 상세 피드백입니다.


종합 피드백

  1. PR 파일 전체 키워드
  • Context / Provider 사용 오류 (AdminContext.Provider 대신 AdminContext(...) 사용)
  • React use() 오용 (useContext 대신 use import from 'react' 후 use(AdminContext) 사용)
  • useCallback / useMemo 의존성 누락 & 클로저 문제 (예: useCoupons.addCoupon, useProducts callbacks)
  • 도메인(entities) vs UI 분리: 대부분 잘 분리됨 (features 폴더 구조가 긍정적)
  • 통합된 Toast/Notification 시스템 도입(좋음) — 구현 세부에서 useContext 사용법 불일치
  • 컴포넌트 재사용성: ListView, ProductCard 등 잘 분리됨
  • 변수/함수 책임: remainingStock 계산 위치 혼선 (ShopPage vs ProductCard)
  • 패키지화 가능성: ___features 단위(상품, 쿠폰, 알림)는 패키지로 떼기 용이
  1. PullRequestBody의 "과제 셀프회고" 관련 피드백(인사이트 중심)
  • 좋은 점(인사이트)
    • 엔티티 중심 훅(상품/쿠폰)을 features로 묶어서 domain logic 을 분리한 점은 교육 목적과 실무 리팩토링 관점에서 매우 적절합니다. 이는 재사용성 및 테스트 용이성을 높입니다.
    • Notification을 ToastProvider로 추상화한 점은 cross-cutting concern(알림)을 잘 캡슐화했습니다.
    • UI 컴포넌트(카드, 리스트 뷰)와 도메인 훅을 분리하여 “view-data vs entity-data” 원칙을 따르려는 시도가 보입니다.
  • 더 생각해볼만한 질문들
    • 훅의 public API는 소비자(페이지/컴포넌트)가 기대하는 최소 계약(시그니처)을 만족하나요? (예: addNotification은 string+type vs Message object 혼재)
    • 도메인 훅들이 “저장소(storage) 구현(localStorage)”에 강하게 결합되어 있나요? 이 결합을 제거하면 어떤 이점과 비용이 생기나요?
    • 각 feature가 외부 의존성(utility-hooks, toast 등)을 직접 사용하고 있는데, 패키지로 뺄 때 의존성을 어떻게 최소화/명시할지 정해봤나요?
  1. PullRequestBody의 "리뷰 받고 싶은 내용"에 대한 답변
  • (본문에 구체 질문이 없으므로) 제가 우선적으로 보았으면 하는 항목:
    • 런타임/빌드에서 바로 터지는 문제: Context/React API 오용 (빠르게 고쳐야 함)
    • 훅의 의존성/클로저로 인한 버그(테스트로 잡기 어려움)
    • 전역 상태관리(jotai/zustand/tanstack-query)로 옮길 때의 변경 범위 및 추상화 제안
    • 패키지화(모듈화) 시 어떤 단위로 나누면 응집도/결합도 측면에서 이상적인지

상세 피드백

  1. 개념 정의 (핵심 용어)
  • 응집도(cohesion): 한 모듈(파일/디렉토리/패키지)가 관련된 책임(behavior)을 얼마나 잘 하나로 모아두었는지를 뜻합니다. 좋은 응집도는 변경이 한 영역에서만 일어나도록 해 유지보수를 쉽게 합니다. (귀하의 규칙: 변경 경로가 짧고, 패키지로 떼어낼 때 매끄럽게 떼어내질 수 있어야 함)
  • 결합도(coupling): 모듈 간의 의존성의 강함을 의미합니다. 낮은 결합도는 인터페이스(명확한 함수/훅 시그니처)를 통해 의존성을 최소화하는 것입니다.
  1. 문제 정의 (파일/구간별 주요 이슈)
  • A. Context/React API 오용 (치명적)

    • 문제: AdminContext를 Provider로 사용하지 않고 형태로 사용했고, 여러 컴포넌트에서 use(AdminContext) / use(ToastValueContext) 등 React의 use() 를 잘못 사용하고 있음.
    • 영향: 런타임 에러(Provider가 전달되지 않거나, use(AdminContext)를 사용하면 undefined) 또는 빌드 시 경고/오작동. 서버전용 use()가 아닌데 사용하여 동작하지 않음.
    • AS-IS (App.tsx 발췌)
      import { AdminContext } from "./____pages/admin-context";
      
      // ...
      
      return (
        <NotificationProvider>
          <AdminContext value={{ isAdmin, setIsAdmin }}>
            ...
          </AdminContext>
        </NotificationProvider>
      );
    • TO-BE (정상)
      import { AdminContext } from "./____pages/admin-context";
      
      return (
        <NotificationProvider>
          <AdminContext.Provider value={{ isAdmin, setIsAdmin }}>
            ...
          </AdminContext.Provider>
        </NotificationProvider>
      );
    • AS-IS (AdminPage / GoToAdminButton에서 use() 사용)
      import { use } from "react";
      import { AdminContext } from "../admin-context";
      
      function GoToAdminButton() {
        const { setIsAdmin } = use(AdminContext);
        // ...
      }
    • TO-BE: useContext 사용
      import { useContext } from "react";
      import { AdminContext } from "../admin-context";
      
      function GoToAdminButton() {
        const { setIsAdmin } = useContext(AdminContext);
        // ...
      }
    • 권장: admin-context.tsx에 Provider도 export 하거나 README에 사용법 문서화. 컴포넌트는 반드시 useContext를 사용하도록 수정.
  • B. Toast / Notification 관련(use() 오용 & Provider/Context 접근 방식 혼선)

    • 문제: ToastProvider 만든 뒤 Toast 컴포넌트에서 use(ToastValueContext) 등을 사용. useContext로 바꿔야 함.
    • AS-IS (Toast.tsx)
      import { Fragment, use } from "react";
      const { messages, config } = use(ToastValueContext);
    • TO-BE
      import { Fragment, useContext } from "react";
      const { messages, config } = useContext(ToastValueContext);
    • 추가: ToastProvider/Context 타입이 Generic으로 되어 있지만 Toast 컴포넌트 쪽에서 any-like 캐스팅을 하고 있음. 타입 안전성 보강 권장.
  • C. useCallback 의존성 / 클로저 문제

    • 문제: 몇몇 useCallback에서 외부 변수(coupons, products 등)를 사용하면서 의존성 배열을 비워두었거나 잘못 처리함 → stale closure 혹은 예측 불가능한 동작(예: addCoupon에서 existing 체크는 최신 coupons가 아니라 선언 시 캡처한 값).
    • AS-IS (use-coupons.ts)
      const addCoupon = useCallback((newCoupon: Coupon) => {
        const existingCoupon = coupons.find((c) => c.code === newCoupon.code);
      
        if (existingCoupon) {
          throw new AlreadyExistsCouponCodeError();
        }
      
        setCoupons((prev) => [...prev, newCoupon]);
      }, []); // <-- 의존성 누락(coupons, setCoupons)
    • TO-BE: 함수형 setState로 의존성 제거 or deps에 setCoupons 포함
      const addCoupon = useCallback((newCoupon: Coupon) => {
        setCoupons((prev) => {
          const existing = prev.find((c) => c.code === newCoupon.code);
          if (existing) {
            throw new AlreadyExistsCouponCodeError();
          }
          return [...prev, newCoupon];
        });
      }, [setCoupons]);
    • 유사 케이스: useProducts의 addProduct/updateProduct/deleteProduct는 setProducts 의존성만 포함해도 안전. 전체 코드에서 useCallback의 deps를 점검하세요.
  • D. 도메인 데이터 변경과 UI 프로퍼티 혼동 (remaining stock 처리)

    • 문제: ProductListSection에서
      product={{ ...product, stock: getRemainingStock(product) }}
      로 product 자체의 stock 를 바꿔 넘김 → 도메인 엔티티를 변형하여 혼란 유발.
    • 왜 문제: 다른 곳(도메인 훅, 저장소)에서는 원래 stock(총 재고)를 쓰고, 뷰에서는 남은 재고를 쓰면 추적하기 어려움.
    • TO-BE: 명시적인 prop으로 분리
      AS-IS:
      <ProductCard
        key={product.id}
        product={{ ...product, stock: getRemainingStock(product) }}
        onClickAddToCart={addToCart}
      />
      TO-BE:
      <ProductCard
        key={product.id}
        product={product}
        remainingStock={getRemainingStock(product)}
        onClickAddToCart={addToCart}
      />
      • ProductCard는 product.stock(원본) 대신 remainingStock prop으로 렌더링/버튼 disabled 로직을 치환.
  • E. API 일관성(함수 시그니처)

    • 예: addNotification이 초기 코드에선 (message: string, type?) 형태였고 리팩토링 후엔 Message 객체 형태를 기대. 프로젝트 전체에서 통일된 시그니처를 사용하세요. 또는 편의 래퍼를 만들어 이전 호출들을 적응시키세요.
    • 제안: useNotification에서 addNotification(messageOrString, type?) 오버로드를 제공하거나, 작은 helper를 추가.
  1. 모듈화 / 패키지 배포 관점 — 응집도·결합도 평가 및 권장 분리 단위
  • 정의(요약): 응집도는 “변경할 때 수정해야 할 파일/라인이 좁은가”, “패키지로 떼어내기 쉬운가”로 평가. 결합도는 “인터페이스(훅 시그니처, 서비스 함수)를 통해 의존성을 낮추고 있는가”로 평가.
  • 현재 상태 평가
    • 응집도: ___features/product, ___features/coupon, ___features/notification로 나누어진 구조는 응집도가 높음(관련 코드가 모여 있음). UI 컴포넌트(product 폴더)도 응집되어 있음.
    • 결합도: 중간 정도. 문제는 많은 훅들이 useLocalStorage, Toast 등 구체 구현에 직접 의존하고 있어 상태관리 라이브러리 교체 시 많은 파일을 고쳐야 함.
  • 패키지로 빼기 추천 단위 (응집도 높고 종속성 적음)
    • @shop-domain/product: types, useProducts(hook), initial mock, service functions (add/update/delete) — 내부 저장소는 추상화된 인터페이스로 주입 가능.
    • @shop-domain/coupon: types, useCoupons, error types, constants
    • @shop-ui/notification: NotificationProvider, useNotification, Toast 컴포넌트 (UI 의존)
    • @shop-ui/components: ProductCard, ListView, CartIcon, CartCount 등
  • 패키지화 시 주의점(결합도 낮추기)
    • public API는 타입과 훅 시그니처만 노출: 예) export function useProducts(adapter?: ProductsAdapter)
    • 저장소/캐시 접근은 adapter 패턴으로 분리: 초기 구현은 localStorage adapter, 추후 zustand/jotai adapter로 교체 가능.
    • UI 패키지는 domain 패키지를 의존하되, domain 패키지는 UI 의존 없어야 함.
  1. 상태관리 라이브러리 변경 시 시나리오 + 현재 코드 영향
  • 시나리오 A: useLocalStorage 기반 → Zustand로 교체

    • 변화 포인트: useProducts/useCoupons/useNotification 내부 구현을 localStorage-based -> zustand store로 변경.
    • 현재 코드 대응성: 훅들이 내부에서 useLocalStorage 직접 호출하므로 각 훅을 수정해야 함. 하지만 UI는 useProducts 훅을 통해 제품 리스트를 받기 때문에 UI 쪽 수정은 최소(훅 시그니처 유지 시)로 끝남.
    • 권장 변경 방식 (TO-BE): 훅은 내부 저장소 구현을 캡슐화하고, adapter를 받게 만듦.
      예:
      AS-IS:
      // use-products.ts (현재)
      const [products, setProducts] = useLocalStorage("products", initialProducts);
      export const useProducts = () => ({ products, addProduct, ... });
      TO-BE:
      // products-adapter.ts
      export interface ProductsAdapter {
        get(): Product[];
        subscribe(cb: (p: Product[]) => void): () => void;
        add(product: Omit<Product,'id'>): void;
        update(id: string, updates: Partial<Product>): void;
        remove(id: string): void;
      }
      
      // use-products.ts
      export const createUseProducts = (adapter: ProductsAdapter) => {
        return function useProducts() {
          const [products, setProducts] = useState(adapter.get());
          useEffect(() => adapter.subscribe(setProducts), [adapter]);
          const addProduct = (p)=> adapter.add(p);
          // ...
          return { products, addProduct, ... }
        }
      }
      • 이렇게 하면 zustand/jotai adapter를 만들어 주입만 하면 됨.
  • 시나리오 B: Jotai / TanStack Query 사용

    • TanStack Query: 서버 상태/캐시 중심이라면 useProducts는 fetcher로 바꿔야 함. 현재 로컬 파일에 의존하는 기능(로컬 CRUD)과는 성격이 달라 adapter로 전환 필요.
    • Jotai/Jotai -> atom 기반: 같은 패턴(어댑터)으로 교체 가능.
  • 요약: 훅의 시그니처(API)를 고정하면 내부 구현체(state store)를 언제든지 바꿀 수 있음. 현재 구조는 훅 단위로 잘 분리되어 있어 교체 작업을 ‘훅 내부’로 한정할 수 있지만, 훅들이 직접 useLocalStorage를 호출하는 결합은 adapter를 통해 완화하세요.

  1. 구체적 코드 개선(AS-IS / TO-BE) 예시들
  • 문제1: AdminContext Provider 사용

    • AS-IS:
      // App.tsx
      <AdminContext value={{ isAdmin, setIsAdmin }}>
        ...
      </AdminContext>
    • TO-BE:
      <AdminContext.Provider value={{ isAdmin, setIsAdmin }}>
        ...
      </AdminContext.Provider>
  • 문제2: use(…) -> useContext(…)

    • AS-IS:
      import { use } from "react";
      const { setIsAdmin } = use(AdminContext);
    • TO-BE:
      import { useContext } from "react";
      const { setIsAdmin } = useContext(AdminContext);
  • 문제3: useCoupons addCoupon stale closure

    • AS-IS:
      const addCoupon = useCallback((newCoupon) => {
        const existingCoupon = coupons.find(c => c.code === newCoupon.code);
        if (existingCoupon) { throw ... }
        setCoupons(prev => [...prev, newCoupon]);
      }, []);
    • TO-BE:
      const addCoupon = useCallback((newCoupon) => {
        setCoupons(prev => {
          const existing = prev.find(c => c.code === newCoupon.code);
          if (existing) throw new AlreadyExistsCouponCodeError();
          return [...prev, newCoupon];
        });
      }, [setCoupons]);
  • 문제4: ProductCard에 remainingStock 별도 prop

    • AS-IS:
      <ProductCard product={{...product, stock: getRemainingStock(product)}} ... />
    • TO-BE:
      <ProductCard product={product} remainingStock={getRemainingStock(product)} ... />
    • ProductCard는 remainingStock prop을 수용하도록 수정.
  • 문제5: Notification API 일관화(legacy adapter)

    • AS-IS: addNotification({text, type})
    • TO-BE: 제공되는 useNotification API를 오버로드로 허용
      // use-notification.ts
      const addNotification = (messageOrText, type?) => {
        if (typeof messageOrText === 'string') {
          show({ id: ..., text: messageOrText, type: type || 'success' });
        } else {
          show(messageOrText);
        }
      }
  1. 테스트/질문들(추가 제안)
  • 단위 테스트: useCoupons.addCoupon(이미 있는 코드) 예외 동작, useProducts 필터링(검색어) 케이스, getRemainingStock 계산(다중 cart scenario)
  • E2E: 장바구니 흐름(재고 초과/쿠폰 적용/결제완료)
  • 문서화: 각 feature의 public 훅 시그니처와 adapter 인터페이스 문서화 필요

마무리 — 결론 (이 코드, 잘 썼는가?)

  • 장점: 도메인 훅과 UI 컴포넌트 분리는 잘 되어 있어 응집도는 전반적으로 높습니다. 이 구조는 패키지화/리팩토링/테스트하기 좋습니다.
  • 단점(치명적): React API 사용 실수(Provider/ useContext vs use())는 런타임 장애를 유발하므로 우선 수정 필요. 그 외 useCallback 의존성 문제도 빠르게 고치세요.
  • 전반적으로 설계 의도(엔티티 분리, 훅 중심 리팩토링)는 적절합니다. 구현상의 몇 가지 실수(React API 오용, stale closures)만 해결하면 좋은 코드가 될 것입니다.

권장 우선 작업 (우선순위)

  1. App.tsx: AdminContext → AdminContext.Provider 로 변경
  2. 모든 파일에서 use(AdminContext) / use(ToastValueContext) / use(ToastCommandContext) → useContext(...) 로 변경
  3. useCallback/ useMemo 의존성 검토(특히 useCoupons, useProducts, ShopPage의 콜백들)
  4. product.remainingStock 처리 방식 정리(도메인과 뷰의 분리)
  5. Notification API 정리(호출형식 통일)
  6. 이후: adapter 패턴으로 state store 추상화(전역 상태관리 교체가 쉬워짐), 패키지화 준비

필요하시면:

  • 제가 PR에 붙일 상세 코드 패치(diff)도 만들어 드릴 수 있습니다(예: App.tsx, Toast.tsx, use-coupons.ts 등 핵심 파일 10개 우선 수정).
  • 또는 "zustand adapter" 예시 코드와 함께 useProducts를 변환하는 실질적인 스텝을 제안해 드릴게요.

원하시면 먼저 치명적 문제(Provider / useContext 교체) 패치부터 PR 댓글 형식으로 제안해 드리겠습니다. 어느 쪽으로 도와드릴까요?

yong added 2 commits August 9, 2025 02:33
1. use-cart 훅 분리
2. ProductListSection 제거. 컴포넌트 뎁스 1 줄임.
3. sumMap 유틸 함수 분리
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.

2 participants