eunsoolib
Lab (도메인 모델·게임)

shopping

XState 기반 장바구니·쿠폰·주문 도메인 모델

쇼핑몰의 장바구니 → 주문서 흐름을 작은 단위로 쪼개 본 설계 실험입니다. 같은 장바구니 규칙(상품 id 기준, 최대 3개)을 XState 머신(cartMachine)과 클래스(CartManager) 두 방식으로 구현했고, 적용 쿠폰(CouponManager)과 주문서의 선택·수량(OrderManager)은 별도 매니저로 둡니다. 가격·할인 계산은 포함하지 않습니다.

설치

pnpm add @cbcruk/shopping xstate

xstate(^5)는 패키지 의존성이지만, cartMachine을 실행하려면 앱에서 createActor를 직접 import해야 하므로 함께 설치합니다. 매니저 클래스만 쓴다면 필요 없습니다.

사용법

XState 머신

import { createActor } from 'xstate'
import { cartMachine } from '@cbcruk/shopping'

const actor = createActor(cartMachine).start()

actor.send({ type: 'ADD', params: { product: { id: 'p1' } } })
actor.send({ type: 'DELETE', params: { id: 'p1' } })
actor.send({ type: 'RESET' })

actor.getSnapshot().context.items // Map<string, { id: string }>

매니저 클래스

import { CartManager, CouponManager, OrderManager } from '@cbcruk/shopping'

const cart = new CartManager()
cart.add({ id: 'p1' })
cart.add({ id: 'p2' })

const order = new OrderManager()
order.toggleCheck('p1')
order.setQty('p1', 2)

const coupon = new CouponManager()
coupon.setCoupon({ code: 'DISCOUNT10' })

// localStorage 등에 보관 후 복원
const restored = CartManager.fromJSON(JSON.stringify(cart))
const restoredOrder = OrderManager.fromJSON(JSON.stringify(order))
const restoredCoupon = CouponManager.fromJSON(JSON.stringify(coupon))

API

cartMachine

단일 active 상태 머신. context는 { items: Map<string, { id: string }>, maxCount: 3 }이며 actor마다 새로 만들어집니다. 이벤트를 처리할 때마다 items를 새 Map으로 교체하므로 이전 스냅샷은 바뀌지 않고, useSelector 같은 참조 비교 셀렉터가 변경을 감지합니다.

이벤트동작
{ type: 'ADD', params: { product } }담기. 같은 id는 덮어씀. 새 iditems.size < 3일 때만 (guard)
{ type: 'DELETE', params: { id } }id로 빼기
{ type: 'RESET' }Map으로 비우기

한도에 걸린 새 idADD는 에러 없이 무시됩니다.

new CartManager(initialItems?)

initialItemsMap<CartProductId, CartProduct>이며 복사해서 보관합니다.

메서드설명
add(product)담기. 같은 id는 덮어씀. 이미 3개인데 새 idError를 던짐
delete(id)빼기. 없는 id는 무시
getItems()CartProduct[]
toJSON()[id, product][]
CartManager.fromJSON(data)JSON.stringify(cart) 문자열로부터 복원

타입: CartProduct = { id: CartProductId }, CartProductId = string.

new CouponManager(initialCoupon = null)

쿠폰 하나만 보관합니다. setCoupon(coupon)(교체) / resetCoupon() / getCoupon()(없으면 null) / toJSON()(쿠폰 값 또는 null) / CouponManager.fromJSON(data)(JSON.stringify(coupon) 문자열로부터 복원, 빈 문자열이면 미적용). 다른 매니저와 같이 JSON.stringify(coupon)으로 직렬화합니다. Coupon 타입은 {}로, 필드를 강제하지 않습니다.

new OrderManager(checked?, qty?)

상품 id별 선택 여부(Map<CartProductId, boolean>)와 수량(Map<CartProductId, number>)을 보관합니다.

메서드설명
toggleCheck(id)선택 여부 반전 (기본 false)
isChecked(id)선택 여부
setQty(id, value)수량 지정 (값 검증 없음)
getQty(id)수량 (기본 1)
toJSON(){ checked: [id, boolean][], qty: [id, number][] }
OrderManager.fromJSON(json)JSON.stringify(order) 문자열로부터 복원

설계 노트

  • 네 구성 요소는 서로를 참조하지 않습니다. OrderManagerCartProductId 타입만 공유할 뿐 장바구니에 실제로 담긴 상품인지 확인하지 않습니다.
  • 두 구현 모두 한도는 서로 다른 id의 개수에만 적용되어, 한도에 도달해도 이미 담긴 id를 다시 담으면(덮어쓰기) 성공합니다. 새 id가 한도를 넘을 때만 처리 방식이 다른데, 이벤트를 보낸 쪽에 예외를 돌려줄 수 없는 머신은 guard로 무시하고 CartManager는 예외를 던집니다.
  • fromJSON은 입력을 검증하지 않아, 복원 시에는 최대 개수 제한도 적용되지 않습니다.

On this page