Skip to content

[3팀 이정우] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 🦍#51

Open
JungWoo0203 wants to merge 12 commits into
hanghae-plus:mainfrom
JungWoo0203:main
Open

[3팀 이정우] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 🦍#51
JungWoo0203 wants to merge 12 commits into
hanghae-plus:mainfrom
JungWoo0203:main

Conversation

@JungWoo0203
Copy link
Copy Markdown

@JungWoo0203 JungWoo0203 commented Aug 7, 2025

jungwoo0203.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는 분리되어 있나요?

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

심화과제

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

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

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

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

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

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

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

과제 셀프회고

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

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

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

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

  • Jotai atom 구조가 현재는 파일 하나(atoms.ts)에 몰려 있습니다. 도메인별 atom 모듈화하는 것이 좋을까요?
  • 할인 계산 로직을 cartModel 레이어까지 더 분리한다면 테스트 용이성이 높아질지 궁금합니다.

@eveneul
Copy link
Copy Markdown

eveneul commented Aug 8, 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의 변경 파일들을 기반으로 작성했으며, 향후 상태관리 라이브러리 변경이나 패키지화(모듈화) 관점에서 실무적으로 고려해야 할 점들을 시나리오별로 정리했습니다.

요약:

  • 지금 PR은 App에서 로컬 상태(localStorage + useState 등)를 떼어내고, jotai atom + custom hooks(useCart, useProducts, useCoupons 등) + UI 컴포넌트로 잘 분리한 리팩터링입니다.
  • 전체적으로 계층(entities -> features/hooks -> UI components) 분리가 잘 되어 있어 패키지화/교체가 비교적 수월하지만, 몇몇 결합 지점(예: addNotification 전달 방식, 로컬스토리지 직접 사용)은 개선 여지가 있습니다.

아래는 요구하신 흐름(시나리오 제시 → 현재 코드가 대응할 때 변화 → 이를 토대로 잘 작성되었는지 평가)대로 정리한 피드백입니다.


질문(간단 답변)

  • PullRequestBody의 "과제 셀프회고 / 내가 제일 신경 쓴 부분" 내용이 비어있습니다. 작성해주시면 피드백을 더 구체화하겠습니다.
  • "리뷰 받고 싶은 내용이나 궁금한 것" 역시 비어있어 가정 기반으로 리뷰했습니다. (예: 상태관리 선택, 모듈화 가능성, 결합도/응집도)

종합 피드백

  1. 핵심 키워드 (PullRequestFiles 기반)
  • 상태관리: jotai (atom, atomWithStorage) 사용
  • 훅화: useCart, useProducts, useCoupons, useDebounce
  • 유틸 분리: utils/cart.ts (계산 로직), hooks에 비즈니스 로직 위임
  • 컴포넌트 분리: ProductCard, CartItem, CartSummary, admin 폴더의 Form/Table/Card 등
  • 퍼시스턴스: atomWithStorage & 직접 localStorage 사용 혼용
  • 결합 포인트: addNotification prop 전달, 일부 컴포넌트에 직접 storage 쓰임 가능성
  • 패키지화 잠재성: entities + hooks + components 단위로 추출 가능
  1. PR 본문의 "과제 셀프회고" / "신경 쓴 부분"이 비어있으므로, 대신 인사이트 피드백과 추가 질문을 남깁니다.
  • 인사이트
    • 훅과 유틸로 비즈니스 로직을 분리한 결정은 재사용성, 테스트 용이성 측면에서 매우 옳습니다. 특히 calculateItemTotal / calculateCartTotal 같은 순수함수를 utils로 뺀 점은 좋습니다.
    • components는 UI에 집중하고 hooks는 상태·로직에 집중하도록 역할을 분리한 점은 유지보수성 및 모듈화에 유리합니다.
    • Jotai의 atomWithStorage로 "간단한 local persistence"를 구현한 점은 빠른 개발에 적합하지만, 로컬스토리지 접근을 훅 내부에 숨겼을 경우(혹은 옵션으로 주입 가능하게 만들 경우) 패키징 시 유연성이 커집니다.
  • 이어서 생각해볼 질문
    • addNotification을 모든 관리자 폼에 전달하고 있는데, 이 알림 기능을 훅 내부 또는 전역(Toast provider 등)로 뺀다면 어떤 장/단점이 있을까요?
    • hooks가 localStorage를 직접 쓰고 있습니다. 패키지로 배포할 때 storage 접근을 "주입(injection)"하면 어떤 이점이 있나요? (테스트, SSR, 플랫폼 이식성)
    • 현재 훅의 public API(함수 시그니처)는 어떤 버전 보장 전략을 적용할 것인가요? (패키지화 시 breaking change 관리)
  1. "리뷰 받고 싶은 내용"이 비어있으므로, 예상 질문별 간단한 답변
  • "jotai로 묶은 상태, 다른 라이브러리로 바꿀 때 어떻게 해야 하나요?"
    • hooks의 public API를 유지한 채 내부 구현만 바꾸면 됩니다(예: useCart는 동일한 반환값을 유지). 즉 adapter 레이어로 감싸면 대체가 쉬워집니다.
  • "패키지화할 수 있을까요?"
    • 네, entities(types), pure utils, domain hooks(useCart, useProducts, useCoupons), UI 컴포넌트를 층별로 분리하면 패키지화가 가능합니다. 하지만 localStorage, DOM 의존, addNotification 같은 외부 의존은 주입/추상화해야 합니다.

상세 피드백 — 개념 정의 및 문제/AS-IS/TO-BE

먼저 개념 정의(요청하신 방식)

  • 응집도(cohesion) 정의 (제안)
    • 응집도는 "관련된 기능이 한 모듈(파일/패키지/훅)에 얼마나 잘 모여 있는지"를 의미합니다.
    • 실무 규칙 (본 PR에 적용할 기준):
      1. 변경 경로가 짧다: 특정 개념을 고치려면 수정해야 할 파일/함수/라인의 개수(동선)가 짧을수록 응집도가 높다.
      2. 모듈로 잘 떼어낼 수 있다: 해당 코드 블록(도메인 훅 + 유틸 + 타입)을 패키지로 떼어낼 때 외부 의존(특정 프레임워크/브라우저 API 등)이 적을수록 응집도가 높다.
  • 결합도(coupling) 정의
    • 결합도는 모듈/함수/컴포넌트가 서로 얼마나 직접적으로 의존하는지입니다.
    • 낮은 결합도는 "인터페이스(명확한 함수 시그니처, 콜백)"를 통해 의존을 최소화함으로써 달성합니다.
    • 예: 좋은 인터페이스 예시는 onSuccess/onError 콜백 패턴, 훅이 내부 동작을 숨기고 결과만 반환.

이제 PR에서 다룰 주요 문제들을 하나씩 정리합니다.

  1. 문제: 상태관리 라이브러리(Jotai) -> 다른 라이브러리(또는 제거)로 바꿀 경우
  • 정의: 현재 훅들은 내부에서 jotai(useAtom, atomWithStorage)를 직접 사용. 훅이 public API를 제공하지만 내부 구현이 jotai에 종속되어 있음.
  • AS-IS (현재)
    • useCart.ts (요약)
      • 내부에서 useAtom(cartAtom)
      • return { cart, addToCart, removeFromCart, updateQuantity, ... }
    • components 및 App은 useCart의 반환값에 의존하지 atom을 직접 사용하지 않음(대체로 올바름).
  • 변화 시 예상 영향 (시나리오별)
    • a) Zustand로 교체
      • 내부 구현을 Zustand store로 바꾸고 useCart의 반환값을 동일하게 유지하면 UI는 변경사항 없이 동작.
      • 다만 Jotai의 atomWithStorage와 같은 자동 퍼시스턴스 API가 없어, persist 로직을 직접 구현하거나 middleware(persist) 사용 필요.
    • b) Redux로 교체
      • 액션/리듀서/selector 패턴으로 교체. useCart 훅을 Redux selector + dispatch 래퍼로 변경하면 외부 코드는 변경 불필요.
      • 테스트 작성/타입 정의 등 보일러플레이트가 증가.
    • c) TanStack Query 중심(로컬 캐시로 사용)
      • 일반적으로 서버 데이터 캐싱에 좋지만, 로컬 엔티티(장바구니) 관리는 mutation+queryClient로 가능. 하지만 실시간 동기화/구조가 복잡할 수 있음.
  • TO-BE (권장 패턴 — 어댑터 레이어)
    • 핵심 원칙: 훅의 public 시그니처를 유지하고 내부 구현(상태 라이브러리 사용)을 추상화한다.
    • AS-IS 예 (useCart의 내부가 jotai)
      • (간단화) current:
        • import { useAtom } from 'jotai';
        • const [cart] = useAtom(cartAtom);
    • TO-BE (adapter pattern; 구현체 교체 가능)
      • createCartAdapter.ts (Zustand/Redux/TBD를 감쌈)
        • export type CartAdapter = { getCart(): CartItem[]; add(product): Result; updateQuantity(...): Result; subscribe(fn): unsubscribe; }
      • useCart.ts (훅)
        • import { cartAdapter } from './adapters/cartAdapter';
        • export const useCart = () => {
          const [state, setState] = useState(cartAdapter.getCart());
          useEffect(() => {
          const unsub = cartAdapter.subscribe(setState);
          return unsub;
          }, []);
          const addToCart = (p) => cartAdapter.add(p);
          // ... 반환 인터페이스 동일
          }
      • cartAdapter 구현만 바꾸면 전체 코드 변경 최소화.
  • 결론/평가:
    • 현재 구조는 "hooks가 public API를 제공"하므로 라이브러리 교체가 비교적 쉬운 편입니다. 다만 atom/atomWithStorage 사용이 내부에 섞여 있어 adapter를 추가하면 완전 교체 가능성이 높아집니다.

코드 예시 (AS-IS vs TO-BE)

  • AS-IS (간략, 사용된 현재 패턴)

    • useCart.ts (일부)
      • const [cart, setCart] = useAtom(cartAtom);
      • const addToCart = useCallback((product) => { ... setCart(next) }, [cart]);
  • TO-BE (adapter 레이어로 분리: pseudo)

    • adapters/cartAdapter.ts (Zustand / Redux / jotai 구현 중 하나)
      • export const cartAdapter = {
        getCart() { /.../ },
        subscribe(cb) { /.../ },
        add(product) { /.../ return { success: true } },
        updateQuantity(productId, q) { /.../ },
        }
    • useCart.ts
      • const useCart = () => {
        const [cart, setCart] = useState(cartAdapter.getCart());
        useEffect(() => cartAdapter.subscribe(setCart), []);
        return {
        cart,
        addToCart: (p) => cartAdapter.add(p),
        updateQuantity: (id, q) => cartAdapter.updateQuantity(id,q),
        }
        }

이렇게 하면 state lib 교체 시 cartAdapter만 교체하면 됨.


  1. 문제: 모듈화/패키지화(패키지로 배포해야 할 경우)
  • 체크포인트 (응집도 / 결합도 관점)
    • 응집성:
      • 훅 + 관련 utils + types가 모여 있으면 패키지화 쉬움. (예: @yourorg/cart: useCart + types + utils/cart.ts)
      • 현재: utils/cart.ts, hooks/useCart.ts, atoms.ts, constants/initialData.ts가 도메인별로 묶여 있음 -> 좋음
      • 하지만 앱(App.tsx)에서 addNotification 등 UI/Toast 의존을 훅/폼에 전달하는 방식은 패키지로 떼어내기엔 UI 의존성이 남음.
    • 결합성:
      • 외부 인터페이스(훅 반환값)를 통해 결합이 낮아졌음 — 긍정적.
      • 다만 Form 컴포넌트들이 addNotification을 직접 prop으로 요구: 결합도가 높음(토스트 로직이 UI에 결합).
  • AS-IS (문제 상황)
    • atoms.ts와 hooks 내에 localStorage 관련 로직이 섞여 있음.
    • AdminProductForm, AdminCouponForm에 addNotification prop을 직접 받음.
    • components가 utils 또는 hooks에 너무 직접 의존하거나 역으로 hooks가 UI API를 알면 추출이 어려움.
  • TO-BE (패키지화 시 권장)
    • 도메인 패키지 분리 제안(예)
      • packages/
        • cart/
          • src/
            • index.ts (export types, useCart, utils)
            • hooks/useCart.ts (no UI deps)
            • utils/cart.ts (pure)
            • types.ts
        • products/
        • coupons/
      • 앱은 packages/* 를 종속성으로 설치하고 useCart hook을 사용.
    • 두가지 권장 변경:
      1. 훅에서 localStorage 등 구현 세부사항을 숨기되 "storage adapter"를 인자로 받거나 옵션으로 제공
        • useCart({ storage?: StorageAdapter }) <-- 테스트/SSR/모듈화에 유리
      2. UI 알림을 추상화: hook/form이 addNotification을 직접 요구하지 않고 onSuccess/onError 콜백 패턴을 사용하거나, 앱 전역의 ToastProvider를 통해서만 처리
        • 예: useProducts.addProduct({ ... }).then(() => onSuccess()) 또는 const res = addProduct(); if (!res.success) onError(res.message)
  • 구체적 코드 예시 (AS-IS vs TO-BE)
    • AS-IS (AdminProductForm prop)
      • <AdminProductForm addNotification={addNotification} ... />
      • AdminProductForm: (props) => { props.addNotification('...', 'error') }
    • TO-BE (콜백/결과 기반)
      • App:
        • const res = await addProductHook(newProduct);
        • if (res.success) showToast('성공'); else showToast(res.message)
      • AdminProductForm:
        • onSubmit -> call props.onSubmit(formState).then(handleResult)
      • 또는 더 깔끔: AdminProductForm는 순수 UI만 담당하고, 제출 이벤트는 onSubmit(form)으로 상위에 위임
    • Storage 의존 제거 예시
      • AS-IS: useProducts.ts persist() { localStorage.setItem(...) }
      • TO-BE:
        • type StorageAdapter = { getItem(key): string|null; setItem(k,v): void; removeItem(k):void }
        • useProducts({ storage = defaultBrowserStorage }) { storage.setItem(...) }
        • defaultBrowserStorage는 localStorage wrapper; 패키지 소비자는 테스트에서 mock storage 주입

결론: 현재 코드 구조(도메인별 훅 + utils + 컴포넌트 분리)는 패키지화에 좋은 출발점입니다. 단, 외부 사이드 이펙트(localStorage, addNotification 등)를 추상화하면 더 매끄럽게 분리/배포 가능합니다.


  1. 문제: 응집도/결합도 구체 점검 (파일 단위)
  • 긍정적 사항
    • utils/cart.ts에 할인·계산 로직이 잘 모여 있습니다(순수함수로 잘 분리). 이것은 테스트 가능성·재사용성 측면에서 매우 좋음.
    • UI 컴포넌트(상품 카드, 카트 아이템, 요약, admin 카드/폼/테이블)가 잘 분리되어 있어 뷰/로직 분리에 충실합니다.
    • hooks가 CRUD 인터페이스(add/update/delete 등)를 가진 점은 좋은 API 설계입니다.
  • 개선 권장(응집도/결합도)
    • atoms.ts가 도메인 전역(atomWithStorage)을 정의하는 방식은 편하지만, 패키지로 떼면 jotai 의존을 밖으로 빼는 것이 좋습니다. atoms를 내부 구현으로 숨기고 hooks가 공개 API를 책임지도록 바꾸세요.
    • addNotification 전달은 결합도 상승 포인트입니다. UI 컴포넌트(폼)는 onSubmit 이벤트만 제공하고 성공/실패 처리는 사용하는 쪽(App)에서 하도록 변경하면 결합도 낮아집니다.
    • useCoupons/useProducts/useCart에서 localStorage를 직접 쓰는 부분은 storage adapter 의존 주입으로 바꿀 것을 권장합니다. (응집도 향상: "퍼시스턴스" 책임을 별도 레이어로 분리)

AS-IS vs TO-BE code examples (실제 코드 스니펫)

  1. Notification 전달 — AS-IS
  • App.tsx:
    • <AdminProductForm addNotification={addNotification} ... />
  • AdminProductForm.tsx:
    • props.addNotification('가격은 0보다 커야 합니다', 'error');

TO-BE (onSubmit으로 비즈니스 로직을 반환하도록)

  • App.tsx:
    • <AdminProductForm onSubmit={async (form) => {
      const res = await addProductHook(form);
      if (res.success) addNotification('상품 추가 성공');
      else addNotification(res.message);
      }} ... />
  • AdminProductForm.tsx:
    • const handleSubmit = (e) => { e.preventDefault(); props.onSubmit(formState); }
  1. Storage 주입 — AS-IS (useProducts 내부에 localStorage 직접 사용)
  • useProducts.ts:
    • const persist = (next) => { setProducts(next); localStorage.setItem(storageKey, JSON.stringify(next)); }

TO-BE (주입)

  • types:
    • export type StorageAdapter = { getItem: (k:string)=>string|null; setItem:(k:string, v:string)=>void; removeItem: (k:string)=>void; }
  • useProducts.ts:
    • export const useProducts = ({ storage = browserStorage }:{storage?:StorageAdapter}) => {
      const persist = (next) => { setProducts(next); storage.setItem(storageKey, JSON.stringify(next)); }
      }
  • browserStorage default 구현은 단순히 window.localStorage 래퍼

추가로 발견된 코드 레벨 개선 포인트(사소하지만 실용적)

  • index.html에서 script 경로가 /src/advanced/main.tsx 인데, Vite base가 변경된 것과 일치하는지 확인 필요.
  • App.tsx에 주석 처리된 initialProducts/initialCoupons 코드가 남아있습니다. (정리 권장)
  • 일부 파일에서 calculateItemTotal을 호출하는 시그니처가 혼용된 것 같습니다(useCart 훅도 calculateItemTotal을 내보냄). 동일 시그니처(예: (item, cart?) 또는 hook의 메서드로만 사용)로 통일하면 혼동을 줄입니다.
  • AdminProductForm/AdminCouponForm에게 addNotification을 직접 전달하는 대신 onSubmit 기반으로 변경 권장(위 참조).
  • 훅들의 useCallback 의존성 배열이 현재 cart/products 상태를 직접 참조합니다. adapter로 분리하면 의존성 관리가 더 쉬워집니다.
  • utils/cart.ts의 getMaxApplicableDiscount에서 상수(예: 대량 구매 추가 할인 0.05, 최대 할인 0.5)는 상수로 분리하면 정책 변경에 유리합니다.

테스트 권장

  • utils/cart.ts는 순수함수로 되어 있어 유닛테스트 작성이 쉽습니다. calculateItemTotal, calculateCartTotal, updateCartItemQuantity에 대한 단위 테스트를 권장합니다(경계값: 재고 초과, 0 또는 음수 수량, 쿠폰 적용 등).

마무리 권고사항(우선순위)

  1. addNotification 전달 → onSubmit/result 기반으로 변경 (결합도 개선, 재사용성↑)
  2. hooks 내 localStorage 사용 → storage adapter 주입 인터페이스 도입 (패키징·테스트·SSR 대비)
  3. adapter 패턴으로 상태관리 라이브러리 의존 분리 (jotai 교체 대비)
  4. 순수 유틸(utils/cart.ts)에 대한 단위테스트 추가
  5. 불필요한 주석/중복 코드 삭제 및 시그니처 일관화

종합 멘트

전반적으로 훅/유틸/컴포넌트로 역할을 잘 분리하였고, 엔티티 로직이 utils에 잘 정리되어 있어 응집도가 비교적 높습니다. 결합도를 더 낮추려면 UI(토스트/알림)와 훅(도메인 로직)을 명확히 분리하고, persistence 및 상태 라이브러리 의존을 추상화하면 패키지화·교체가 훨씬 쉬워집니다.

원하시면 다음 단계로,

  • 제안한 adapter 패턴을 적용한 리팩터링 코드(예: cartAdapter + useCart wrapper) 예시를 만들어 드리거나,
  • AdminForm의 onSubmit 기반 변경 PR용 패치(diff) 예시를 만들어 드릴 수 있습니다.

다음에 어떤 부분을 우선적으로 고쳐볼지 알려주시면 구체적 코드(AS-IS/TO-BE) 패치를 만들어 드리겠습니다.

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