Skip to content

[5팀 허정석] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#28

Open
heojungseok wants to merge 71 commits into
hanghae-plus:mainfrom
heojungseok:main
Open

[5팀 허정석] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍#28
heojungseok wants to merge 71 commits into
hanghae-plus:mainfrom
heojungseok:main

Conversation

@heojungseok
Copy link
Copy Markdown

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

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

과제 셀프회고

과제 배포

이번 5주차 과제는 지난 회고 에서 언급했던 Try 부분과 지난 주차에서도 사용했던 history 를 남기는 방식을 함께 진행해봤습니다.

image

과제 진행 요약

  • 전체 방향
    • SRP 기반 계층 분리: 계산/검증은 utils/*, 도메인 로직은 services/*, 상태·흐름은 hooks/*, 렌더링은 components/*
    • 점진 리팩터링과 테스트/트러블슈팅 병행

타임라인

  • 08/04 기본 리팩터링

    • 계산 분리: utils/calculators.ts(아이템 합계, 최대 할인, 카트 합계)
    • 검색 분리: utils/filters.ts
    • 타입 정리: types.ts(뷰/엔티티 타입 구분)
    • 함수 합성, 상수화, 순수함수 원칙 정립
    • 테스트 에러 해결 및 통과
  • 08/05 훅/서비스 분리

    • 공용 훅: useLocalStorage, useNotifications, useDebounce
    • 도메인 훅+서비스: useCoupon+couponService, useProducts+productService, useCart+cartService/validators
    • 의존성 주입, 단일 책임 강화
    • UI 상태 훅: useUIState, useCouponForm(소유권 충돌 해결)
    • 버그 해결: 편집 핸들러, 쿠폰 합계 반영
  • 08/06 컴포넌트 분리(Middle-Out)

    • CouponManagementCouponForm/CouponCard/AddCouponButton
    • ProductManagementProductForm/ProductTable/AddProductButton
    • 디렉터리 도메인화: components/pages/admin/*, components/pages/shopping/*
    • App.tsx 간소화 계획 수립
  • 08/08 고급 단계(Jotai)

    • 전역 상태 일원화: atoms/*(cart/product/coupon/ui/notification)
    • 파생 atom 도입(예: cartTotalsAtom), 액션 atom 적용
    • Props Drilling 대폭 감소, 포맷/검색 관련 버그 수정

AI시대 개발자가 인간 클립보드 되지 않는 법

내가 설정한 커서 룰

description: 'AI 강사 역할 및 프로그래밍 가이드라인 - React, TypeScript, 클린코드 교육을 위한 7가지 원칙'
globs: ['src//*.tsx', 'src//*.ts']
alwaysApply: true

AI Instructor Role and Programming Guidelines

Role

  • You are an instructor who systematically teaches React, TypeScript, and Clean Code to students.
  • Prioritize student learning and growth, providing clear and easy-to-understand explanations step by step.

7 Principles of AI Programming

  1. Verification and Understanding: Don't blindly trust AI results. Always verify them. Ask yourself: "Why does this solution work? What are its limitations?"

  2. Maintain Fundamentals: Don't lose basic coding skills. Set aside one "No-AI Day" per week for manual coding mode.

  3. Self-Directed Learning: Try on your own before depending on AI. Spend 15-30 minutes researching and planning solutions before seeking AI help.

  4. Quality Management: Review AI code like human code. Foster a culture where "AI creates drafts, but ownership belongs to us."

  5. Active Learning: Don't just receive answers, understand them. Explain complex concepts in simple terms or ask "Why don't other approaches work?"

  6. Knowledge Management: Record and improve areas where AI helped. Follow the principle that "AI should be the last resort."

  7. Collaborative Relationship: Treat AI as a partner while maintaining control. Use AI as a pair programming partner, not just a query tool.

Educational Principles

React Education

  • Teach component-based thinking systematically
  • Clearly explain the importance and patterns of state management
  • Focus on performance optimization and best practices in real-world scenarios

TypeScript Education

  • Explain the importance of type safety with concrete examples
  • Teach gradual type application methods step by step
  • Present methods for improving development productivity using the type system

Clean Code Education

  • Explain the Single Responsibility Principle (SRP) with practical examples
  • Emphasize functional programming and the importance of pure functions
  • Systematically teach the necessity and methods of refactoring

Conversation/Explanation/Feedback/Code Review Standards

  • Emphasize step-by-step approach, practical examples, visual explanations, and interaction
  • Provide specific and constructive feedback with positive reinforcement and growth motivation
  • Focus code reviews on readability, maintainability, type safety, performance, and testability
  • Delivers information following the principles of functional programming

My Coding Style

My coding style favors simplicity and clarity.
I prefer not to over-abstract functionality—keeping abstractions meaningful but not excessively granular.
I also prefer to modularize code that is reused across multiple parts of the application.

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

파일, 함수, 컴포넌트가 딱 한 가지 역할만 하도록 나누자!!!

UI(컴포넌트) ↔ 상태/이벤트(훅) ↔ 도메인 로직(서비스/유틸)의 경계를 명확히 하고, 전역 상태는 Jotai로 일원화해 Props Drilling을 제거

  • 순수함수/유틸 분리: 계산, 검증, 포맷을 utils/* 하위로 생성하여 테스트와 재사용성에 용이하게 만들었습니다.
  • 도메인 서비스 추출: productService, cartService, couponService 로 비즈니스 로직을 컴포넌트 밖으로 이동 시켰습니다.
  • Custom Hook 설계: useProduct, useCart 등으로 상태.절차 캡슐화를 하고 의존성 주입으로 결합도를 축소했습니다.
  • Middle-Out 컴포넌트 분리: CouponManagement, ProductManagement 처럼 복잡한 영역부터 점진적으로 분리했습니다.
  • Jotai 전환: atmos/* 와 파생 atom 으로 전역 상태 단일 소스화를 하여 Props Drilling 을 제거하고 Hook 독립성을 강화시켰습니다.
  • 테스트/트러블슈팅 중심: 실패 케이스의 원인 분석을 하여 요구사항에 맞는 프로젝트를 구현했습니다.
SRP: “한 파일/함수 = 한 역할”
재사용성: “한 번 만들면 여러 곳에서”
레이어: entities(순수함수/서비스) → features(훅) → UI(컴포넌트)

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

다음에는 services를 행동(use-case) 기준으로 재구성하고, 중복되는 도메인 규칙은 정책(policy)으로 상향 분리해 훅/컴포넌트의 책임을 줄여보는 방향으로 해보고 싶습니다.

사용자 행동(담기, 적용, 수정) 같은 use-case 단위로 묶으면 여러 컴포넌트·모듈·서비스가 협력하도록 조정하는 과정이 단순해지고, 테스트 부담도 줄어듭니다. 행동 레이어는 입출력과 흐름만 담당하고 로직은 도메인에 집중하므로, 책임 위치가 명확해져 유지보수가 더 용이해집니다.

그리고 아래는 공통 로직을 상위로 만들었을 때의 예시 구조입니다. (AI 활용)

src/
├─ application/
│  ├─ cart/
│  │  ├─ addItemToCart.ts
│  │  ├─ removeItem.ts
│  │  └─ updateQuantity.ts
│  ├─ product/
│  │  ├─ createProduct.ts
│  │  └─ updateProduct.ts
│  └─ coupon/
│     ├─ applyCoupon.ts
│     └─ removeCoupon.ts
├─ domain/
│  ├─ policies/
│  │  ├─ cart.ts       # canAddItem, calculateTotals
│  │  ├─ discount.ts   # getMaxApplicableDiscount
│  │  └─ validation.ts # validatePrice/Stock/Coupon
│  └─ models/          # 타입/엔티티
└─ shared/
   ├─ money.ts
   ├─ id.ts
   └─ date.ts

예시 기준

  • 2곳 이상 재사용 + 순수함수 + 도메인 언어로 설명 가능 → domain/policies/*

특정 도메인(예: 유저, 주문)에 관련된 로직이지만 여러 군데에서 공유될 수 있는 정책들

  • 완전 범용(도메인 무관) → shared/*

유틸함수 모음

  • 입출력이나 사이드이펙트 포함 → application/*에 유지

실제 외부와 연결되는 행동 코드들

주의점

  • 과도한 일반화 금지
  • 사이드이펙트 경계

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

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

  1. 구조/네이밍
    위 예시 구조의 application/cart/addItemToCart.ts처럼 “도메인/동사+목적어” 네이밍과 디렉터리 구조가 적절한지 또는 다른 컨벤션 예시가 궁금합니다.
    “한 파일 = 한 행동” 원칙에서 복잡한 행동(검증·계산·여러 정책 조합)의 분기 처리는 어디까지 application/* 에 두는 게 좋을지 의견이 궁금합니다.
  2. 정책 상향 기준
    “재사용 ≥ 2, 순수성, 화면 무관 설명 가능”이면 domain/policies/*로 승격한다는 기준이 충분할까요? 더 추가/수정할 항목이 있을까요?

- calculateItemTotal, getMaxApplicableDiscount, calculateCartTotal 함수 분리
- App.tsx에서 import하여 사용하도록 수정
- 순수 함수로 테스트 가능성 향상
- 계산 함수들을 utils/calculators.ts로 이동
- 포맷팅 함수들을 utils/formatters.ts로 이동
- 전역 상태 의존성 제거하고 명시적 매개변수 전달로 변경
- rules 생성
- timeline 작업 내역 생성
- basic 단계별 적용 범위 작성
- 파일 이동
- utils/filters.ts 생성: filterProducts 함수 추가
- types.ts 생성: ProductWithUI, Notification, ProductForm, CouponForm 타입 정의
- App.tsx에서 검색 로직을 순수함수로 분리
- 타입 안정성 확보 및 코드 구조화
- localStorage 관리를 재사용 가능한 Hook으로 분리
- React prevValue 사용으로 순환 참조 문제 해결
- useNotifications Hook으로 알림 시스템 캡슐화 및 재사용성 확보
- useDebounce Hook으로 검색 성능 최적화
- useCoupon Hook으로 쿠폰 상태 관리 로직 분리
- couponService 생성으로 순수 비즈니스 로직 캡슐화
- App.tsx 중복 상태 관리 문제 해결 (selectedCoupon)
- 의존성 주입 패턴 적용으로 결합도 감소
@yuhyeon99
Copy link
Copy Markdown

화이팅!!

- productService로 순수 비즈니스 로직 분리
- useProducts Hook으로 상품 CRUD 로직 캡슐화
- 수정 버튼 핸들러 오류 수정 (updateProduct → startEditProduct)
- utils/validators.ts 생성 (validatePrice, validateStock)
- services/productService.ts에서 검증 로직을 utils로 이동
- useCart Hook 생성 (hooks/useCart.ts)
- cartService 생성 (services/cartService.ts)
- validators에 검증 로직 추가 (utils/validators.ts)
  - 도메인 서비스 패턴 적용 방법
  - cartService와 validators 분리 과정
  - 의존성 주입과 인터페이스 학습 포인트
- useCart Hook 수정
  - selectedCoupon을 인터페이스에 추가하여 의존성 명시
  - useEffect에서 selectedCoupon을 사용하여 totals 계산
  - 의존성 배열에 selectedCoupon 추가하여 쿠폰 변경 시 재계산

- App.tsx 수정
  - useCoupon을 useCart보다 먼저 호출하여 selectedCoupon 선언
  - useCart에 selectedCoupon 전달하여 쿠폰 적용된 totals 계산
  - 중복된 totals 계산 코드 제거
@eveneul
Copy link
Copy Markdown

eveneul commented Aug 5, 2025

🦍

- useUIState Hook 생성: 전역 UI 상태 관리 (isAdmin, activeTab, showCouponForm)
- useCouponForm Hook 생성: 쿠폰 폼 상태 관리
- 모든 Hook에 JSDoc 주석 추가 (9개 Hook 완료)
- 도메인 경계에 따른 폼 상태 분리 구조 검증
- App.tsx에서 UI 상태들을 Hook으로 분리하여 관심사 분리 완성
- useUIState, useCouponForm Hook 분리 과정 문서화
- 모든 Hook 주석 추가 작업 완료 내용 정리
- 폼 상태 분리 구조 검토 및 도메인 경계 분석
- 데이터 소유권 원칙과 응집성 원칙 적용 사례
- Hook 분리 작업의 최종 완성도 정리
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과 상세한 리포트(타임라인/설계/구현 문서들) 잘 정리해주셨네요. 전체적으로 SRP·계층분리·순수함수·훅 추상화를 의도대로 잘 적용하신 게 보입니다. 아래 피드백은 PR의 파일 변경 목록(.cursor, docs, atoms, hooks, package 등)을 바탕으로 작성했습니다. GitHub PR 코멘트로 바로 붙여넣어 사용하실 수 있게 마크다운 형식으로 정리했습니다.

요약 흐름(요청하신 순서대로)

  • 기술 요구사항 변화 시나리오 제시
  • 현재 코드(AS-IS)가 각 시나리오에 대응하면 어떤 변화가 발생할지
  • 이를 바탕으로 현재 코드가 잘 작성되었는지(응집도 · 결합도 관점) 판단과 개선안(TO-BE; 코드 예시 포함)

공통 안내: 여기서 말하는 "응집도"와 "결합도" 정의

  • 응집도(사용자 정의 규칙)
    • 변경 작업(파일/코드 추가·수정·삭제)의 동선이 얼마나 짧은가 (작업 범위의 국소성)
    • 라이브러리(패키지)로 떼어낼 때 매끄럽게 떼어낼 수 있는가 (추출 난이도)
  • 결합도
    • 모듈/함수/컴포넌트가 직접 내부 구현을 참조하거나 특정 용어·행위(addNotification 등)에 결속되어 있는지
    • 인터페이스(onSuccess/onError, DI)를 통해 약하게 결합되어 있는지

간단 판단 기준:

  • 응집도 높음: 관련 코드(도메인 모델, 서비스, 순수함수)가 같은 도메인 폴더에 있고 변경 범위가 작음
  • 결합도 낮음: 외부 동작(알림, 저장소, 네트워크)을 콜백/의존성 주입으로 분리함

종합 피드백 (키워드 + PR Body 회고 코멘트에 대한 피드백 + 질문 답변)

  1. PR 파일 분석에서 도출한 핵심 키워드
  • Jotai 전환 & atoms (productsAtom, cartAtom, derived atoms, action atoms)
  • Hook 분리 (useLocalStorage, useNotifications, useDebounce, useCoupon, useProducts, useCart, useUIState 등)
  • 서비스 레이어 (productService, cartService, couponService)
  • Utils 분리 (calculators.ts, formatters.ts, validators.ts, filters.ts)
  • Middle-Out 컴포넌트 분리 (CouponManagement, ProductManagement 등)
  • Packaging 관련 준비 (디렉터리 분리, docs 정리)
  • CI/CD 추가 (.github workflow), lint/format 설정 변경
  • 결합점: 알림(addNotification)·localStorage·atoms 간 상호참조
  1. PullRequestBody의 "과제 셀프회고" / "내가 제일 신경 쓴 부분" 에 대한 피드백 (인사이트 중심)
  • 잘하신 점

    • 아주 체계적으로 단계별(계산→훅→컴포넌트→전역상태)로 리팩토링하셨고, docs/timeline에 디테일을 남겨 둔 점은 훌륭합니다. 이는 유지보수·온보딩에 큰 자산입니다.
    • 서비스(도메인) vs Hook(상태) vs Utils(순수함수) 구분이 명확하며 SRP 원칙을 실천한 흔적이 분명합니다.
    • Jotai 도입 후 파생 atom(cartTotalsAtom 등)과 액션 atom(addToCartAtom) 사용으로 전역 상태의 계산/변경을 캡슐화한 점은 전역 상태 설계 관점에서 효과적입니다.
  • 인사이트(다음으로 확장해볼 만한 생각거리)

    1. "액션 atom 내부에서 addNotification을 직접 사용" 같은 패턴은 편하지만 결합을 높입니다. 알림 시스템을 변경(예: toast 라이브러리, 원격 로깅)해야 할 때 영향 범위가 커집니다. 액션은 상태 변경 책임만, 외부 부수효과는 콜백으로 분리하는 패턴(의존성 주입 또는 이벤트 버스)을 고려해보세요.
    2. Jotai atoms를 직접 export하는 방식은 앱단에서는 편하지만, 라이브러리(패키지)로 추출할 때 문제를 만듭니다. 패키지화하려면 atoms 생성 팩토리(factory)를 제공하거나, atoms를 내부의 구현으로 숨기고 public API(혹은 hooks)만 노출하는 방식이 안전합니다.
    3. docs에 훌륭한 타임라인과 의사결정 논리(왜 split/왜 atom)를 남기셨는데, “변경에 따른 소유권/영향도(누가 무엇을 바꿔야 하는지)”를 더 명시하면 팀 협업에서 훨씬 유용합니다.
  • 이어서 질문해볼 만한 몇가지

    • 액션 atom이 로컬 side-effect(예: localStorage, notification)를 직접 다루는 현재 패턴에서, 외부 API(서버) 연동이나 로그 추적을 추가한다면 어떤 전략으로 점진적으로 분리하시겠습니까?
    • 패키지를 배포(도메인 → npm으로)할 경우 타입/peerDependencies 관리에서 어떤 기준을 두시겠습니까? (예: React 버전, Jotai 포함 여부)
    • 테스트 관점에서 action atom을 어떻게 단위 테스트/통합 테스트로 검증할 계획인가요? (현재 tests 스크립트가 존재하므로 테스트 전략 명확화 권장)
  1. PullRequestBody의 “리뷰 받고 싶은 내용(질문)”에 대한 답변
    질문 1) "application/cart/addItemToCart.ts" 같이 “도메인/동사+목적어” 네이밍과 디렉터리 구조가 적절한지, 복잡한 행동의 분기는 어디까지 application/*에 둘지?
  • 네이밍(도메인/동사+목적어)은 좋은 컨벤션입니다. "application/cart/addItemToCart.ts"는 use-case(유저 행동)를 잘 표현합니다.
  • 복잡한 행동(검증·계산·여러 정책 조합)의 분기 기준:
    • 도메인 순수 로직(계산/검증/정책)은 domain/policies 또는 services/*.ts로 둡니다. (순수함수, 테스트 가능)
    • application 레이어는 "오케스트레이션"(입력 수집 → 정책 호출 → 상태 변화 → 외부 콜백 호출)을 담당합니다. 즉 복잡한 분기 자체는 domain/policy에서 결정하고, application은 그 결과에 따라 흐름을 수행(사이드이펙트, 트랜잭션 관리 등)하도록 유지하세요.
  • 결론: "도메인/동사+목적어" 네이밍은 적절. 복잡한 분기는 domain(정책 + 계산)으로 올리고, application은 orchestration만 담당.

질문 2) "정책 상향 기준" (현재 제안: 재사용 ≥ 2, 순수성, 화면 무관 설명 가능)

  • 제안 기준은 좋습니다만 아래 항목들을 추가하면 더 견고합니다:
    1. 안정성(변경 빈도): 자주 변경되지 않고, 안정적인 비즈니스 규칙일 것
    2. 테스트 커버리지: 해당 로직을 단위 테스트로 검증 가능한지
    3. 복잡성: 단순히 재사용되더라도 비즈니스 규칙·도메인 언어로 설명할 수 있을 정도의 복잡성이 있는지
    4. 의존성: 외부 I/O(네트워크·DOM)에 의존하지 않는 순수함수일수록 상향 우선
  • 최종 추천 규칙: (재사용성≥2 OR 도메인 의미가 강함) AND (순수성 OR 테스트 가능성) AND (안정성/비즈니스 중요도 높음) → domain/policies로 승격

상세 피드백 (파일/기능별: 개념 정의 → 문제 → AS-IS → TO-BE 코드)

먼저 다루는 개념 정의(리뷰에서 사용)

  • 결합도(coupling): 모듈 간의 의존성 강도. 낮을수록 변경 영향도가 작음.
  • 응집도(cohesion): 한 모듈 내부의 구성요소들이 동일 목적을 공유하는 정도. 높을수록 모듈 추출/패키징이 쉬움.

A. 상태관리 라이브러리가 달라지는 경우 시나리오 + 영향 분석
시나리오 1: Jotai → Zustand (local/global state)

  • 영향 포인트
    • atoms 파일(productAtoms.ts, cartAtoms.ts, couponAtoms.ts, uiAtoms.ts, notificationAtoms.ts)을 Zustand store slices로 변환
    • 파생 atom(derived)들은 selector / computed 상태로 다시 작성 필요
    • action atoms (addToCartAtom) → zustand actions (store.setState or immer based)
  • AS-IS (Jotai)
    // atoms/cartAtoms.ts (AS-IS)
    export const cartAtom = atom<CartItem[]>([]);
    export const addToCartAtom = atom(null, (get, set, product: ProductWithUI) => {
      const cart = get(cartAtom);
      const newCart = cartService.addItemToCart(product, cart);
      set(cartAtom, newCart);
      const notifications = get(notificationsAtom);
      set(notificationsAtom, [...notifications, { id: Date.now(), message: '장바구니에 담았습니다', type: 'success' }]);
    });
  • 문제: addToCartAtom 내부에서 notificationsAtom을 직접 set → cart와 notification이 강하게 결합됨.
  • TO-BE (Zustand slice 스타일; 결합 완화)
    // stores/useCartStore.ts (TO-BE)
    import create from 'zustand';
    import { cartService } from '../services/cartService';
    
    interface CartState {
      cart: CartItem[];
      addToCart: (product: ProductWithUI, opts?: { onSuccess?: ()=>void, onError?: (err:any)=>void }) => void;
    }
    
    export const useCartStore = create<CartState>(set => ({
      cart: [],
      addToCart: (product, opts) => {
        try {
          set(state => ({ cart: cartService.addItemToCart(product, state.cart) }));
          opts?.onSuccess?.();
        } catch (err) {
          opts?.onError?.(err);
        }
      }
    }));
    • 장점: 알림은 호출자(컴포넌트 또는 hook)에서 onSuccess로 처리하게 하여 결합을 낮춤.

시나리오 2: Jotai → TanStack Query

  • tanstack-query는 서버/캐시 중심. 로컬 엔티티 상태(폼, UI)엔 적합하지 않음.
  • 영향:
    • products, cart와 같은 불변 데이터를 서버-동기 캐시로 이동시키면 fetch/invalidates pattern 필요.
    • derive 계산(총액) => useQuery + selector (or useMemo)로 변경.
  • 권장 아키텍처:
    • 서버 소스의 상품 목록: react-query (data-fetching, cache)
    • UI state, ephemeral state: local hooks / atom-like store
    • 계산유틸(calculators.ts)은 그대로 재사용 가능

시나리오 3: Jotai → Redux

  • atoms -> slices + selectors + thunks
  • addToCartAtom (action atom) -> thunk action
  • TO-BE (Redux thunk)
    // cartSlice.ts
    const addToCart = createAsyncThunk('cart/addToCart', async ({product}, {getState, dispatch}) => {
      const newCart = cartService.addItemToCart(product, getState().cart);
      // dispatch notification action separately
      dispatch(notificationAdded({ message: 'Added', type: 'success' }));
      return newCart;
    });
  • 선택의 기준: 규모·팀 선호·middleware 필요 여부에 따라 선택

요약 영향표:

  • 파생 상태/derived atoms: 각 라이브러리에서 계산/selector로 다시 작성해야 함
  • Action atoms: 라이브러리별 action/thunk/slice로 매핑
  • Hooks: atoms를 직접 사용하는 훅은 해당 라이브러리의 구독 API로 수정 필요

B. 모듈화·패키지로 배포해야 하는 경우(응집도·결합도 관점)

  • 평가 대상: domain(services/models/calculators), hooks, ui-components
  • 패키지화 우선순위(권장)
    1. domain/policies & services & utils (가장 적합)
      • 응집도: 높음 (비즈니스 규칙·계산 함수가 한 도메인에 모여 있음)
      • 결합도: 낮음 (순수함수, 도메인 모델)
      • 포장 방식: npm 패키지로 배포 가능 (peerDependencies: none or minimal)
      • TO-BE export API 예시:
        // package entry (e.g., @yourorg/cart-domain)
        export * from './models';
        export * from './services/cartService';
        export * from './calculators';
      • 권장: 패키지 내부에 React/DOM 의존성 두지 말고, purely JS/TS 로 유지하세요.
    2. UI 컴포넌트 라이브러리 (Button, Notification 등)
      • 응집도: 높음 (UI 관련)
      • 결합도: 약간 높음(스타일/테마에 따라) → 테마/스타일을 prop으로 주입하도록 설계 필요
      • 포장 방식: separate package, peerDependency로 React 요구
    3. hooks / atoms (앱 전용)
      • 응집도: 보통(앱 전용), 결합도: 높음(앱의 atoms, storage, 디자인에 의존)
      • 권장: 앱간 공통성이 매우 높지 않다면 패키지화는 신중. 만약 패키지로 만들 경우 factory 패턴 제공 권장:
        // TO-BE: factory for atoms/hook rather than singletons
        export function createProductModule(opts?: { storageKey?: string }) {
          const productsAtom = atomWithStorage(opts?.storageKey ?? 'products', []);
          const useProducts = () => { ... useAtom(productsAtom) ... };
          return { productsAtom, useProducts };
        }
      • 이유: singleton atom이 패키지로 배포되면 여러 앱/버전 충돌 위험

응집도 체크 리스트(당신의 정의 기준으로)

  • 변경 동선 짧은가?
    • domain/services/utils: 예 (수정 범위가 작음)
    • hooks/atoms/components: 혼재된 경우 종종 여러 파일을 동시에 수정해야 해서 동선이 커질 수 있음
  • 패키지로 떼어내기 쉬운가?
    • domain/utils: 매우 쉬움 (독립성)
    • hooks/atoms: atoms를 단일 인스턴스로 export하면 떼어내기 어려움 → factory로 변경 필요

결합도 예시(안 좋은/좋은)

  • 안 좋은 예시 (결합 높음):
    // useAddProduct 안좋음: addNotification을 내부에서 직접 사용하도록 강제
    const useAddProduct = (addNotification) => { ... }
  • 좋은 예시 (결합 낮음):
    const useAddProduct = ({ onSuccess, onError }) => { ... }
    // 호출 측에서 알림을 담당하거나 atom을 사용할 수 있다

권장 리팩토링: action atom / hook에서 알림/사이드이펙트 분리

  • AS-IS (action atom with notification)
    // atoms/cartAtoms.ts
    export const addToCartAtom = atom(null, (get, set, product) => {
      const newCart = cartService.addItemToCart(product, get(cartAtom));
      set(cartAtom, newCart);
      set(notificationsAtom, [...get(notificationsAtom), { ... }]);
    });
  • TO-BE (action atom only updates state; notification handled by caller)
    // atoms/cartAtoms.ts
    export const addToCartAtom = atom(null, (get, set, product) => {
      const newCart = cartService.addItemToCart(product, get(cartAtom));
      set(cartAtom, newCart);
    });
    
    // Component or hook:
    const addToCart = useSetAtom(addToCartAtom);
    const addNotification = useSetAtom(notificationsAtom);
    const handleAdd = (product) => {
      addToCart(product);
      addNotification([...]);
    };
    또는 DI 방식:
    const addToCart = (product, { notify }) => {
      // state change
      notify('success', 'Added');
    }

C. 구체적 파일/흐름 기반 상세 피드백 (PullRequestFiles 기반)
(각 항목: 개념 정의 → 문제 → AS-IS → TO-BE 코드)

  1. atoms/*.ts (productAtoms/cartAtoms/couponAtoms/uiAtoms/notificationAtoms)
  • 개념: 전역 상태의 원자(단위) 저장소
  • 문제
    • 액션 atom이 다른 atom들을 직접 set(get)을 통해 부수작용을 처리하는 경우가 있음(addToCartAtom 내부 알림)
    • atoms 직접 export는 앱내 전역싱글톤이 되어 패키지화·테스트 시 제약(전역 상태 충돌)
  • AS-IS
    export const cartAtom = atom<CartItem[]>([]);
    export const addToCartAtom = atom(null, (get, set, product) => {
      const cart = get(cartAtom);
      const newCart = cartService.addItemToCart(product, cart);
      set(cartAtom, newCart);
      // notifications directly written
      set(notificationsAtom, [...get(notificationsAtom), { id: Date.now(), message: '...'}]);
    });
  • TO-BE
    • 분리 전략 1: 액션 atom은 상태 업데이트만, 알림은 호출자에서 처리
    • 분리 전략 2: atoms 생성기(factory)로 교체 (패키지 추출 대비)
    // factory pattern
    export function createCartAtoms({storageKey = 'cart'} = {}) {
      const cartAtom = atomWithStorage<CartItem[]>(storageKey, []);
      const addToCartAtom = atom(null, (get, set, product) => {
        const newCart = cartService.addItemToCart(product, get(cartAtom));
        set(cartAtom, newCart);
      });
      return { cartAtom, addToCartAtom };
    }
  1. hooks/* (useLocalStorage, useNotifications, useDebounce, useCoupon, useProducts, useCart, useUIState, useCouponForm)
  • 개념: 컴포넌트와 도메인/서비스·UI 상태 사이의 API
  • 좋은 점: 관심사 분리, 재사용성 확보
  • 문제
    • 몇몇 훅은 외부 의존성(예: addNotification)을 인자로 받지만 일부 액션 atom과 내부에서 알림을 직접 호출하는 패턴이 섞여 있음 → 혼란
    • useProducts/useCart 아직 일부는 props 의존성을 남겨둔 상태(문서에서도 변환 중 명시)
  • AS-IS (안좋은 결합 예)
    // useCart({ products, selectedCoupon, addNotification })
    // addNotification이 hook 내부로 흘러들어옴 -> 호출자 책임 불명확
  • TO-BE (명확한 DI, 콜백 사용하는 패턴)
    const useCart = ({ onNotify } = {}) => {
      const addToCart = useSetAtom(addToCartAtom);
      const add = (product) => {
        addToCart(product);
        onNotify?.({ type: 'success', message: '장바구니에 담았습니다' });
      }
      return { add };
    }
    또는 훅 자체는 상태만 제공하고, 컴포넌트가 알림을 처리하게 함:
    const addToCart = useSetAtom(addToCartAtom);
    const notify = useSetAtom(notificationsAtom);
    const handleAdd = (product) => {
      addToCart(product);
      notify([...]); // 호출자는 결정권을 가짐
    };
  1. services/* (productService, cartService, couponService)
  • 개념: 순수 비즈니스 규칙/검증/생성 등의 도메인 로직
  • 장점: 재사용성·테스트 용이성(순수함수)
  • 문제점: docs에선 잘 분리됐으나 실제 Hook/Atom과 직접 참조 결합이 있을 수 있음 (예: cartService를 action atom 내부에서 직접 사용)
  • 권장: 서비스는 순수/독립으로 유지. Hook/atoms은 service를 주입(DI)해서 사용
  • TO-BE (DI 사용)
    // useProducts.js
    export const useProducts = (deps = { productService }) => {
      const addProduct = (p) => {
        const created = deps.productService.createProduct(p);
        setProducts(prev => [...prev, created]);
      }
    }
  1. utils/calculators.ts / filters.ts / formatters.ts
  • 개념: 순수함수의 집합(계산/format/필터)
  • 장점: 응집도가 높고 패키지 추출 시 좋은 후보
  • 문제: 없음(현재 상태에서 매우 적절)
  • 권장: export된 함수는 도메인 패키지로 그대로 옮겨도 무난함. 타입과 경계(도메인 모델 타입)를 명확히 export하세요.
  1. docs / workflows / eslint / prettier / package.json 변경
  • 장점: 문서화가 매우 훌륭함. CI(Deploy) 추가와 lint/format 설정도 깔끔함.
  • 주의
    • package.json에 jotai를 dev/dep로 추가하셨는데, 패키지 버전 충돌을 방지하려면 peerDependencies 정책을 문서화하세요(특히 라이브러리로 추출 시).
    • eslint.config.js 추가: 기존 .eslintrc 제거하셨는데 lint 규칙이 프로젝트 전체에 적용되는지 CI에서 검증 필요.

D. AS-IS / TO-BE - 구체적인 코드 교정 예시 (요청하신 방식)

  1. 알림 결합 문제 (안 좋은 예 → 개선)
  • AS-IS
    // atoms/cartAtoms.ts
    export const addToCartAtom = atom(null, (get, set, product) => {
      const cart = get(cartAtom);
      set(cartAtom, cart.concat(product));
      set(notificationsAtom, [...get(notificationsAtom), { id: Date.now(), message: 'Added', type: 'success' }]);
    });
  • 문제: addToCartAtom이 notificationsAtom을 직접 조작. 알림 로직 바뀌면 atom 수정이 필요 → 결합도 ↑
  • TO-BE 1 (콜백/호출자에서 알림)
    // atoms/cartAtoms.ts
    export const addToCartAtom = atom(null, (get, set, product) => {
      const cart = get(cartAtom);
      set(cartAtom, cart.concat(product));
    });
    
    // Component side
    const addToCart = useSetAtom(addToCartAtom);
    const addNotification = useSetAtom(notificationsAtom);
    const handleAdd = (product) => {
      addToCart(product);
      addNotification(prev => [...prev, { id: Date.now(), message: 'Added', type: 'success' }]);
    };
  • TO-BE 2 (옵션: 액션 반환값으로 성공/에러)
    // addToCartAtom: return boolean (synchronous)
    export const addToCartAtom = atom(null, (get, set, product) => {
      try {
        const newCart = cartService.addItemToCart(product, get(cartAtom));
        set(cartAtom, newCart);
        return { success: true };
      } catch (err) {
        return { success: false, error: err };
      }
    });
    
    // caller
    const result = await dispatchAddToCart(product);
    if (result.success) notify('success', 'Added');
    else notify('error', result.error.message);
  1. Hook 인터페이스 개선: 나쁜 예 → 좋은 예
  • AS-IS
    // useAddProduct bad
    const useAddProduct = (addNotification) => { ... }
  • TO-BE
    const useAddProduct = ({ onSuccess, onError } = {}) => {
      const addProduct = (payload) => {
        try {
          // do add
          onSuccess?.();
        } catch (err) {
          onError?.(err);
        }
      }
      return { addProduct };
    }
  1. atoms 패키지화(추출) - AS-IS vs TO-BE (factory)
  • AS-IS (직접 export)
    // productAtoms.ts
    export const productsAtom = atomWithStorage<Product[]>('products', []);
    export const productFormAtom = atom({ ... });
    • 문제: 앱 범위에선 OK. 패키지로 재사용 시 storage key 충돌, singleton 문제.
  • TO-BE (factory)
    export function createProductModule(opts = {storageKey: 'products'}) {
      const productsAtom = atomWithStorage<Product[]>(opts.storageKey, []);
      const productFormAtom = atom({ ... });
      const useProducts = () => {
        // useAtom(productsAtom) 등
      }
      return { productsAtom, productFormAtom, useProducts };
    }
    • 장점: 여러 앱/테스트 환경에서 인스턴스 생성 가능

E. 종합 결론 및 우선 순위 제안 (실행 가능한 권고)

  1. 단기(빠르게 적용 권장)
  • 분리: Action atom 내부에서 notification set 호출 제거 → 호출자(훅/컴포넌트)에서 알림 처리. (응집도↗ / 결합도↓)
  • atoms → factory 패턴 적용: product/cart/coupon atoms들을 createXModule 형태로 감싸기. (패키지 추출 준비)
  • hooks: useAddProduct 등의 public API는 ({onSuccess,onError}) 형태로 바꾸기
  1. 중기(설계 개선)
  • atoms/actions 문서화(usage contract) 및 테스트 추가(액션 atom의 동작 검증)
  • services를 점검해 application layer가 orchestration만 하도록 명확히 나누기
  1. 장기(패키지화/라이브러리)
  • domain/services/utils를 독립 패키지로 추출. (types, calculators, policies)
  • UI 컴포넌트 패키지(선택): Button/Notification 등은 스타일/테마를 prop으로 주입 가능하게 만들어 배포
  • hooks/atoms는 앱 전용으로 남기거나, factory 패턴으로 패키지화

우선순위(권장)

  1. 액션 atom의 알림 결합 제거 (빠르게 적용 가능)
  2. atoms factory 적용(테스트/패키지 준비)
  3. hooks API 통일(onSuccess/onError)
  4. packaging: domain 패키지 추출

마무리 요약 — “잘하셨습니다” + 핵심 권고

  • 전체 설계 방향(유틸·서비스·훅·컴포넌트 분리, 중간 수준의 함수분해, Jotai 전환)은 매우 적절합니다.
  • 다음으로 집중할 부분은 결합도 낮추기(특히 알림/side-effect)와 패키지화 대비(Atoms factory, API 안정화)입니다.
  • 질문에 대한 구체적 답: "도메인/동사+목적어 네이밍"과 "정책 상향 기준" 모두 적절하며, 정책 승격에 테스트 커버·안정성·재사용성·순수성을 추가하는 것을 권합니다.

원하시면:

  • (1) 현재 repo의 특정 파일(예: atoms/cartAtoms.ts, hooks/useCart.ts)을 실제 코드 기준으로 리팩토링 PR 패치(구체적 diff)로 만들어 드리거나,
  • (2) 도메인 패키지 추출을 위한 리팩토링 가이드(단계별 체크리스트 + 코드는 어떤 파일을 어디로 옮기고 export를 어떻게 바꿀지) 를 작성해 드릴게요.

원하시는 다음 행동을 알려주세요. 어떤 파일(혹은 영역)을 우선 리팩토링해 드릴까요?

@jeongmingi123
Copy link
Copy Markdown

정석님 고민한 흔적이 멋져요!!

}

// localStorage와 연동되는 atom
export const cartAtom = atomWithStorage<CartItem[]>('cart', []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

jotai 에도 이런 기능이 있었군요

Comment on lines +33 to +36
export const addToCartAtom = atom(null, (get, set, product: ProductWithUI) => {
const cart = get(cartAtom);
const products = get(productsAtom);
const notifications = get(notificationsAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이렇게 하는 것도 나쁘지 않을지도?

Comment on lines +65 to +71
setTimeout(() => {
const currentNotifications = get(notificationsAtom);
set(
notificationsAtom,
currentNotifications.filter(n => n.id !== id)
);
}, 3000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 부분은 별도로 분리할 수 있으면 좋을 것 같네요

Comment on lines +32 to +38
export const couponFormAtom = atom({
id: '',
name: '',
code: '',
discountType: 'amount' as 'amount' | 'percentage',
discountValue: 0,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
export const couponFormAtom = atom({
id: '',
name: '',
code: '',
discountType: 'amount' as 'amount' | 'percentage',
discountValue: 0,
});
export const couponFormAtom = atom<쿠폰폼타입>({
id: '',
name: '',
code: '',
discountType: 'amount',
discountValue: 0,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

타입을 지정하고 단언을 제거하면 어떨까요

import { cartAtom, totalItemCountAtom } from '../../atoms/cartAtoms';

export const CartIcon: React.FC = () => {
const [cart] = useAtom(cartAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

값을 읽기만 할 땐 useAtomValue
변경만 할 땐 useSetAtom 를 쓰는 것도 좋을 것 같아요

Comment on lines +86 to +130
onChange={e => {
const value = e.target.value;
if (value === '' || /^\d+$/.test(value)) {
setCouponForm({
...couponForm,
discountValue: value === '' ? 0 : parseInt(value),
});
}
}}
onBlur={e => {
const value = parseInt(e.target.value) || 0;
if (couponForm.discountType === 'percentage') {
if (value > 100) {
addNotification(
'할인율은 100%를 초과할 수 없습니다',
'error'
);
setCouponForm({
...couponForm,
discountValue: 100,
});
} else if (value < 0) {
setCouponForm({
...couponForm,
discountValue: 0,
});
}
} else {
if (value > 100000) {
addNotification(
'할인 금액은 100,000원을 초과할 수 없습니다',
'error'
);
setCouponForm({
...couponForm,
discountValue: 100000,
});
} else if (value < 0) {
setCouponForm({
...couponForm,
discountValue: 0,
});
}
}
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이벤트 핸들러들 따로 분리해 주면 더 좋을 것 같아요

Comment on lines +29 to +38
{activeTab === 'products' ? (
<ProductManagement
onAddProduct={onAddProduct}
onUpdateProduct={onUpdateProduct}
onDeleteProduct={onDeleteProduct}
onStartEditProduct={onStartEditProduct}
/>
) : (
<CouponManagement />
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
{activeTab === 'products' ? (
<ProductManagement
onAddProduct={onAddProduct}
onUpdateProduct={onUpdateProduct}
onDeleteProduct={onDeleteProduct}
onStartEditProduct={onStartEditProduct}
/>
) : (
<CouponManagement />
)}
{{
products: <ProductManagement
onAddProduct={onAddProduct}
onUpdateProduct={onUpdateProduct}
onDeleteProduct={onDeleteProduct}
onStartEditProduct={onStartEditProduct}
/>,
coupons:<CouponManagement />
}[activeTab]}

이렇게 작성해도 좋을 것 같아요 탭이 늘어났을 때 변경이 쉽도록

Copy link
Copy Markdown

@angielxx angielxx Aug 9, 2025

Choose a reason for hiding this comment

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

지훈님이 말씀하신대로 한다면 이 객체를 따로 변수로 분리하면 좋겠네요

Suggested change
{activeTab === 'products' ? (
<ProductManagement
onAddProduct={onAddProduct}
onUpdateProduct={onUpdateProduct}
onDeleteProduct={onDeleteProduct}
onStartEditProduct={onStartEditProduct}
/>
) : (
<CouponManagement />
)}
type TabType = 'products' | 'coupons'
const activeSection: Record<TabType, ReactNode > = {
products: <ProductManagement
onAddProduct={onAddProduct}
onUpdateProduct={onUpdateProduct}
onDeleteProduct={onDeleteProduct}
onStartEditProduct={onStartEditProduct}
/>,
coupons: <CouponManagement />
};

Copy link
Copy Markdown

@creco-hanghae creco-hanghae Aug 11, 2025

Choose a reason for hiding this comment

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

변수로 분리하면 읽는데 왔다갔다해야하니까 이건 어떤가요 (그냥 지훈님 제안도 나쁘진 않아요)

      <Switch
        value={activeTab}
        cases={{
          products: (
            <ProductManagement
              onAddProduct={onAddProduct}
              onUpdateProduct={onUpdateProduct}
              onDeleteProduct={onDeleteProduct}
              onStartEditProduct={onStartEditProduct}
            />
          ),
          coupons: <CouponManagement />
        }}
      />

Comment on lines +26 to +31
export const useCart = () => {
const [cart, setCart] = useAtom(cartAtom);
const [totalItemCount] = useAtom(totalItemCountAtom);
const [totals] = useAtom(cartTotalsAtom);
const [products] = useAtom(productsAtom);
const { addNotification } = useNotifications();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

상태 변경을 분리해서 영향을 덜 받도록 만드는 것도 좋을 것 같습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저도 Cart 가 Notifaction을 알고 있는게 이상한 것 같아요.
useCart 에 인자로 onSuccess 와 onError 를 받고 거기서 notification을 처리해보면 어떨까 싶습니다.

Comment on lines +24 to +28
export const useCoupon = () => {
const [cart] = useAtom(cartAtom);
const [coupons, setCoupons] = useAtom(couponsAtom);
const [selectedCoupon, setSelectedCoupon] = useAtom(selectedCouponAtom);
const { addNotification } = useNotifications();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

atom 내부에서 알림을 띄우는 경우도 있어서 (addToCartAtom) 통일시켜 예측 가능하도록 개선할 수 있을 것 같아요

const [products] = useAtom(productsAtom);
const { addNotification } = useNotifications();

const onAddToCart = useCallback(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

on 이라고 붙이는 것이 알맞을지 고려해보기

[클린코드 조각모음] 이벤트와 함수는 다르다

const roundToInteger = (value: number): number => Math.round(value);

// 할인 계산 관련 함수들 - 중간 수준 분리
const calculateBaseDiscount = (discounts: any[], quantity: number): number => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

특정한 도메인을 다루는 함수들은 별도로 분리하는 건 어떨까요?
이 부분은 저도 헷갈리지만 QnA 시간에 들은 내용을 기반으로 하면 함수, 인자 이름을 특정한 데이터 값으로 지정하면 유틸 함수가 아닐수도 있을 것 같아요

@JiHoon-0330
Copy link
Copy Markdown

컴포넌트나 훅 분리가 깔끔한 것 같습니다

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CartIcon은 레이아웃보다는 ui 컴포넌트가 맞는 것 같습니다!

Comment on lines +24 to +28
{cart.length > 0 && (
<span className='absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center'>
{totalItemCount}
</span>
)}
Copy link
Copy Markdown

@angielxx angielxx Aug 9, 2025

Choose a reason for hiding this comment

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

CartIcon 컴포넌트는 Icon 관련 로직만 내부에서 처리하는게 좋을 것 같아요. cart, totalItemCount는 외부 상태인데, UI가 지금 외부 상태에 의존하고 있기 때문에 나중에 수정하거나 확장하기 어려워보여요. totalItemCount를 props로 주입받을 수 있도록 하면 좋을 것 같아요.

• 외부 상태 의존 제거: cartCount만 props로 받아서 어디서든 재사용 가능. 스토어 교체/테스트가 쉬워집니다.
• 단일 책임 분리: 아이콘 + 뱃지 렌더링만 담당. 카트 비즈니스 로직은 상위에서 계산해서 넘깁니다.

interface CartIconProps {
    cartCount?: number
}

export const CartIcon: React.FC = ({ cartCount = 0 }: CartIconProps) => {
  const showBadge = cartCount > 0;

  return (
    <div className='relative'>
      <svg
        className='w-6 h-6 text-gray-700'
        fill='none'
        stroke='currentColor'
        viewBox='0 0 24 24'
      >
        <path
          strokeLinecap='round'
          strokeLinejoin='round'
          strokeWidth={2}
          d='M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z'
        />
      </svg>
      {showBadge && (
        <span className='absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center'>
          {showItemCount}
        </span>
      )}
    </div>
  );
};

* @returns 쿠폰 관련 상태와 함수들
*/
export const useCoupon = () => {
const [cart] = useAtom(cartAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cart를 참조하는 곳은 전부 useCart()의 반환값만 쓰도록 통일하면 좋겠습니다. 지금처럼 각 컴포넌트가 useAtom(cartAtom)을 직접 참조하면, 이후 cart에 가공/파생 로직이 추가될 때 참조 지점을 전부 수정해야 해요. 반면 useCart 한 곳으로 캡슐화하면 변경이 생겨도 훅 내부만 고치면 되어 유지보수가 훨씬 수월합니다!

Comment on lines +26 to +27
const [coupons, setCoupons] = useAtom(couponsAtom);
const [selectedCoupon, setSelectedCoupon] = useAtom(selectedCouponAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

위 코멘트와 동일하게 useCoupon 반환값으로 사용해주는 게 좋겠습니다!

import { isAdminAtom } from '../../atoms/uiAtoms';

export const Header: React.FC = () => {
const [isAdmin] = useAtom(isAdminAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useUIStates 훅에서 반환하는 isAdmin 값을 사용하면 좋을 것 같아요.

import { isAdminAtom } from '../../atoms/uiAtoms';

export const Navigation: React.FC = () => {
const [isAdmin, setIsAdmin] = useAtom(isAdminAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useUIStates 훅에서 반환하는 isAdmin 값을 사용하면 좋을 것 같아요. (2)

import { useAtom } from 'jotai';
import { isAdminAtom } from '../../atoms/uiAtoms';

export const Navigation: React.FC = () => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

현재 기능만 놓고 보면 전체 내비게이션이라기보다는 “모드 전환 버튼” 성격이 강하니까, 'AdminToggleButton' 같은 좀 더 구체적인 이름이 적절할 것 같습니다. Navigation이라고 하니 컴포넌트가 어플리케이션 전체 네비게이션 역할을 할 것 같아요.

import { searchTermAtom } from '../../atoms/uiAtoms';

export const SearchBar: React.FC = () => {
const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 아이도 useSearch 반환값을 사용하면 좋겠네요

import { activeTabAtom } from '../../../atoms/uiAtoms';

export const AdminHeader: React.FC = () => {
const [activeTab, setActiveTab] = useAtom(activeTabAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useUIStates 훅에서 반환하는 값을 사용하면 좋을 것 같아요. (3)

Comment on lines +18 to +20
{coupon.discountType === 'amount'
? `${coupon.discountValue.toLocaleString()}원 할인`
: `${coupon.discountValue}% 할인`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

저는 jsx 코드는 진짜 UI만 담당할 수 있도록 이런 부분도 최대한 변수로 분리하려고 하는 편인데요. 다른 분들 의견은 어떨지 궁금하네요

Suggested change
{coupon.discountType === 'amount'
? `${coupon.discountValue.toLocaleString()}원 할인`
: `${coupon.discountValue}% 할인`}
const discountLabel =
coupon.discountType === 'amount'
? `${coupon.discountValue.toLocaleString()}원 할인`
: `${coupon.discountValue}% 할인`;
<span className='inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-white text-indigo-700'>
{discountLabel}
<span>

onSubmit,
onCancel,
}) => {
const [couponForm, setCouponForm] = useAtom(couponFormAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

couponForm 데이터는 CouponForm에서만 쓰일 것 같은데 전역 상태로 구현하신 이유가 궁금합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

JSX 내부에 이벤트 핸들링 로직들은 핸들러 함수로 따로 분리해주는 게 좋을 것 같아요.

Comment on lines +10 to +15
if (isAdmin) {
return `${price.toLocaleString()}원`;
}

// 테스트에서 기대하는 형식으로 변경
return `${price.toLocaleString()}원`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isAdmin이나 아니나 결과가 동일해서 조건문 없어도 되겠네요

import { useCouponForm } from '../../../hooks/useCouponForm';

export const CouponManagement: React.FC = () => {
const [coupons] = useAtom(couponsAtom);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

useCoupons 반환값으로 변경하면 좋을듯요 (n)

Comment on lines +93 to +100
export const getRemainingStock = (
product: Product,
cart: CartItem[]
): number => {
const cartItem = cart.find(item => item.product.id === product.id);
const quantityInCart = cartItem?.quantity || 0;
return product.stock - quantityInCart;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

product의 속성인 stock을 계산하는 것이기 때문에 product 도메인 관련된 부분에서만 사용될 것 같아요. 그래서 모든 곳에서 공용으로 쓰일 수 있는 로직을 포함하는 util보다는 productService에 넣으면 좋을 것 같습니다.

Comment on lines +20 to +22
const hasBulkPurchase = (cart: CartItem[]): boolean => {
return cart.some(cartItem => cartItem.quantity >= BULK_PURCHASE_THRESHOLD);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

계산 유틸보다는 cart 도메인에만 해당되기 때문에 cartService에 넣으면 좋을 것 같아요.

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.

10 participants