Skip to content

[4팀 김지혜] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #42

Open
adds9810 wants to merge 54 commits intohanghae-plus:mainfrom
adds9810:main
Open

[4팀 김지혜] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #42
adds9810 wants to merge 54 commits intohanghae-plus:mainfrom
adds9810:main

Conversation

@adds9810
Copy link

@adds9810 adds9810 commented Aug 6, 2025

과제링크

https://adds9810.github.io/front_6th_chapter2-2/index.basic.html
https://adds9810.github.io/front_6th_chapter2-2/index.advanced.html

과제의 핵심취지

  • 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는 잘 제거했나요?
  • 전체적으로 분리와 재조립이 더 수월해진 결합도가 낮아진 코드가 되었나요?

과제 셀프회고

항해의 절반을 지나오면서 과제를 해나가는 것 자체에는 익숙해졌지만, 매 과제마다 등장하는 새로운 개념과 이론에 여전히 허우적거리는 중입니다. 이번 과제에서 가장 큰 고민거리였던 것은 Entity와 UI의 개념과 역할 분리였습니다.
화요일에 진행되었던 팀원들과의 토론을 통해 다양한 관점을 접할 수 있었고, 이는 큰 도움이 되었습니다. "엔티티는 비즈니스 주체다", "앱의 명사에 해당하는 데이터 덩어리다"와 같은 명확한 정의부터, "UI는 어디까지 상태를 가져야 하는가?", "엔티티가 절대 해서는 안 되는 것은 무엇인가?"와 같은 실용적인 부분도 나눌 수 있었습니다. 막히는 것에 대한 질문에 답해주고 함께 고민해주는 팀원들 덕분에, 혼자였다면 한참을 헤맸을 길을 더듬더듬 나아갈 수 있었습니다.
그 덕분에 어찌어찌 과제를 완성할 수 있었습니다. 솔직히 말하면 "깊은 이해"라고 하기에는 아직 부족하지만, 단순히 코드를 작성하는 것을 넘어서 **"왜 이렇게 분리해야 하는지"**에 대해 생각해보게 되었고, 앞으로 비슷한 상황에서 "이렇게 분리하는 게 맞나?"라는 질문을 던질 수 있게 되었다는 점이 가장 큰 성장이었습니다.

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

  • Props Drilling의 구체적 해결: 이론적으로만 알고 있던 Props Drilling 문제를 기본과제에서 컴포넌트를 분리하는 과정에서 실제로 불편함을 겪고, 심화과정에서 Jotai로 해결해보면서 깊이 있게 이해할 수 있었습니다. 특히 Basic에서 App.tsx에서 isAdmin, cart, totalItemCount 등 많은 props를 HeaderCartPage로 전달하는 과정에서 "이게 맞나?" 하는 의문을 가졌고, Advanced에서는 Jotai를 도입하여 각 컴포넌트에서 직접 isAdminAtom, cartAtom 등을 사용할 수 있게 되어 코드가 훨씬 깔끔해진 것을 체감했습니다. 이론으로만 알던 "props drilling이 문제다"라는 말이 실제로 어떤 의미인지 몸소 경험할 수 있었던 점이 좋았습니다.
  • 컴포넌트 독립성과 재사용성: ProductFormCouponForm에서 자체 상태(productForm, couponForm)를 관리하고, Jotai Actions를 직접 호출하여 비즈니스 로직을 중앙화하는 패턴을 학습했습니다. 다만 여전히 일부 props(showProductForm, setShowProductForm 등)는 받고 있어 완전한 독립성은 달성하지 못했습니다.
  • 상태 관리 복잡성의 이해: React 애플리케이션에서 상태 관리가 어떻게 복잡해지는지, 그리고 이를 해결하는 다양한 방법들을 체감할 수 있었습니다.

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

  • 단계적 접근: 이전 과제에서 한 번에 모든 것을 바꾸려다가 테스트가 실패하는 경험을 통해, ai를 활용하던 그렇지 않던 버튼 등 ui 컴포넌트 - models - utils - 엔티티 컴포넌트 - Jotai기반 상태관리(심화) 점진적으로 하나씩 변경하고 각 단계마다 테스트를 통과시키는 방식으로 접근했습니다.
  • 점진적 개선과 분리 기준에 대한 고민: 단순히 코드를 분리하는 것을 넘어서, 각 단계에서 "어떻게 분리할 것인가"를 깊이 고민했습니다. UI 컴포넌트 → models → utils → 엔티티 컴포넌트 → Jotai 기반 상태관리(심화) 순서로 점진적으로 개선하면서, 각 단계마다 "이 로직은 어디에 속해야 할까?", "이 함수는 순수 함수인가 훅인가?", "이 컴포넌트는 UI인가 엔티티인가?"라는 질문을 계속 던졌습니다. 특히 AI의 제안이 '순수 함수'와 '훅' 사이에서 오락가락할 때마다 혼란스러웠지만, 결국에는 각 요소의 책임과 역할을 명확히 정의하는 것이 중요하다는 것을 깨달았습니다.
  • Props의 최소화와 상태 관리의 중앙화: 어떤 상태가 전역적으로 관리되어야 하는지, 어떤 상태는 컴포넌트 내부에 남아있어도 되는지(예: 폼 입력값) 신중하게 판단했습니다. Jotai의 atom을 설계하면서, 이 상태가 애플리케이션의 다른 부분과 어떻게 상호작용할지 고민하며 최대한 props를 넘기지 않고도 컴포넌트가 독립적으로 작동할 수 있도록 구조를 잡고자 많이 노력했습니다.

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

  • '엔티티' 개념 정복하기: '기능을 가진 대상'이라는 저만의 정의를 넘어, '엔티티'가 무엇인지 명확하게 설명할 수 있을 때까지 학습하고, 다음 프로젝트에서는 자신감을 가지고 엔티티 기반으로 컴포넌트와 로직을 설계하고 싶습니다.
  • Jotai 제대로 배워서 적용해보기: 기한에 급급해 심화 과제에서 AI에 의존해 겨우겨우 에러를 해결했던 경험을 반복하고 싶지 않습니다. Jotai가 어떻게 상태를 관리하는지 공식 문서를 보며 직접 코드를 짜보고, 그 원리를 이해하여 능숙하게 사용하고 싶습니다.

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

  • Jotai atom 설계 : advanced/store/atoms.ts에서 atom들을 작성했는데, 혹시 더 효율적으로 atom을 구성하거나 조합할 수 있는 방법이 있을까요? 예를 들어, 액션 atom들을 하나의 파일로 관리하는 것이 좋은 패턴인지, 아니면 관련된 atom과 함께 두는 것이 나을지 궁금합니다.

adds9810 added 30 commits August 4, 2025 20:22
- Header, Toast 컴포넌트 분리 (View 계층)
- 상수 데이터 분리 (constants/index.ts)
- App.tsx에 분리된 컴포넌트 적용
- origin 파일의 정확한 SVG path 사용
- 7개 아이콘 컴포넌트 생성 (CloseIcon, CartIcon, TrashIcon, PlusIcon, ImageIcon, CartHeaderIcon, EmptyCartIcon)
- 재사용 가능한 구조로 분리
- VAC 패턴의 View 계층 구현
- CartIcon 컴포넌트 import 및 사용
- App.tsx: 인라인 SVG를 아이콘 컴포넌트로 교체
- Toast.tsx: 1개 인라인 SVG를 CloseIcon으로 교체
- 모든 SVG path와 className 정확히 유지
- Toast, Header 컴포넌트로 분리 및 App에 연결
- Header, Toast 컴포넌트 인라인화
- Button 컴포넌트 생성
- 모든 버튼을 Button 컴포넌트로 교체
- 아이콘 컴포넌트 분리 및 재사용성 향상
- 상수 데이터 constants 폴더로 분리
- 타입 정의 중앙화
- ESLint 에러 수정
- 재사용 가능한 Input 컴포넌트 생성 (공통 스타일 + label 지원)
- App.tsx의 모든 input을 Input 컴포넌트로 교체
- Card 컴포넌트 생성 (padding: none/sm/md/lg, header 옵션)
- App.tsx의 모든 카드 스타일을 Card 컴포넌트로 통일
- 관리자 상품/쿠폰 관리, 상품 카드, 장바구니, 쿠폰 할인, 결제 정보 카드 적용
- Card 컴포넌트에 headerStyle과 contentPadding props 추가
- 관리자 카드: border 스타일 헤더 + 내용 패딩 없음
- 쇼핑몰 카드: margin 스타일 헤더 + 전체 패딩 적용
- Select 관련 소스 분석 후 공통 스타일만 컴포넌트화
- focusStyle prop: 'indigo' (기본), 'blue' - 포커스 색상 제어
- className으로 개별적인 스타일 (shadow-sm 등) 전달
- 할인 타입 선택과 쿠폰 선택 드롭다운에 Select 컴포넌트 적용
- Button 컴포넌트를 옵션 기반 시스템으로 개선
  - hasTransition, hasFontMedium, hasTextSm, hasRounded 옵션 추가

- App.tsx의 모든 button 태그를 Button 컴포넌트로 교체
  - 토스트 닫기, 쿠폰 관리, 장바구니, 결제 등  버튼 리팩토링
  - 각 버튼의 용도에 맞는 옵션 조합 적용
- 관련함수 cart.ts 모듈로 이동
- App.tsx에서 카트 관련 함수들을 import로 변경
- 사용하지 않는 Product import 제거
- TODO 주석 정리 및 구조화
- addItemToCart, removeItemFromCart, updateCartItemQuantity 순수 함수 추가
- App.tsx에서 카트 상태 변경 로직을 순수 함수로 교체
- 모든 함수가 새로운 장바구니 배열을 반환하도록 구현
- cart.ts: 순수 함수 4개 모듈화 (getMaxApplicableDiscount, calculateItemTotal, calculateCartTotal, getRemainingStock)
- coupon.ts: 순수 함수 7개 모듈화 (isCouponApplicable, validateCouponCode, formatCouponDisplay, validateCouponDiscountValue, getCouponDiscountLabel, getCouponDiscountPlaceholder, calculateCouponDiscount)
- App.tsx: 모둘화한 함수들 적용
- 린트설정 추가 : 'react-hooks/exhaustive-deps': 'warn', useEffect나 useCallback 사용하실 때 필요한 dependencies 추적해서 알려주는 규칙
- discount.ts에 할인 관련 순수 함수 구현
- cart.ts에 calculateOriginalPrice 함수 추가 및 연결
- App.tsx에서 인라인 할인 로직을 모듈 함수로 교체
- product.ts에 순수 함수 구현
- App.tsx에서 불필요한 TODO 주석 제거 (이미 분리된 함수들)
- product.isRecommended 직접 접근을 함수 호출로 변경
- useCart Hook 생성 및 구현
  - 장바구니 상태 관리 (cart, selectedCoupon, totalItemCount)
  - localStorage 연동
  - 원본 로직과 동일한 비즈니스 로직 구현

- App.tsx 리팩토링
  - 장바구니 관련 로직을 Hook으로 이동
  - 알림 콜백 함수들 생성 (handleAddToCart, handleUpdateQuantity 등)
  - 함수 호출 부분 수정

- models/cart.ts 정리
  - 요구사항에 맞는 순수 함수들만 유지
  - calculateItemTotal, getMaxApplicableDiscount, getRemainingStock
- useCoupons Hook에서 중복된 applyCoupon 함수 제거
- App.tsx의 handleCouponSubmit 매개변수 불일치 수정
- useCart Hook의 applyCoupon 로직에서 calculateCartTotal 의존성 제거
- 사용하지 않는 import 정리 (isCouponApplicable)
- 요구사항에 맞게 applyCoupon을 useCart Hook에만 존재하도록 수정
- useLocalStorage 훅 제거하고 원본 패턴으로 복귀
- App.tsx에서 중앙 집중식 localStorage 관리
- addNotification 타입 순서 통일
- React Hook 의존성 배열 수정
- 대량 구매 할인 로직 수정
- useProducts 훅 추가로 상품 관리 분리
✨ 주요 변경사항:
- useCart의 applyCoupon 함수 개선 (calculateCartTotal 재사용)
- useCoupons의 removeCoupon 함수 개선 (단일 책임 원칙 적용)
- 타입 정의 개선 (NotificationCallback, ProductForm, CouponForm)
- 유틸리티 함수 분리 (formatters, validators, hooks)
- 대량 구매 할인 로직 원본과 동일하게 복원

🐛 수정사항:
- 중복 코드 제거
- 의존성 최소화
- 모든 테스트 통과 (21/21)
- utils/formatters.ts: formatPrice 함수 분리
- utils/hooks/useDebounce.ts: 검색 디바운싱 훅 분리
- utils/validators.ts: 검증 함수들 분리 (isValidPrice, isValidStock)
- App.tsx: 분리된 유틸리티 함수들 연결 및 적용
- useLocalStorage 훅 완전 구현 (localStorage 동기화, 에러 처리)
- useProducts, useCoupons 훅에 useLocalStorage 적용
- App.tsx에서 중복 localStorage useEffect 제거
- useCart 훅에서 cart, selectedCoupon 모두 useLocalStorage 적용
- App.tsx에서 중복 localStorage useEffect 제거
- selectedCoupon 상태도 localStorage에 자동 저장 (원본 개선)
- Notification, ProductCard, Header 컴포넌트 분리
- Cart 컴포넌트: 장바구니 표시, 수량 조절, 쿠폰 적용, 결제 정보 (205줄)
- ProductList 컴포넌트: 상품 목록 표시 및 필터링 (60줄)
- AdminPage를 독립적인 페이지 컴포넌트로 분리 (488줄)
- App.tsx 코드량 97% 감소 (1,054줄 → 34줄)
- 컴포넌트 계층 구조 개선:
  * pages/AdminPage.tsx, pages/CartPage.tsx
  * components/common/ (Header, Notification)
  * components/shopping/ (Cart, ProductList, ProductCard)
- 관리자 전용 상태 및 핸들러를 AdminPage로 이동
- AdminPage: 라우팅 + 탭 관리로 역할 단순화
- 엔티티 컴포넌트: ProductManagement, CouponManagement
- UI 컴포넌트: AdminTabs
- 폼 컴포넌트: ProductForm, CouponForm
- localStorage: selectedCoupon 상태 유지
adds9810 added 24 commits August 7, 2025 17:54
- CartItem 컴포넌트 분리로 개별 아이템 로직 추출
- Admin 컴포넌트 import 경로 및 타입 정리
- shopping → cart/ + product/ 폴더 분리
- Cart 컴포넌트 5개로 분리
- 기능별 컴포넌트 구조화
- 주석 정리 및 ESLint 경고 해결
- 페이지 컴포넌트 폴더 구조 변경
- basic → advanced 코드 복사
- Jotai 설치 (전역 상태 관리)
- store/atoms.ts: Jotai atoms 구조 설계 (상태, 파생 상태, 필터링)
- store/actions.ts: Jotai action atoms 구현 (장바구니, 상품, 쿠폰 관리)
- hooks/useCart.ts: Jotai 기반으로 변경, 기존 인터페이스 유지
- hooks/useProducts.ts: Jotai 기반으로 변경, 기존 인터페이스 유지
- hooks/useCoupons.ts: Jotai 기반으로 변경, 기존 인터페이스 유지
- App.tsx: useCart 호출 방식 수정 (products 파라미터 제거)
- 3-1단계: Notification 컴포넌트 Jotai화
  - notifications, onRemoveNotification props 제거
  - useAtom(notificationsAtom), useAtom(removeNotificationAtom) 직접 사용
  - App.tsx에서 notifications 관련 코드 제거

- 3-2단계: CartItem 컴포넌트 Jotai화
  - cart, handleUpdateQuantity, removeFromCart props 제거
  - useAtom(cartAtom), useAtom(updateQuantityAtom) 직접 사용
  - CartItems, Cart, CartPage 컴포넌트 props 단순화
- ProductList: debouncedSearchTerm props 제거, Jotai 직접 사용
- Header: props 6개 → 0개, Jotai 적용
- searchTermAtom, isAdminAtom을 atomWithStorage로 변경
- Cart 컴포넌트에서 상태/액션 atoms 직접 사용
- CartPage props 8개 → 1개로 감소
- completeOrderAtom 내부 알림 처리 개선
- Props Drilling 해결 중
- GitHub Pages 배포를 위한 오류 수정 및 세틍
- App.tsx에서 isAdmin useState를 isAdminAtom으로 변경
- Header, CartPage, ProductList에서 isAdmin props 제거
- ProductForm에서 addNotification props 제거하고 addNotificationAtom 직접 사용
- main 브랜치 push 시 자동 배포 워크플로우 추가
- 배포 파일 구조 정리 (dist 폴더 정리)
- 환경 보호 규칙 제거로 main 브랜치 배포 가능
- 불필요한 설정 제거 및 코드 정리
- 배포 워크플로우 최적화
- vite.config.ts 빌드 설정 개선
- CouponManagement 알림 처리 로직 정리
- 패키지 및 워크스페이스 설정 업데이트
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.

안녕하세요. 과제 잘 정리해주셨습니다 — 큰 컴포넌트를 도메인(entities/models) + UI + store(전역 상태)로 깔끔히 분해한 것이 눈에 잘 보입니다. 아래 피드백은 PR 파일들을 바탕으로 작성한 코드 품질 · 구조 관점의 리뷰입니다. 특히 요청하신 흐름(요구사항 변화 시나리오 → 변화 파급 분석 → 응집도·결합도 평가 및 개선 제안)을 따랐습니다.

요약

  • 전반적으로 잘 분리되어 있습니다: App.tsx가 얇아지고 Header/Notification/AdminPage/CartPage 등으로 쪼개져 가독성·테스트성 향상
  • Jotai 기반 store + action atoms 패턴이 도입되어 도메인 로직(모델)과 UI가 잘 나뉘어 있음
  • 남아있는 개선 포인트: 상태 라이브러리 교체나 패키지화(모듈화) 시 최소 수정으로 옮기기 위한 어댑터/공통 인터페이스 준비, 일부 유틸/formatter의 시그니처·의존성 정리

먼저 PR에서 본 핵심 변경/구성요소

  • src/advanced/App.tsx: 대규모 컴포넌트 → atoms + 여러 컴포넌트로 분리 (Header, NotificationComponent, AdminPage, CartPage)
  • components/*: admin, cart, product, common, ui 등 세분화
  • models/*, store/atoms & store/actions (참조로 보임): 도메인 로직(계산 함수)이 모델로 이동된 흔적
  • ui 원자 컴포넌트(Button/Input/Select 등) 도입
  • Jotai 사용 (package.json / pnpm-lock 반영)

질문(advanced/store/atoms.ts 관련) — 짧히 먼저 답변

  • atom 설계: 도메인별로 분리(예: cartAtoms.ts, productAtoms.ts, couponAtoms.ts, uiAtoms.ts)하고 index에서 재수출(re-export)하세요. 액션 atom들은 '동일 도메인'에 두는 것이 유지·이해하기 좋습니다(예: cartAtoms.ts 옆에 cartActions.ts). 이유: 관련된 상태·액션을 한 눈에 볼 수 있어 응집도가 높아지고, 패키지화 시 별도로 떼어내기 수월합니다.
  • 액션 atom을 한 파일에 몰아두는 것도 가능하지만 (중앙화 장점) 도메인 경계가 흐려질 수 있어 추천하지 않습니다. (도메인이 작고 액션이 매우 적다면 묶어도 괜찮음)

아래는 요청하신 흐름대로 정리한 종합 피드백 → 상세 피드백입니다.

────────────────────────────────
질문에 대한 답변 (PullRequestBody 내 문의)

  • “atom들을 하나의 파일로 관리할지, 관련된 atom과 함께 두는지?”
    권장: 도메인 단위로 파일을 쪼개고, 도메인 내부에서 상태(atom)/액션(action atom)/도메인 훅(useCart 등)을 같이 위치시키세요.
    구조 예:
    • store/
      • cart/
        • atoms.ts (cartAtom, totalAtom 등)
        • actions.ts (addToCartAtom, updateQuantityAtom 등)
        • index.ts (re-export, useCart hook 구현)
      • products/
        • atoms.ts
        • actions.ts
        • index.ts
      • ui/
        • atoms.ts (searchTermAtom, isAdminAtom 등)
      • index.ts (도메인들을 export)
        장점: 변경 시 파일 이동 범위가 좁아지고(응집도↑), 도메인 단위로 패키지화하기 쉬움.

질문 부가: 액션/utility 분리

  • “액션 atom들을 하나의 파일로 관리/분리하는 패턴”
    • 작은 프로젝트: 도메인별로 같이 두는게 직관적
    • 중대형/여러 패키지로 쪼갤 계획: actions는 도메인 패키지 내부에서 관리하고 패키지 외부에선 도메인 훅(useCart)만 노출 — 내부 구현(Atom/Redux) 숨김

────────────────────────────────
종합 피드백

  1. PullRequestFiles 분석을 통한 핵심 키워드
  • 긍정적: 분리된 컴포넌트(재사용성↑), 모델/유틸로의 로직 이동(순수 함수 활용), Jotai 도입(심화)
  • 주의 필요: 상태 라이브러리 종속성(직접 useAtom 사용 위치), 일부 유틸 시그니처에 의존성(포맷터가 products/cart를 요구), 패키지화할 때의 경계(어떤 코드가 domain인지 UI인지) 불명확성 가능
  1. PullRequestBody의 self-reflection(“과제 셀프회고” / “제일 신경 쓴 부분”)에 대한 인사이트
  • 매우 좋은 접근: “왜 분리해야 하는지”를 고민한 점이 인상적입니다. 단순한 리팩터링(코드 분해)에서 더 나아가 설계 판단 기준(엔티티 vs UI, 훅 vs 순수함수)을 고민하신 점이 성장 포인트입니다.
  • 이어서 생각해볼 질문:
    • “이 도메인에서의 단위 변경(예: 할인 로직 변경)이 생기면 수정을 어디에서 얼마나 해야 하는가?” — 실제로 수정 경로(파일/함수)를 그려보세요.
    • “이 컴포넌트나 훅이 어떤 테스트(단위/통합)를 가져야 하는가?” — 분산된 책임에 맞춘 테스트 전략(모델: 유닛테스트, domain hook: 통합 테스트, UI: 스냅샷/유저 시나리오)
    • “패키지화한다면 public API(호출자에게 노출되는 함수/훅)는 무엇이어야 하는가?” — 최소한의 surface 만들기
  1. PullRequestBody의 “리뷰 받고 싶은 내용”에 대한 답변 (Jotai atom 설계)
  • 위에서 요약한 권장 방식(도메인별 모듈화) 추천
  • 액션 atom들은 도메인별로 둬라(예: cartActions.ts 같이 보관). 이유: 관련 상태+행위가 같은 곳에 있어 응집성이 높고, 외부에서는 도메인 훅(useCart)을 통해 접근하면 내부 구현 바꿔도 영향 적음.
  • 추가 팁:
    • atom 이름은 역할을 명확히 (cartAtom, cartTotalAtom, selectedCouponAtom)
    • 액션은 payload 객체로(옵션: onSuccess/onError 콜백) 받아서 호출자에게 유연성 제공
    • atom 파일은 너무 커지지 않도록 분리 — atoms.ts, actions.ts, selectors.ts(계산용 파생 state)

────────────────────────────────
상세 피드백 (개념정의 → 문제 → AS-IS → TO-BE)

먼저 개념 정의(요청하신 항목)

  • 응집도(cohesion) 정의 (요청하신 규칙 적용):
    • 변경에 대한 파일 및 코드의 추가/수정/삭제 경로가 얼마나 짧은가(짧을수록 응집도 높음).
    • 라이브러리(패키지)로 떼어낼 때 관련된 코드만 묶어서 분리할 수 있는가(떼어내기 쉬울수록 응집도 높음).
  • 결합도(coupling) 정의:
    • 모듈(함수/컴포넌트/훅)이 외부와 얼마나 강하게 연결되어 있는가. 인터페이스(명확한 인자, 콜백, 훅등)를 통해 상호작용하면 결합도가 낮음.
    • 예: addNotification 과 같은 행위를 컴포넌트가 직접 의존하면 결합도가 높음; onSuccess/onError 콜백으로 전달하면 낮음.

문제 1: 상태관리 라이브러리가 달라지는 경우 (예: Jotai → Redux / Zustand / TanStack State)

  • 문제 정의:
    • 현재 컴포넌트들이 useAtom, action atoms 를 직접 사용. 라이브러리 교체 시 코드 전체에 걸쳐 useAtom → useSelector/useDispatch 또는 useStore 변경 필요.
    • 즉 결합도가 라이브러리에 대한 구체적 호출에 대해 높음(전역 상태 접근 방식이 컴포넌트마다 흩어져 있음).
  • AS-IS (예시)
    • ProductCard에서 addToCart 사용 (Jotai)
      • AS-IS (간략):
        const [, addToCart] = useAtom(addToCartAtom);
        addToCart({ product, onNotification: (...) => ... });
  • 문제 발생 시:
    • 모든 컴포넌트에서 useAtom 호출을 useDispatch/useStore 등으로 바꿔야 함 → 광범위한 리팩터링
  • 개선 방향 (TO-BE): 도메인 훅 추상화 계층을 만들기 (Adapter pattern)
    • 목표: 컴포넌트는 전역 스토리지 구현에 의존하지 않고 도메인 훅(인터페이스)에 의존하게 만든다.
    • TO-BE 예시 인터페이스(간단한 코드)
      // useCart.ts (도메인 훅의 공개 인터페이스)
      export function useCart() {
      // 구현체는 Jotai든 Redux든 여기에서 감춘다.
      return {
      cart, // CartItem[]
      totals, // 계산된 합계
      addToCart: ({ product, onNotification }) => { ... },
      updateQuantity: ({ productId, newQuantity, onNotification }) => { ... },
      removeFromCart: (productId) => { ... },
      applyCoupon: ({ coupon, onNotification }) => { ... },
      };
      }
    • Jotai 구현(예):
      function useCart() {
      const [cart] = useAtom(cartAtom);
      const [, addToCartAtomFn] = useAtom(addToCartAtom);
      return {
      cart,
      addToCart: (payload) => addToCartAtomFn(payload),
      ...
      };
      }
    • Redux 구현(예):
      function useCart() {
      const cart = useSelector(...);
      const dispatch = useDispatch();
      return {
      cart,
      addToCart: (payload) => dispatch(cartSlice.actions.addToCart(payload)),
      ...
      };
      }
    • 이 어댑터를 만들면 컴포넌트는 그대로 두고 useCart의 내부 구현만 바꿔서 라이브러리를 교체할 수 있음.
  • 왜 좋은가:
    • 응집도: UI 코드와 상태 구현이 격리 → 변경 동선이 짧음
    • 결합도: 컴포넌트는 useCart 인터페이스에만 의존(구현 교체 가능)

문제 2: 모듈화(패키지화) 필요 시(응집도/매끄러운 떼어내기)

  • 문제 정의:
    • 패키지화(예: domain/cart, domain/product, ui/core) 시 코드가 명확히 도메인 경계에 따라 분리되어 있지 않으면 떼어내기 어려움.
    • 예: 일부 유틸/formatter가 여러 곳에서 products+cart 모두 받아 복잡한 시그니처를 갖는다면 domain으로 떼어내기엔 혼선 발생.
  • AS-IS 예시: formatPrice/formatters (현재 repository에선 formatPrice가 여러 곳에서 호출되고 signature가 많음)
    • AS-IS (추상화된 예):
      function formatPrice(price:number, productId?: string, isAdmin?: boolean, products?: Product[], cart?: CartItem[]) { ... }
    • 문제점: formatPrice가 products, cart 등 전체 컨텍스트를 요구 → 포팅/패키지화시 외부 의존성 증가
  • TO-BE 제안:
    • 모델/유틸은 명확한 입력과 순수함수로 만들기
    • formatPrice를 단일 책임(가격 포맷팅)으로 좁히고, 재고나 뱃지 같은 로직은 별도의 순수 함수(예: getRemainingStock)로 뽑기
    • 예:
      // AS-IS:
      formatPrice(price, productId, isAdmin, products, cart)
      // TO-BE:
      // - formatCurrency(price, { locale, isAdmin }) // 단순 포맷
      // - getRemainingStock(product, cart) // 재고 계산
      // 호출부의 역할: 필요한 데이터를 불러(도메인 훅) 합성해서 UI를 렌더
  • 패키지화 시 권장 구조
    • packages/
      • domain-cart/
        • src/index.ts (export useCart, models, types)
      • domain-product/
        • models, hooks
      • ui-core/
        • Button, Input, Card, Badge 등
    • 각 패키지는 자체 테스트와 public API(도메인 훅, 모델 함수)만 노출

응집도/결합도 현재 상태 평가 (PR 기준)

  • 응집도: 전반적으로 괜찮음
    • 장점: models(예: ../../models/cart 등)을 사용하여 계산 로직을 모아서 유지·테스트 가능(응집도 ↑)
    • 개선점: 일부 유틸/formatters가 많은 context를 요구해서 응집도를 낮출 소지 있음(formatPrice 예)
  • 결합도: 보통에서 좋은 편
    • 장점: action atoms가 옵션 콜백(onNotification)을 통해 콜백으로 처리하는 패턴은 결합도를 낮추는 좋은 사례
    • 개선점:
      • 컴포넌트들이 직접 useAtom를 통해 여러 atom에 접근(예: productsAtom, cartAtom, addNotificationAtom) → 컴포넌트가 여러 구현 세부사항을 알고 있음. 도메인 훅으로 감싸면 더 낮아짐.

구체적 코드 개선 샘플(AS-IS vs TO-BE)

  1. 컴포넌트가 직접 useAtom를 많이 쓰는 경우 → 도메인 훅으로 추상화
  • AS-IS (ProductCard 일부):
    const [cart] = useAtom(cartAtom);
    const [, addToCart] = useAtom(addToCartAtom);
    // 직접 addToCart 호출

  • TO-BE:
    // useCart.ts
    export function useCart() {
    const [cart] = useAtom(cartAtom);
    const [, addToCartAtomFn] = useAtom(addToCartAtom);
    return {
    cart,
    addToCart: ({ product, onNotification }) => addToCartAtomFn({ product, onNotification }),
    ...
    };
    }
    // ProductCard:
    const { cart, addToCart } = useCart();
    addToCart({ product, onNotification: (msg, type) => addNotification({ message: msg, type })});

    장점: ProductCard는 Jotai에 직접 의존하지 않음. 라이브러리 교체 시 useCart만 교체하면 됨.

  1. formatPrice 리팩토링 (간단 예)
  • AS-IS (간략화된 현재 형태):
    function formatPrice(price, productId, isAdmin, products, cart) { /* 내부에서 remaining stock 조회, isAdmin 포맷 등 혼재 */ }

  • TO-BE (분리):
    // formatters.ts
    export function formatCurrency(price: number, isAdmin = false) {
    return isAdmin ? ${price.toLocaleString()}원 : ₩${price.toLocaleString()};
    }

    // cart model
    export function getRemainingStock(product: Product, cart: CartItem[]) {
    const cartItem = cart.find(i => i.product.id === product.id);
    return product.stock - (cartItem?.quantity || 0);
    }

    // 호출부 (ProductCard)
    const remaining = getRemainingStock(product, cart);

    {formatCurrency(product.price, isAdmin)}

    // 재고/라벨 처리도 호출부에서 담당

    장점: 각 함수 단일 책임, 단위 테스트 작성 쉬움, 패키지화 시 의존 경계 명확

  1. 액션 인터페이스 통일 (onSuccess/onError 패턴)
  • 현재 좋은 사례: addToCartAtom 등은 onNotification 콜백을 옵션으로 받음.

  • 권장: 모든 앱 액션은 옵션 객체로 onSuccess/onError onNotification 같은 콜백을 표준화
    // 예:
    addToCart({ product, onSuccess?: ()=>void, onError?: (err)=>void })

    이유: 호출자가 결과 처리 책임을 명확히 가질 수 있어 결합도 낮아짐.

파일별/영역별 세부 코멘트(주요 포인트)

  • src/advanced/App.tsx
    • 좋은 점: App이 매우 얇아졌고, Header/Notification/AdminPage/CartPage로 역할이 분리됨.
    • 제안: App은 가능한 한 훅(useIsAdmin 등)을 통해 상태를 구독하도록 하고 도메인 훅을 만들어 라이브러리 변경 영향범위를 축소하세요.
  • components/common/Header.tsx / Notification.tsx
    • Header에서 검색 입력을 searchTermAtom으로 분리한 점이 좋습니다.
    • NotificationComponent가 removeNotificationAtom을 사용해 제거하는 구조는 명확하지만, 만약 Notification이 UI 패키지로 분리될 경우, removeNotification은 도메인 액션(또는 이벤트 버스) 형태로 노출되어야 함.
  • components/* (admin/cart/product)
    • 전반적으로 책임 잘 분리됨(예: ProductManagement, CouponManagement, ProductForm 등)
    • ProductForm/ProductManagement: 컴포넌트가 addNotificationAtom/addProductAtom 등 여러 액션을 바로 사용 → useProduct 도메인 훅으로 묶어 추상화 가능
  • models/*
    • PR에서 models 호출이 보입니다(예: models/cart, models/discount). 모델이 순수함수로 잘 되어 있다면 매우 좋음 — 테스트 커버 확보 권장.
  • store/atoms & actions (간접 관찰)
    • Atom + action-atom 패턴(특히 action이 payload 객체를 받음)은 모듈성/유연성에 장점이 있음.
    • 다만 action atoms가 도메인 로직(예: localStorage 직접 접근, side effect)을 포함하고 있다면, 이를 명확히 분리(서버/비동기 처리 레이어)하세요.

마이그레이션 시 고려사항(실무적 체크리스트)

  • public API(도메인 훅) 정의: useCart, useProducts, useNotifications 등
  • persistence(로컬스토리지) 책임: store 내부로 모으고, 패키지 경계에 의존하지 않도록 추상화
  • CSS/스타일 분리: ui-core 패키지를 만들 경우 Tailwind 의존성/설정(빌드 위치, PostCSS 설정)을 함께 고려
  • 테스트: 모델 함수(순수 함수) 단위 테스트 우선 확보 → 도메인 훅의 통합 테스트 → UI 시나리오 E2E

권장 작업 우선순위(실무적)

  1. 도메인 훅(useCart, useProducts, useCoupons, useNotification) 생성 — 컴포넌트가 이 훅들만 사용하도록 리팩토링
  2. 기존 action atoms/atoms를 도메인 폴더로 재배치(예: store/cart/*)
  3. formatters/유틸 시그니처 단일 책임으로 리팩토링
  4. 패키지화 계획 시 public API 문서화(README + 타입 선언)
  5. 상태 라이브러리 교체 필요 시 도메인 훅만 재작성 (컴포넌트 변경 최소화)

간단한 코드 예시: useCart adapter (AS-IS vs TO-BE)

  • AS-IS (컴포넌트에서 직접 Jotai 사용)
    // ProductCard.tsx (AS-IS)
    const [cart] = useAtom(cartAtom);
    const [, addToCart] = useAtom(addToCartAtom);
    addToCart({ product, onNotification: (m,t) => addNotification({ message:m, type:t }) });

  • TO-BE (useCart 훅으로 추상화)
    // useCart.ts (도메인 훅)
    import { useAtom } from 'jotai';
    import { cartAtom, addToCartAtom, updateQuantityAtom } from './cart/atoms';
    export function useCart() {
    const [cart] = useAtom(cartAtom);
    const [, addToCartFn] = useAtom(addToCartAtom);
    const [, updateQuantityFn] = useAtom(updateQuantityAtom);
    return {
    cart,
    addToCart: (payload) => addToCartFn(payload),
    updateQuantity: (payload) => updateQuantityFn(payload),
    };
    }

    // ProductCard.tsx (TO-BE)
    const { cart, addToCart } = useCart();
    addToCart({ product, onSuccess: () => showToast('담겼습니다') });

이 방식으로 구현하면 Jotai → Redux 등의 전환 시 useCart 내부만 바꿔주면 됩니다.

결론(코드 품질 관점)

  • 현재 PR 상태는 좋은 분리 기준을 따르고 있으며, 모델(순수 함수)과 UI(컴포넌트) 분리가 잘 되어 테스트성과 유지보수성이 향상되었습니다.
  • 향후 상태 라이브러리 교체나 패키지화(모듈화)를 계획한다면 도메인 훅을 통해 인터페이스를 추상화하고, 모델/유틸의 시그니처를 단일 책임으로 좁히는 리팩터링을 권장합니다.
  • atom/액션의 파일 배치는 도메인별로 구성하는 것이 장기 유지보수·패키지화에 유리합니다.

필요하시면

  • 현재 코드(폴더) 기반으로 “useCart/useProducts” 같은 도메인 훅의 구체적 구현(파일별) 스켈레톤을 작성해드릴게요.
  • 또는 Jotai → Redux(또는 Zustand)로 바꿀 때 필요한 변경 파일 목록(자동 스크립트용 엑셀/JSON)을 뽑아 드릴 수도 있습니다.

감사합니다. 추가로 보고 싶은 부분(예: store/atoms.ts 전체, models/cart.ts 원본 등)을 올려주시면 그 파일들을 보고 더 구체적인 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.

2 participants