Skip to content

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

Open
annkimm wants to merge 16 commits intohanghae-plus:mainfrom
annkimm:main
Open

[8팀 김민지] Chapter 2-2. 디자인 패턴과 함수형 프로그래밍 #39
annkimm wants to merge 16 commits intohanghae-plus:mainfrom
annkimm:main

Conversation

@annkimm
Copy link

@annkimm annkimm commented Aug 5, 2025

링크

https://annkimm.github.io/front_6th_chapter2-2/

과제의 핵심취지

  • React의 hook 이해하기
  • 함수형 프로그래밍에 대한 이해
  • 액션과 순수함수의 분리

과제에서 꼭 알아가길 바라는 점

  • 엔티티를 다루는 상태와 그렇지 않은 상태 - cart, isCartFull vs isShowPopup
  • 엔티티를 다루는 컴포넌트와 훅 - CartItemView, useCart(), useProduct()
  • 엔티티를 다루지 않는 컴포넌트와 훅 - Button, useRoute, useEvent 등
  • 엔티티를 다루는 함수와 그렇지 않은 함수 - calculateCartTotal(cart) vs capaitalize(str)

기본과제

  • Component에서 비즈니스 로직을 분리하기

  • 비즈니스 로직에서 특정 엔티티만 다루는 계산을 분리하기

  • 뷰데이터와 엔티티데이터의 분리에 대한 이해

  • entities -> features -> UI 계층에 대한 이해

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • Component에서 사용되는 Data가 아닌 로직들은 hook으로 옮겨졌나요?

  • 주어진 hook의 책임에 맞도록 코드가 분리가 되었나요?

  • 계산함수는 순수함수로 작성이 되었나요?

  • 특정 Entitiy만 다루는 함수는 분리되어 있나요?

  • 특정 Entitiy만 다루는 Component와 UI를 다루는 Component는 분리되어 있나요?

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

심화과제

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

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

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

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

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

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

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

과제 셀프회고

  • 약간 뇌빼고 한 거 같긴 한데...
    너무 욕심 부리지 않고 심화과제까지는 완성한 듯...
  • 큰 것부터 시작해서 작은 것으로 끝나는 흐름으로 완성한 듯 하다.
    (UI부터 시작하여 model 함수까지 다다르는...)

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

  • 저번 과제는 양이 너무 많아서 감당이 안됐는데, 이번 과제는 그래도 어느 정도 핸들링이 돼서 AI에게 부분적으로 물어보고,
    잘 감이 안잡히는 부분은 AI에게 튜터로써 물어볼 수 있었던 점이 좋았습니다.
  • model → hooks → component로 흐르는 흐름을 이번에 처음 작성해봐서 좀 생소했던 점은 있었습니다만,
    어떤 식으로 작성해야 하는지는 알 수 있어서 좋았습니다. 이게 FSD 방식인가라는 생각이 언뜻 들었습니다.

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

  • 이번에는 Context API로 상태 관리를 구현했습니다. coupon, product, cart는 전역에서 사용해야 하기 때문에 Context에 두고, 나머지 상태는 props로 전달해도 충분하다고 판단했습니다. 따라서 Context API는 꼭 필요한 부분에만 적용하고, 그 외에는 props를 통해 값을 전달하도록 했습니다.
  • entities(models) → features(hooks) → UI(components) 구조로 각 계층이 명확한 역할을 가지고 있도록 하여, Models는 입력과 출력이 같은 비지니스 및 계산 로직만, Hooks는 상태 관리를 주로 다루며, Components는 렌더링만 담당하여 단일 책임 원칙에 따르게 하였습니다.
  • 또한 각 Hook이 하나의 도메인만 담당하도록 설계되어 있어서(useCart는 장바구니만, useProducts는 상품만), 기능을 확장하거나 수정할 때 다른 부분에 영향을 받지 않습니다.

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

  • 고정적인 초기 데이터가 있는 것보다는 실제 API나 mock API로 데이터를 실제 끌어와서 작업을 해보면 실무에 도움이 되겠다 생각이 들었습니다.

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

  • 아래 부분같은 경우에는 cart와 product가 혼재되어 있습니다. 이런 경우 모델의 어느쪽으로 가는게 맞는건지 아니면 새로운 모델 파일을 만들어서 혼재 되어 있는 경우에 그 파일에 넣어야할지... 헤깔린 부분 중에 하나기도 합니다. 어떤 기준으로 어떤 파일에 들어가야할까요? (뭔가 product와 cart 모델로 따로 넣기에는 코드 자체가 작아보이기도 하고 그래서 그런가 더 고민된달까요...흠...)
export const getRemainingStock = (
  product: Product,
  cart: CartItem[],
): number => {
  const cartItem = cart.find((item) => item.product.id === product.id)
  const remaining = product.stock - (cartItem?.quantity || 0)

  return remaining
}
  • 아직까지는 그래도 유틸에 들어가는 함수와 도메인에 들어가는 함수가 좀 헤깔리기도 합니다. 예를 들면 아래의 함수가 있다면 이건 어디든 써먹을 수 있는 함수갖기도 하고, 또 언뜻 보면 product와 관련된 함수로 보여서 비지니스 로직으로 봐야할지 잘 모르겠다는 생각이 듭니다. 저 함수가 도메인 함수로 봐야할까요? 아니면 유틸 함수로 봐야할까요.
export const calcRemainingStocks = (stock: number, min: number, max: number) => {
  return stock >= min && stock < max
}
  • 음... 이런 질문을 해도 되는지는 모르겠는데;; 이번 과제는 좀 익숙하기도 하고 양이 적당해서 코드를 갈아엎지 않고 한 것 같은데, 저번 과제인 '클린코드'는 저에게 양도 굉장히 많은 편이고, 썩 익숙한 편도 아니어서도 그런가 코드를 갈아 엎다 보니 완성하지 못했었습니다. 이렇게 많은 양이고 익숙하지 않은 코드인데 시간은 리밋이 있다 이럴 때 어떤 식으로 코드를 작성해야 코드를 갈아엎지 않고 할 수 있을까요?

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 잘 정리하셨고, 엔티티·훅·UI 계층 분리가 명확하게 진행된 점이 인상적입니다. 아래 리뷰는 주어진 변경사항을 중심으로 '기술 요구사항 변화 시나리오 제시 → 변화 시 어떤 영향이 생기는지 → 응집도·결합도 관점에서 개선안' 순으로 정리했습니다. Github PR 코멘트용 마크다운 형태(종합 피드백 + 상세 피드백)로 드립니다.

주의: 코드 일부는 TODO 또는 미완성(주석/힌트/빈 구현)이 남아 있어 그것들을 개선하는 방향으로 제안을 드립니다.


질문에 대한 요약 답변 (PullRequestBody 관련)

  • "getRemainingStock 같은 함수는 어디 소속이 맞을까?"
    → product 정보 + cart 상태를 같이 참조하므로 'cart 도메인' (또는 cart 모델)에 두는 것이 자연스럽습니다. 만약 여러 도메인에서 같은 계산을 자주 쓴다면 cart.getRemainingStock(...) 같은 명명으로 모델에 둡니다.
  • "calcRemainingStocks( stock, min, max ) 는 도메인인가 유틸인가?"
    → 입력이 완전히 일반적(숫자 3개)이고 특정 엔티티(Product/Cart)에 의존하지 않으면 유틸. 하지만 실사용 맥락(상품 재고 판정 로직)이 명확하다면 도메인(상품/재고 모델)에 포함시켜 의미를 더 부여하는 편이죠.
  • "양이 많고 익숙하지 않을 때 어떻게 개발하나?"
    → Vertical slice(작동하는 작은 흐름) 먼저, 작은 API/모듈(모델) 단위로 커밋 & 테스트, 리팩터링 할 때는 public API(함수 시그니처)를 먼저 고정. 점진적 리팩터링 + 자동화(스크립트, linter, 타입 체크) 권장.

다음 섹션에 PR 전체에 대한 종합 피드백과 상세 피드백(개념 정의, 문제, AS-IS/TO-BE 예시)을 드립니다.


종합 피드백

  1. 주요 피드백 키워드
  • 도메인/모델(순수함수) 분리: 잘 되어 있음 (models/*.ts)
  • Context 기반 글로벌 상태 → 추후 상태 라이브러리로 변경 가능성 존재
  • Notification(알림) 결합도: addNotification 을 훅/컴포넌트 인자로 전달하는 패턴이 널리 퍼져 있음(결합도 상승)
  • useLocalStorage 재사용성: 훅으로 추상화되어 있음(좋음) — 다만 저장/삭제 정책 일관성 확인 필요
  • TODO/힌트 코드(미완성) 다수: 배포/패키지화 전 반드시 구현 필요
  • 파일/폴더 응집도: 기능별로 잘 모듈화됨 (components / hooks / models / utils / constants) — 다만 cross-cutting인 notifications / localStorage는 더 명확히 분리 권장
  1. PullRequestBody의 "과제 셀프회고 / 내가 신경 쓴 부분" 피드백 (인사이트 관점)
  • 분리(entities → features → UI) 접근과 Context를 "꼭 필요한 부분에만" 적용한 판단은 매우 좋은 접근입니다. 실제로 전역 상태는 남용하면 결합도를 높이므로 최소한의 전역화 판단은 맞습니다.
  • 이어서 생각해볼 질문:
    • 현재 앱이 커졌을 때(새 도메인 추가, 멀티 페이지 등) 지금의 Context 배치가 계속 유지될지?
    • '꼭 필요한 부분'의 기준을 문서화(README 또는 코드 주석)해두면 다음 리팩터 시 의사결정이 쉬워짐
    • Notification 처리 방식(현재 App이 addNotification을 생성하여 hooks에 주입하는 방식)을 바꿔야 하는 상황(예: 전역 Toast 라이브러리 사용)에서 변환 비용은 어느 정도일지?
  1. PullRequestBody의 "리뷰 받고 싶은 내용(혼재 함수 분류 등)"에 대한 답변
  • getRemainingStock(product, cart) → cart 도메인 함수로 두세요. 함수명도 cart.getRemainingStock(product, cart) 처럼 명확히 도메인을 드러내면 좋습니다.
  • calcRemainingStocks(stock, min, max) → '범용적인 논리(숫자 비교)'라면 utils로 두고, 실제 상품/카트 문맥에서만 쓰이면 도메인 안에 둡니다. 기준 = "동일 로직을 다른 도메인에서도 쓸 가능성"이 핵심입니다.
  • large/unknown task 대응 팁:
    • MVP(동작하는 최소단위) 목표로 먼저 끝내고 리팩터링
    • 각 계층의 public API(훅/모델 함수 시그니처)를 먼저 정의하고 테스트
    • 통합 테스트/유닛 테스트를 조금이라도 작성 — 리팩터링 안전망

상세 피드백 (파일/문제별)

우선 피드백에서 다룰 개념을 정의합니다.

  • 결합도(Coupling): 모듈(파일, 훅, 컴포넌트) 사이에 의존성이 얼마나 강하게 묶여 있는지. 낮을수록 한 부분 변경이 다른 부분에 미치는 영향이 작음. 인터페이스(함수 시그니처, 이벤트 콜백)를 통해 의존을 줄이면 결합도가 낮아짐.
  • 응집도(Cohesion): 하나의 모듈(파일/폴더)이 얼마나 '단일 관심사'를 잘 모아두는가. 응집도가 높을수록 해당 모듈의 수정 범위가 작고 이해하기 쉬움.
    • 실무 기준(질문자의 정의 반영):
      • 변경 시 코드 추가/수정/삭제 동선이 짧은가?
      • 패키지로 떼어낼 때 매끄럽게 분리되는가?

이제 주요 이슈별로 문제 정의 → AS-IS → TO-BE(코드 포함) 방식으로 정리합니다.

항목 A. Notification(알림) 결합도 — addNotification을 여러 훅에 주입

  • 개념 정의: 결합도(나쁜 예). 훅이 UI 의존(알림을 직접 띄우는 함수)을 가지면 재사용/테스트가 어렵습니다.
  • 문제: 여러 훅(useCart, useProducts, useCoupons, useForm 등)에 addNotification을 직접 인자로 전달하고 있음. 이는 알림 구현을 바꾸면(Toast 라이브러리 교체, 서버 사이드 알림 등) 훅들을 모두 수정해야 하는 높은 결합도를 유발합니다.
  • AS-IS (PR 코드 발췌)
    • App에서:
      const { ... } = useProducts(addNotification)
      const { ... } = useCart(addNotification)
      const { coupons, addCoupon } = useCoupons(addNotification, selectedCoupon, setSelectedCoupon)
  • TO-BE (권장) — NotificationContext / useNotification 훅으로 분리
    • 장점: addNotification 구현을 한 곳만 바꾸면 됨, 훅은 알림에 의존하지 않고 결과(성공/에러)를 반환하도록 변경하여 호출자가 알림을 띄우도록 함.
    • 예시 코드 (간단화):

AS-IS (현재)

// App.tsx
const addNotification = useCallback((msg,type='success') => { setNotifications(prev => [...]) }, [])
const { addToCart } = useCart(addNotification)

TO-BE (제안)

// notification/NotificationProvider.tsx
export const NotificationContext = createContext<{ add: (msg:string,type?:string)=>void }>(...)
export const useNotification = () => useContext(NotificationContext)

// App.tsx
<NotificationProvider>
  <AppChildren />
</NotificationProvider>

// useCart.ts (훅 내부)
import { useNotification } from '../notification/NotificationProvider'
function useCart() {
  const { add } = useNotification()
  // 내부에서 add(...)를 호출하거나
  // 더 권장: 훅은 결과 객체/예외를 반환 → 호출자가 add() 호출
}
  • 추가 권장: 훅 내부는 가능한 한 예외(throw) 또는 반환 값(result { ok, message })로 상태를 알려주고 UI(또는 호출자)가 알림을 담당하게 하세요.
  • 예시: useAddProduct로부터 addProductResult를 받아서 UI가 알림 처리.

항목 B. 상태관리 라이브러리 변경 시 영향(시나리오) — 예: Context → Zustand / Jotai / redux / tanstack-query

  • 요약: 현재 구조는 Context Providers + useLocalStorage + useState 기반. 전환 난이도는 '훅 단위의 래핑(bridge)'가 잘되어 있느냐에 따라 달라집니다. 현재 훅(useCart, useProducts, useCoupons)은 상태를 캡슐화하고 있어 전환 비용을 낮춰줍니다(좋음). 다만 addNotification과 같은 cross-cutting concern이 인자로 많이 전달되는 부분은 제거해야 전환이 매끄럽습니다.

시나리오 1) Zustand로 전환

  • 변경 포인트:
    • 구현: src/advanced/hooks/useCart.ts, useProducts.ts, useCoupons.ts 내부 구현을 Zustand store API로 교체.
    • App: context providers 제거(또는 유지하되 내부에 store 주입 생략). 컴포넌트는 useCartContext() 대신 useCartStore(state => state.cart) 등을 사용.
    • useLocalStorage: Zustand 미들웨어(persist)를 사용하면 로컬스토리지 연동을 한 곳에서 처리 가능(좋음).
  • 예상 작업량: 중간. models(순수함수) 변경 없음. UI 컴포넌트(Provider 사용 부분) 일부 변경.
  • 예시 TO-BE(간단)
// store/cartStore.ts
import create from 'zustand'
import { persist } from 'zustand/middleware'
import { addItemToCart as pureAddItem } from '../models/cart'

export const useCartStore = create(persist((set,get)=>({
  cart: [],
  selectedCoupon: null,
  addToCart: (product) => set(state => {
    const res = pureAddItem(state.cart, product)
    // res.carts, res.message, res.type
    return { cart: res.carts }
  }),
  // updateQuantity, removeFromCart, calculateTotals ... 등
}), { name: 'my-app-cart' }))
  • 컴포넌트 사용:
const cart = useCartStore(s => s.cart)
const addToCart = useCartStore(s => s.addToCart)

시나리오 2) TanStack Query (주로 원격 데이터 캐싱)

  • TanStack Query는 로컬 클라이언트 상태관리보다는 서버 state 관리에 강합니다. 상품 목록/쿠폰을 원격 API로 바꾸는 경우, getProducts 등을 useQuery로 바꿉니다. 로컬 카트는 로컬 상태(Zustand / Context)로 유지하는 하이브리드 패턴 권장.
  • 예상 작업량: 상품/쿠폰을 API로 바꾸는 경우 크다. 모델/훅 계층이 API 호출을 추상화하고 있다면 전환이 훨씬 쉬움.

시나리오 3) Jotai

  • Jotai atom으로 도메인 상태를 정의하고 훅에 atom 접근을 래핑하는 방식. Context 제거 가능. 모델은 그대로 재사용 가능. Jotai는 컴포넌트별 구독 성능에 유리.
  • 예시:
// atoms/cartAtoms.ts
export const cartAtom = atom<CartItem[]>([])
export const addToCartAtom = atom(null, (get, set, product) => {
  const carts = get(cartAtom)
  const res = addItemToCart(carts, product)
  set(cartAtom, res.carts)
})
// useCart 훅은 Jotai hook을 래핑

결론: 훅 계층이 잘 추상화되어 있으니 상태관리 라이브러리 전환은 '훅 내부 구현'을 바꾸는 수준으로 좁히면 됩니다. App/컴포넌트 레벨에서 provider 제거/변경 작업 및 notifications 분리(앞서 제안)는 필수로 권장합니다.


항목 C. 패키지화(모듈화) — 어떤 것이 잘 분리되는가? 응집도 관점

  • 응집도 정의(요청하신 관점):
    • 변경에 대한 동선이 짧은가? (한 변경 시 수정해야 할 파일이 적은가)
    • 패키지로 떼어낼 때 의존성이 명확한가?
  • 현재 상태(평가):
    • 모델(models/*)들은 순수함수로 작성되어 응집도 높음(패키지로 떼기 쉬움) — 좋은 점.
    • hooks/* 는 도메인 로직 + 로컬 저장(persist) + 알림 호출을 섞는 경우가 있어(특히 addNotification 주입) 완전한 패키지화 시 알림 부분을 분리해야 함.
    • components/* 는 UI 전용으로 잘 분리됨.
    • types/context.ts 는 hooks와 components간 계약(인터페이스)을 제공하고 있어 패키지 경계 정의에 유리.
  • 권장 패키지 분해 (예)
    • @myorg/domain-cart (models/cart.ts + types related to Cart) — 순수 비즈니스 로직 + 타입
    • @myorg/domain-product (models/product.ts + constants + types)
    • @myorg/hooks (thin adapters — useCart/useProducts, 이 레이어는 앱 특화: 이 패키지는 앱에 남겨두거나 내부 패키지화)
    • @myorg/ui (재사용 가능한 UI 컴포넌트) — 필요시
  • 패키지화 시 고려사항:
    • 외부 의존성(react)은 peerDependencies로 설정
    • side-effect(로컬스토리지 접근, window 등)는 패키지에서 제거하고 앱에서 adapter를 주입하도록 설계
    • public API(훅/모델 함수 시그니처)를 문서화 & 테스트

AS-IS → TO-BE (모델 패키지 예시)

  • AS-IS:
    import { calculateItemTotal } from '../models/cart'
  • TO-BE:
    // domain-cart 패키지
    import { calculateItemTotal } from '@myorg/domain-cart'

그리고 domain-cart는 node/npm으로 배포 가능(순수함수, 타입 포함).


항목 D. 구체적 코드 품질 이슈(작은 것들)

  1. useLocalStorage 훅: 현재 구현은 잘 되어 있으나 업데이트 시 인자 타입 판별을 런타임 typeof function 체크로 하고 있음. 문제 없음(실무에서도 흔한 패턴) — 다만 래핑된 함수에서 setState와 localStorage 동기화를 더 확실히 보장하세요(비동기 setState 이슈).
  2. useCart.updateQuantity 시그니처: 현재 정의는 (productId, newQuantity, products) — 훅에서 products를 받아 처리하는 편이 더 깔끔합니다. 외부에서 products를 넘기는 것은 결합을 증가시킴.
    • AS-IS:
      updateQuantity(productId, newQuantity, products)
    • TO-BE:
      updateQuantity(productId, newQuantity) // useCart가 내부적으로 products 접근
  3. constants / magic numbers: 여러 파일에서 10000, 9999 등의 숫자 사용. constants에 통일해 두어 관리하세요.
  4. TODO / 미완성 코드: models/* 및 hooks/* 에 TODO 주석이 남아있음. 패키지화/배포 전에 반드시 구현 & 유닛 테스트 필요.

구체적 AS-IS / TO-BE 코드 예시 (핵심 개선 포인트)

  1. Notification 분리 (AS-IS → TO-BE)

AS-IS (현재, PR 발췌)

// App.tsx
const addNotification = useCallback((message, type='success') => {
  // setNotifications...
}, [])

const { cart, addToCart } = useCart(addNotification)
const { products, addProduct } = useProducts(addNotification)

TO-BE (NotificationContext 사용 — 더 낮은 결합)

// notification/NotificationProvider.tsx
export const NotificationContext = createContext({ add: (m,t) => {} })
export function NotificationProvider({children}) {
  const [list,setList] = useState([])
  const add = (message, type='success') => { /* push with timeout */ }
  return <NotificationContext.Provider value={{add}}>{children}</...>
}
export const useNotification = () => useContext(NotificationContext)

// useCart.ts (내부)
import { useNotification } from '../notification/NotificationProvider'
export function useCart() {
  const { add } = useNotification()
  const addToCart = (product) => {
    // 비즈니스 로직 수행
    // add('장바구니에 담겼습니다', 'success')  // 필요시 호출
  }
  // 또는: 훅은 결과를 반환하고 호출자가 add 호출 (더 낮은 결합)
  return { addToCart, ... }
}
  1. 상태관리 변경(예: Zustand로 바꾸는 예시)

AS-IS (Context + Provider)

<CartContext.Provider value={{ cart, addToCart, ... }}>
  <ProductsContext.Provider value={{ products, addProduct, ... }}>
    <AppChildren />
  </ProductsContext.Provider>
</CartContext.Provider>

TO-BE (Zustand) — App에서 Provider 제거 가능

// stores/cartStore.ts
export const useCartStore = create(set => ({
  cart: [],
  addToCart: (product) => set(state => {
    const res = addItemToCart(state.cart, product) // models/cart 순수함수
    // optionally: notify via global notification hook
    return { cart: res.carts }
  }),
  ...
}))

// Component
const cart = useCartStore(s => s.cart)
const addToCart = useCartStore(s => s.addToCart)

장점: 로컬 저장(persist) 같은 cross-cutting 기능은 zustand middleware로 한 번에 처리 가능.

  1. useAddProduct 인터페이스 개선 (결합도 개선)
  • 안 좋은 예(결합): useAddProduct(addNotification)
  • 좋은 예(느슨한 결합): useAddProduct({ onSuccess, onError }) 또는 useAddProduct()가 결과값을 반환하고 호출자가 알림/처리를 담당

AS-IS

const useAddProduct = (addNotification) => {
  return { addProduct: (p) => { /* addNotification('성공') */ } }
}

TO-BE

const useAddProduct = () => {
  const addProduct = (p) => {
    // perform
    return { ok: true, message: '상품 추가됨' }
  }
  return { addProduct }
}

// 호출부
const { addProduct } = useAddProduct()
const res = addProduct(product)
if (!res.ok) showError(res.message)
else showSuccess(res.message)

최종 권장 액션 리스트 (우선순위 순)

  1. Notification 분리 (NotificationContext / useNotification) — 즉시 권장. addNotification 인자 주입 제거.
  2. 훅과 store 간 경계 정리 — useCart/useProducts/useCoupons가 "앱-특화 어댑터"인지, "도메인 로직 포함"인지 결정. 모델(순수함수)은 그대로 유지하고 훅은 store/adapter만 담당하도록 리팩터.
  3. useLocalStorage의 책임 정리(혹은 store 미들웨어로 통합) — 중복 로직 정리.
  4. updateQuantity 시그니처 정리: products param 제거, 훅 내부에서 products 접근.
  5. 남아있는 TODO/힌트 코드 구현 및 유닛 테스트 추가 (models 특히).
  6. 패키지화 계획: models는 독립 패키지로 떼기 쉬움 — 먼저 domain-cart 패키지로 분리해보세요.
  7. 문서화: 각 훅/모델의 public API(시그니처), 변경 기준(언제 Context를 쓰는가?)을 README에 기록.

필요하시면 다음 단계로

    1. NotificationContext 실제 코드 패치 PR 샘플을 만들어 드리거나
    1. useCart → zustand 마이그레이션 패치 예시를 만들어 드릴 수 있습니다.

원하시는 우선순위 알려주세요. 어떤 부분(예: 알림 분리 / Zustand 전환 / 패키지화 샘플)에 대해 실제 코드 패치를 바로 보고 싶으신가요?

Copy link

@unseoJang unseoJang left a comment

Choose a reason for hiding this comment

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

안녕하세요 민지님~
2-2과제도 수고가 많았습니다.
시간이 없는데도 불구하고 심화과제까지 진행하느라 고생이 많으셨네요

전체적으로 작업해주신 좋습니다.
허나 util 함수에서의 비지니스 로직과 도메인 로직이 겹쳐 있는 부분,
util 함수에서의 시간복잡도 계산이 안되어 복잡도 계산도가 최악으로 가는 부분,
전체적인 정책의 일관성,
최소 단위의 컴포넌트 분리,
등등 아직은 손볼수 있는 부분이 많이 보이네요
항해가 모두 다 끝나고 반드시 한번더 과제를 여유있게 진행한다면 훨씬 좋은 결과물을 얻을 수 잇다고 생각이 드네요
다른분들 것들도 참고해서 어떻게 디자인 패턴과 프로그래밍을 활용했는지 확인하면서 작업하면 민지님에게 더 좋은 영향으로 과제가 남을거에요

그래도 타입스크립트를 써서 과제 진행도 해주셧고 나름의 기준으로 디렉토리 라인을 나눠주신 부분은 잘하셧어요!
꼭 복습을 권장드려요!

onClick={() => toggleShowCouponForm()}
className="text-gray-400 hover:text-gray-600 flex flex-col items-center"
>
<svg

Choose a reason for hiding this comment

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

이런 도메인 없는 SVG 파일은 따로 관리하는 편이 좋아요

Choose a reason for hiding this comment

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

혹시...AdminPage 자체를 컴포넌트 디렉토리에 넣어둔 이유를 알고싶어요
제 생각엔 AdminPage나 CartPage나 되게 큰 Page 라인업에 들어가야되는 파일이라고 생각이 들거든요
시간이 되시면 제 질문에 한번 적어보는 것도 좋고 도메인에 대한 이유를 한번 고민해봐 주셨으면 싶어요
이 부분은 AI가 할수 있다 라기 보다는 사람이 도메인 라인구성을 할떄 한번쯤 고민해보고 작업해야되는 부분이라고 생각이 들거든요

)}
</section>

{cart.length > 0 && (

Choose a reason for hiding this comment

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

여기서 삼항연산자로 import 되는 컴포넌트들은 묶어서 따로 관리해야지 가독성이 더 좋아질것 같아요

@@ -0,0 +1,197 @@
// 힌트:

Choose a reason for hiding this comment

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

주석에 본인이 진행해야할 부분을 적고 진행하는 부분이 너무 좋네요

}
}

export const addItemToCart = (

Choose a reason for hiding this comment

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

해당 함수는 비즈니스 코드와 UI 타입/문구가 혼합되어 있어서 주석 달아주신것과 다르게 혼용된 메서드 같아요, product: ProductWithUI, 타입을 이렇게 받으면 원래 목적과도 다르네요

const cartItem = cart.find((item) => item.product.id === product.id)
const remaining = product.stock - (cartItem?.quantity || 0)

return remaining

Choose a reason for hiding this comment

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

해당 값 음수 반환의 위험이 있어요
Math.max로 음수 반환할수 있는 가능성을 없애야 할것 같아요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants