[4팀 김지혜] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #42
[4팀 김지혜] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #42adds9810 wants to merge 54 commits intohanghae-plus:mainfrom
Conversation
- 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 상태 유지
- 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 알림 처리 로직 정리 - 패키지 및 워크스페이스 설정 업데이트
JunilHwang
left a comment
There was a problem hiding this comment.
안녕하세요. 과제 잘 정리해주셨습니다 — 큰 컴포넌트를 도메인(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)
장점: 변경 시 파일 이동 범위가 좁아지고(응집도↑), 도메인 단위로 패키지화하기 쉬움.
- cart/
- store/
질문 부가: 액션/utility 분리
- “액션 atom들을 하나의 파일로 관리/분리하는 패턴”
- 작은 프로젝트: 도메인별로 같이 두는게 직관적
- 중대형/여러 패키지로 쪼갤 계획: actions는 도메인 패키지 내부에서 관리하고 패키지 외부에선 도메인 훅(useCart)만 노출 — 내부 구현(Atom/Redux) 숨김
────────────────────────────────
종합 피드백
- PullRequestFiles 분석을 통한 핵심 키워드
- 긍정적: 분리된 컴포넌트(재사용성↑), 모델/유틸로의 로직 이동(순수 함수 활용), Jotai 도입(심화)
- 주의 필요: 상태 라이브러리 종속성(직접 useAtom 사용 위치), 일부 유틸 시그니처에 의존성(포맷터가 products/cart를 요구), 패키지화할 때의 경계(어떤 코드가 domain인지 UI인지) 불명확성 가능
- PullRequestBody의 self-reflection(“과제 셀프회고” / “제일 신경 쓴 부분”)에 대한 인사이트
- 매우 좋은 접근: “왜 분리해야 하는지”를 고민한 점이 인상적입니다. 단순한 리팩터링(코드 분해)에서 더 나아가 설계 판단 기준(엔티티 vs UI, 훅 vs 순수함수)을 고민하신 점이 성장 포인트입니다.
- 이어서 생각해볼 질문:
- “이 도메인에서의 단위 변경(예: 할인 로직 변경)이 생기면 수정을 어디에서 얼마나 해야 하는가?” — 실제로 수정 경로(파일/함수)를 그려보세요.
- “이 컴포넌트나 훅이 어떤 테스트(단위/통합)를 가져야 하는가?” — 분산된 책임에 맞춘 테스트 전략(모델: 유닛테스트, domain hook: 통합 테스트, UI: 스냅샷/유저 시나리오)
- “패키지화한다면 public API(호출자에게 노출되는 함수/훅)는 무엇이어야 하는가?” — 최소한의 surface 만들기
- 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: (...) => ... });
- AS-IS (간략):
- ProductCard에서 addToCart 사용 (Jotai)
- 문제 발생 시:
- 모든 컴포넌트에서 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 등 전체 컨텍스트를 요구 → 포팅/패키지화시 외부 의존성 증가
- AS-IS (추상화된 예):
- 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 등
- domain-cart/
- 각 패키지는 자체 테스트와 public API(도메인 훅, 모델 함수)만 노출
- packages/
응집도/결합도 현재 상태 평가 (PR 기준)
- 응집도: 전반적으로 괜찮음
- 장점: models(예: ../../models/cart 등)을 사용하여 계산 로직을 모아서 유지·테스트 가능(응집도 ↑)
- 개선점: 일부 유틸/formatters가 많은 context를 요구해서 응집도를 낮출 소지 있음(formatPrice 예)
- 결합도: 보통에서 좋은 편
- 장점: action atoms가 옵션 콜백(onNotification)을 통해 콜백으로 처리하는 패턴은 결합도를 낮추는 좋은 사례
- 개선점:
- 컴포넌트들이 직접 useAtom를 통해 여러 atom에 접근(예: productsAtom, cartAtom, addNotificationAtom) → 컴포넌트가 여러 구현 세부사항을 알고 있음. 도메인 훅으로 감싸면 더 낮아짐.
구체적 코드 개선 샘플(AS-IS vs TO-BE)
- 컴포넌트가 직접 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만 교체하면 됨.
- 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)}
// 재고/라벨 처리도 호출부에서 담당장점: 각 함수 단일 책임, 단위 테스트 작성 쉬움, 패키지화 시 의존 경계 명확
- 액션 인터페이스 통일 (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
권장 작업 우선순위(실무적)
- 도메인 훅(useCart, useProducts, useCoupons, useNotification) 생성 — 컴포넌트가 이 훅들만 사용하도록 리팩토링
- 기존 action atoms/atoms를 도메인 폴더로 재배치(예: store/cart/*)
- formatters/유틸 시그니처 단일 책임으로 리팩토링
- 패키지화 계획 시 public API 문서화(README + 타입 선언)
- 상태 라이브러리 교체 필요 시 도메인 훅만 재작성 (컴포넌트 변경 최소화)
간단한 코드 예시: 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 코드 패치 예시를 드리겠습니다.
과제링크
https://adds9810.github.io/front_6th_chapter2-2/index.basic.html
https://adds9810.github.io/front_6th_chapter2-2/index.advanced.html
과제의 핵심취지
과제에서 꼭 알아가길 바라는 점
기본과제
심화과제
과제 셀프회고
항해의 절반을 지나오면서 과제를 해나가는 것 자체에는 익숙해졌지만, 매 과제마다 등장하는 새로운 개념과 이론에 여전히 허우적거리는 중입니다. 이번 과제에서 가장 큰 고민거리였던 것은 Entity와 UI의 개념과 역할 분리였습니다.
화요일에 진행되었던 팀원들과의 토론을 통해 다양한 관점을 접할 수 있었고, 이는 큰 도움이 되었습니다. "엔티티는 비즈니스 주체다", "앱의 명사에 해당하는 데이터 덩어리다"와 같은 명확한 정의부터, "UI는 어디까지 상태를 가져야 하는가?", "엔티티가 절대 해서는 안 되는 것은 무엇인가?"와 같은 실용적인 부분도 나눌 수 있었습니다. 막히는 것에 대한 질문에 답해주고 함께 고민해주는 팀원들 덕분에, 혼자였다면 한참을 헤맸을 길을 더듬더듬 나아갈 수 있었습니다.
그 덕분에 어찌어찌 과제를 완성할 수 있었습니다. 솔직히 말하면 "깊은 이해"라고 하기에는 아직 부족하지만, 단순히 코드를 작성하는 것을 넘어서 **"왜 이렇게 분리해야 하는지"**에 대해 생각해보게 되었고, 앞으로 비슷한 상황에서 "이렇게 분리하는 게 맞나?"라는 질문을 던질 수 있게 되었다는 점이 가장 큰 성장이었습니다.
과제를 하면서 내가 알게된 점, 좋았던 점은 무엇인가요?
App.tsx에서isAdmin,cart,totalItemCount등 많은 props를Header와CartPage로 전달하는 과정에서 "이게 맞나?" 하는 의문을 가졌고, Advanced에서는 Jotai를 도입하여 각 컴포넌트에서 직접isAdminAtom,cartAtom등을 사용할 수 있게 되어 코드가 훨씬 깔끔해진 것을 체감했습니다. 이론으로만 알던 "props drilling이 문제다"라는 말이 실제로 어떤 의미인지 몸소 경험할 수 있었던 점이 좋았습니다.ProductForm과CouponForm에서 자체 상태(productForm,couponForm)를 관리하고, Jotai Actions를 직접 호출하여 비즈니스 로직을 중앙화하는 패턴을 학습했습니다. 다만 여전히 일부 props(showProductForm,setShowProductForm등)는 받고 있어 완전한 독립성은 달성하지 못했습니다.이번 과제에서 내가 제일 신경 쓴 부분은 무엇인가요?
이번 과제를 통해 앞으로 해보고 싶은게 있다면 알려주세요!
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 편하게 남겨주세요 :)
atom설계 : advanced/store/atoms.ts에서 atom들을 작성했는데, 혹시 더 효율적으로 atom을 구성하거나 조합할 수 있는 방법이 있을까요? 예를 들어, 액션 atom들을 하나의 파일로 관리하는 것이 좋은 패턴인지, 아니면 관련된 atom과 함께 두는 것이 나을지 궁금합니다.