Skip to content

[3팀 장루빈] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#30

Open
JangRuBin2 wants to merge 8 commits intohanghae-plus:mainfrom
JangRuBin2:main
Open

[3팀 장루빈] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#30
JangRuBin2 wants to merge 8 commits intohanghae-plus:mainfrom
JangRuBin2:main

Conversation

@JangRuBin2
Copy link

과제의 핵심취지

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

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

과제 셀프회고

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

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

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

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

@JangRuBin2 JangRuBin2 changed the title 1 [3팀 장루빈] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 Aug 4, 2025
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 정리 / 리팩토링 시도 잘 하셨습니다. 변경량(파일 추가·이동·컴포넌트 분리 등)이 큽니다. 아래 리뷰는 코드·구조·아키텍처 관점에서 '응집도 / 결합도 / 상태관리 교체 시 영향 / 패키지화 가능성'에 초점을 맞춰 작성했습니다. PR 코멘트로 바로 붙여 넣어도 무방하도록 마크다운 + 코드 예시(AS-IS / TO-BE) 형태로 정리합니다.

요약

  • 무엇이 좋아졌나
    • App 단일파일에서 상당량을 분리(components/, admin/, product/ 등)해 UI/페이지 책임을 나눴습니다.
    • 타입 정의를 중앙화(src/types/product.type.ts)하고 초기 데이터(initialProducts, initialCoupons)를 모았습니다.
    • ProductContext, models 힌트 파일 생성 등 '더 깔끔한 구조'를 향한 첫 단계가 보입니다.
  • 남아있는 문제(우선순위)
    1. 상태(entities)와 UI가 여전히 혼재된 곳이 일부 있음(특히 App 파일들에 비즈니스 로직 잔존).
    2. 동일/유사 로직이 여러 App 버전(origin/basic/advanced/refactoring(hint))에 중복되어 유지비용 발생.
    3. 알림(addNotification) 등 cross-cutting concern이 여러곳에 중복/결합되어 있음.
    4. models/ 폴더에 핵심 순수 함수가 미구현(TODO) 되어있고, 구현/사용 흐름이 완전하지 않음.
    5. ProductContext가 도입되었으나 전체적으로 일관되게 사용되지 않음(일부 컴포넌트는 prop drilling, 일부는 context).

목차

  1. 종합 피드백 (키워드)
  2. PullRequestBody 관련 인사이트 & 질문(셀프회고 항목 피드백)
  3. 리뷰 받고 싶은 내용(빈칸 대체 답변)
  4. 상세 피드백(정의 → 문제 → AS-IS → TO-BE 코드)
  5. 권장 작업(우선순위로 정리)

  1. 종합 피드백 키워드
  • 분리(components / admin / product) — 긍정적
  • 타입 중앙화 (product.type.ts) — 긍정적
  • 중복(여러 App 파일) — 리스크
  • 모델(순수함수) 미완성 — 테스트 불가
  • 상태관리 일관성 부족 (local useState vs ProductContext)
  • 알림/유틸(Notifications / formatPrice) 결합도 높음
  • Path alias 설정(@/*) — 패키지화 준비
  • UI vs 엔티티(들) 계층 분리 시도 — 좋음

  1. PullRequestBody의 "과제 셀프회고" / "내가 제일 신경 쓴 부분"에 대한 피드백 (인사이트 형태)
  • 현재 PR에서 가장 신경 쓴 부분으로 보이는 것은 '엔티티와 UI의 분리'와 '컴포넌트의 모듈화'입니다. 제품/쿠폰/장바구니 관련 데이터를 types로 모으고, UI를 작은 컴포넌트로 분리한 점은 바람직합니다.
  • 다음 수준으로 고려할 포인트
    • 비즈니스 로직(예: 할인 계산, 총합 계산)을 models로 완전히 분리해서 단위 테스트를 만들면 설계의 퀄리티가 더 드러납니다.
    • 상태 관리를 전역으로 변경(예: Jotai / Zustand / Redux / Tanstack Query)할 때 변경범위를 최소화하려면 '훅 API'를 정의하여 내부 구현을 교체 가능하게 만들어두는 것이 좋습니다(예: useCart() 인터페이스 고정).
    • Notification 같은 cross-cutting concern은 이벤트 콜백(onSuccess/onError)이나 Hook 추상(useNotification)으로 추출해 재사용성을 높이세요.

질문(자기검증을 위한 제안)

  • 제품/쿠폰/장바구니 계산 로직을 각각 models로 만든 뒤, 그 함수들에 대한 단위테스트는 작성했나요? (예: calculateCartTotal의 edge-case)
  • UI 컴포넌트와 엔티티 컴포넌트 사이의 인터페이스가 명확한가요? (props에서 entity를 넘기는가, 아니면 primitive viewData를 넘기는가)
  • Notification, formatPrice 등의 함수들을 엔티티 영역에서 호출하고 있나요? UI 혹은 훅에서만 호출하도록 역할을 정리해보셨나요?

  1. PullRequestBody의 "리뷰 받고 싶은 내용이나 궁금한 것" — (빈칸 대체 답변)
  • 궁금한 것: “이 구조가 상태관리 솔직히 바꾸기 쉬운 구조인가요?”
    • 요약: 아직 완전히 쉬운 상태는 아닙니다. 지금처럼 컴포넌트 내부에 상태와 로직(useState + 계산 함수)이 남아있다면 상태관리 라이브러리를 바꾸는 범위가 넓습니다. 반대로, 핵심 엔티티 상태 및 행위를 훅(useCart, useProducts, useCoupons)으로 추출하면 내부 구현만 바꿔도 UI는 그대로 둬서 교체 부담이 작아집니다.
  • 궁금한 것: “모듈(패키지)으로 떼어내기 쉬운가요?”
    • 요약: UI 컴포넌트들은 비교적 떼어내기 쉬우나, 엔티티 로직(계산 함수)과 상태 훅은 현재 분산되어 있어 한번 더 정리 후 패키지화해야 매끄럽습니다.

  1. 상세 피드백 — 개념 정의 → 문제 → AS-IS → TO-BE

A. 응집도 정의 (제안)

  • 응집도(Cohesion): 한 모듈(파일/패키지)이 맡은 책임이 얼마나 일관되고 밀접한가를 나타냄.
    • 높은 응집도: 한 파일/모듈이 한 종류의 책임(예: cart 계산)만 수행 → 변경 시 수정 범위가 작음.
    • 낮은 응집도: 한 파일/모듈이 UI rendering, 계산, 로컬 저장, API 호출 등을 동시에 담당 → 변경 시 여러 파일을 건드려야 함.
  • 내가 제안하는 실무 기준 (프로젝트용):
    1. 변경 경로가 짧을 것(변경 시 수정 파일 수 최소화).
    2. 패키지화 시 '자명하게' 떼어낼 수 있을 것(도메인 경계가 명확).
    3. 외부 구현(예: 상태관리 라이브러리) 변경 시 내부 구현만 바꾸면 될 것.

B. 결합도 정의 (요약)

  • 결합도(Coupling): 모듈 간 의존성 강도. 낮을수록 독립적으로 교체 가능.
  • 좋은 패턴: 함수/컴포넌트는 인터페이스(옵션 객체, 콜백)으로 결합을 줄인다.

문제항목 1: 상태관리 라이브러리 교체 시 (예: useState -> jotai/tanstack-query/zustand/redux)

  • 문제 정의: 현재 다수의 상태( products, cart, coupons, notifications )가 로컬 useState로 흩어져 있고, 핵심 행위(addToCart, updateQuantity, calculateCartTotal 등)가 여러 컴포넌트(또는 App에) 분산되어 있어 상태관리 라이브러리를 바꾸면 영향범위가 넓음.
  • AS-IS (일부 발췌, 단축)
// src/basic/App.tsx (AS-IS 요약)
const [cart, setCart] = useState<CartItem[]>([]);
const addToCart = useCallback((product) => {
  const remainingStock = getRemainingStock(product);
  if (remainingStock <= 0) { addNotification('재고 부족', 'error'); return; }
  setCart(prev => { /* logic: update or push */});
  addNotification('장바구니에 담았습니다', 'success');
}, [cart]);
  • 문제가 되는 이유
    • addToCart가 components 내부 로직(remainingStock, addNotification, setCart)의 결합에 의존 → 상태 라이브러리 변경 시 이 함수를 찾아 모두 바꿔야 함.
  • TO-BE: useCart 훅으로 추상화 (hook 인터페이스 고정, 내부 구현만 바꾸면 됨)
    • 인터페이스 설계 (export된 API를 고정)
// packages/hooks/useCart.ts (TO-BE: store-agnostic)
export type UseCart = {
  cart: CartItem[];
  totalItemCount: number;
  addToCart: (product: Product) => Promise<{ ok: boolean; message?: string }>;
  removeFromCart: (productId: string) => void;
  updateQuantity: (productId: string, quantity: number) => Promise<{ ok: boolean; message?: string }>;
  calculateTotals: (couponCode?: string) => { totalBeforeDiscount: number; totalAfterDiscount: number };
};

// 간단한 구현 예 (useState 기반)
export const useCartImpl = (): UseCart => {
  const [cart, setCart] = useState<CartItem[]>([]);
  const addToCart = async (product: Product) => {
    // 내부 로직: 재고 확인, setCart update, return result (onSuccess/onError는 caller가 결정)
    // 정리된 책임: 훅은 cart 상태와 행위만 담당
  };
  return { cart, totalItemCount, addToCart, removeFromCart, updateQuantity, calculateTotals };
};
  • 컴포넌트 사용 예:
const { cart, addToCart, calculateTotals } = useCart();
const handleAdd = async (product) => {
  const res = await addToCart(product);
  if (!res.ok) showNotification(res.message, 'error'); // notification은 호출측에서 결정
}
  • 이렇게 하면:
    • useCart 내부만 바꿔도(예: jotai atom 사용) App/컴포넌트 코드는 변경되지 않음.
    • 상태 라이브러리 전환 시 영향범위가 hook 내부로 국한됨.

추가 권장: 모든 엔티티 상태(products, coupons 등)는 유사하게 useProducts, useCoupons 훅으로 추상화하세요.


문제항목 2: 모듈화(패키지화) 가능성

  • 문제 정의: 현재 구조는 components/, admin/ 로 분리돼 있으나 models(순수함수)와 hooks(상태 래퍼)가 완전히 나뉘지 않아 패키지로 떼어내기 애매함.
  • 평가(응집도 관점)
    • UI 컴포넌트 집합: 응집도 높음(재사용 UI 패키지로 떼어내기 수월).
    • 엔티티 모델/훅: 응집도 낮음 → 재정리 필요(계산 함수, 훅, 타입을 같은 패키지로 묶기).
  • TO-BE: 권장 패키지 구조
/packages
  /entities
    /cart (models, utils, tests)  -> calculateItemTotal, calculateCartTotal
    /product (types, product utils)
  /hooks
    useCart, useProducts, useCoupons, useNotification
  /ui
    Button, Input, Modal, ProductCard, CartItemView (presentation-only)
  /webapp (app specific, compose above packages)
  • 간단한 export 예 (entities/cart/index.ts)
export * from './cart.model'; // calculateItemTotal, getMaxApplicableDiscount, ...
export * from './cart.types';
  • 장점:
    • entities 패키지는 다른 프로젝트에서도 재사용 가능(독립적, pure function 포함).
    • hooks는 상태관리 교체 시 구현만 바꾸면 됨.
    • ui 패키지는 디자인 시스템으로 재사용 가능.

주의:

  • path alias('@/*')는 monorepo/패키지화 시 번들링/tsconfig paths 설정을 재검토해야 합니다. (패키지 내부에서 상대경로로 구현하거나 build step에 paths 처리)

문제항목 3: 응집도(현황 평가 & 제안)

  • 정의(요약): 한 파일/폴더가 '한 가지 목적'만 갖는 정도.
  • 현황:
    • positive: basic/components/* 의 작은 컴포넌트들은 UI 책임만 맡아 응집도가 높음.
    • negative: App 파일들(특히 refactoring(hint)/App.tsx, origin/App.tsx 등)은 UI, 상태, 계산, 로컬 storage, form validation이 섞여 있음 → 낮은 응집도.
  • 개선안 (우선순위):
    1. 모든 순수 계산 함수(models) → packages/entities/cart.ts 로 이동(테스트 작성).
    2. 모든 상태 관리는 hooks로 추출(실제 store는 hook 내부에 캡슐화).
    3. UI 컴포넌트는 presentation-only (props: view data + callbacks) 로 유지.

AS-IS / TO-BE 예 (formatPrice)

  • AS-IS (컴포넌트 내부에 혼재)
const formatPrice = (price: number, productId?: string): string => {
  if (productId) {
    const product = products.find(p => p.id === productId);
    if (product && getRemainingStock(product) <= 0) return 'SOLD OUT';
  }
  if (isAdmin) return `${price.toLocaleString()}원`;
  return `₩${price.toLocaleString()}`;
}
  • TO-BE (pure function + adapter)
// packages/entities/formatters.ts
export const formatCurrency = (price: number, opts?: { locale?: string; currency?: string }) =>
  `${opts?.currency || '₩'}${price.toLocaleString(opts?.locale)}`;

// UI adapter (component or hook):
const displayPrice = (price: number, productId?: string, isAdmin?: boolean, remainingStock?: number) => {
  if (productId && remainingStock !== undefined && remainingStock <= 0) return 'SOLD OUT';
  return isAdmin ? `${price.toLocaleString()}원` : formatCurrency(price);
}
  • 효과: formatCurrency는 테스트/패키지화 가능, UI쪽은 최소한의 결정만 하게 됨.

문제항목 4: 결합도 (예: Notification, callback style)

  • 문제: addNotification 함수를 훅/컴포넌트 내부에서 직접 호출하거나 여러곳에서 사용. 또 일부 훅 함수들은 addNotification을 직접 의존(즉, 상태 훅이 알림 로직에 결합됨).
  • 나쁜 예시(결합 높음):
// bad
const useAddProduct = (addNotification) => { ... };
// 사용자가 addNotification의 구체형식(addNotification: (msg,type)=>void)에 의존.
  • 좋은 예시(낮은 결합): 이벤트 콜백(onSuccess/onError) 또는 Promise 반환으로 호출자가 결정
// good: onSuccess/onError 옵션
type UseAddProductOptions = { onSuccess?: () => void; onError?: (message: string) => void; };
const useAddProduct = () => {
  const addProduct = async (product: Product, opts?: UseAddProductOptions) => {
    try {
      // business logic
      opts?.onSuccess?.();
      return { ok: true };
    } catch (e) {
      opts?.onError?.(String(e));
      return { ok: false, message: String(e) };
    }
  };
  return { addProduct };
};

// 호출부
const { addProduct } = useAddProduct();
addProduct(p, { onSuccess: () => showNotification('추가됨', 'success') });
  • 장점: 훅이 UI concern(알림)을 몰라서 재사용성이 높아짐.

문제항목 5: 모델(순수함수) 분리 및 테스트성

  • 현재: refactoring(hint)/models/cart.ts 에 함수 템플릿이 있으나 실제 사용 형태(생성/배치)가 분리되지 않음.
  • 권장 TO-BE:
    • calculateItemTotal(item) : pure
    • calculateCartTotal(cart, selectedCoupon?) : pure
    • addItemToCart(cart, product) : pure -> 변경된 cart를 반환(immutable)
    • updateCartItemQuantity(cart, productId, quantity) : pure
  • AS-IS vs TO-BE 예
    AS-IS 간단화:
// App 내부
const calculateItemTotal = (item) => { /* uses cart, discounts, etc */ }

TO-BE (models/cart.ts)

export const calculateItemTotal = (item: CartItem, hasBulkPurchase = false): number => {
  const discount = getMaxApplicableDiscount(item, hasBulkPurchase);
  return Math.round(item.product.price * item.quantity * (1 - discount));
};

export const calculateCartTotal = (cart: CartItem[], coupon?: Coupon) => {
  const totalBefore = cart.reduce((s, it) => s + it.product.price * it.quantity, 0);
  const totalAfter = cart.reduce((s, it) => s + calculateItemTotal(it, cart.some(ci => ci.quantity >= 10)), 0);
  // apply coupon...
  return { totalBeforeDiscount: totalBefore, totalAfterDiscount: totalAfter };
};
  • 장점: pure 함수는 단위테스트 작성 용이(로직 검증), 버그 발생 시 원인 국한.

문제항목 6: 여러 App 파일(중복) 정리 필요

  • 현재: src/basic/App.tsx, src/origin/App.tsx, src/advanced/App.tsx, src/refactoring(hint)/App.tsx 등 여러 변형이 병존. 협업/배포 시 혼란도 증가.
  • 권장:
    • 하나의 canonical App만 유지하고 나머지는 예제/참고용으로 docs 폴더에 옮기기.
    • 중복 로직은 packages로 공통화 (hooks/models/ui) 후 App에서 조합.

문제항목 7: path alias 및 types 위치

  • 잘한 점: types/product.type.ts로 타입을 통합한 점.
  • 유의점: monorepo나 패키지로 분리 시 '@/*' alias 사용은 빌드/테스트 환경에서 별도 설정 필요. 패키지 내부 코드에서는 상대 경로(./)를 권장하거나 package-level exports를 사용하세요.

  1. 권장 작업(우선순위)
  1. 모델(순수함수) 완성 및 테스트 추가
    • refactoring(hint)/models/cart.ts 완성
    • 단위 테스트로 핵심 케이스(쿠폰 적용, bulk discount, edge case) 검증
  2. 훅 추출 (useCart, useProducts, useCoupons, useNotification)
    • 인터페이스 문서화(반환값 + 동작/Promise 규격)
    • 내부 구현을 useState 기반으로 두되, 추후 상태 라이브러리로 교체 가능하게 캡슐화
  3. 알림(Notification) 결합도 낮추기
    • 훅은 이벤트/Promise 결과만 반환, 알림은 컴포넌트(또는 useNotification 훅)에서 처리
  4. UI ↔ 엔티티 경계 확실히 하기
    • UI 컴포넌트는 presentation-only (viewData + callbacks)
    • 엔티티 전용 코드(계산, 검증)는 entities 패키지로 이동
  5. 중복 App 정리
    • canonical App만 유지, 나머지는 reference 또는 브랜치로 정리
  6. 모노레포/패키지 준비
    • packages 구조 제안대로 옮기기 (entities/hooks/ui/app)
    • 각 패키지에 package.json, 빌드 스크립트, exports 정리
  7. 문서화
    • 각 hook/interface의 contract(파라미터/반환값/에러 처리)를 README에 문서화

마무리

  • 현재 리팩토링은 좋은 방향(컴포넌트 분리, 타입중심 수정)을 향하고 있습니다. 다음 단계는 '모듈화(훅/모델 분리) → 테스트 → 상태관리 라이브러리 교체(필요 시)' 순으로 진행하시면 유지보수성이 크게 개선됩니다.
  • 원하시면 다음 작업(예: useCart 훅 구현 예제 전체 코드, models/cart.ts 완전 구현, 혹은 Jotai/Zustand 전환 예시)을 구체적으로 만들어 드리겠습니다. 어느 부분을 먼저 도와드릴까요?

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