OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 73 / 90
stack-analysis
CHAPTER 73 / 90
읽기 약 2
FUNCTION

장바구니와 체크아웃 흐름


핵심 개념

cart 저장·세션·체크아웃 단계·재고 검증 — 이탈률 30% 감소.

본문

장바구니 저장 전략

📋 코드 (7줄)
[비로그인]
- localStorage / cookie
- merge on login

[로그인]
- DB 저장
- 디바이스 간 동기화

장바구니 데이터 모델

SQL📋 코드 (17줄)
CREATE TABLE carts (
  id UUID PRIMARY KEY,
  user_id UUID UNIQUE,    -- 로그인 사용자 (1인 1cart)
  session_id VARCHAR UNIQUE,  -- 비로그인 (cookie)
  expires_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE cart_items (
  id UUID PRIMARY KEY,
  cart_id UUID NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
  variant_id UUID NOT NULL,
  quantity INT NOT NULL CHECK (quantity > 0),
  added_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (cart_id, variant_id)
);

장바구니 API

TYPESCRIPT📋 코드 (57줄)
// POST /api/cart/items
async function addToCart(userId: string | null, sessionId: string | null, variantId: string, quantity: number) {
  const variant = await db.productVariant.findUnique({
    where: { id: variantId },
    include: { product: true },
  });
  if (!variant || variant.product.status !== 'active') {
    throw new NotFoundError('Product');
  }
  if (variant.stockQuantity < quantity) {
    throw new ConflictError('Insufficient stock');
  }

  // cart 찾거나 생성
  let cart = userId
    ? await db.cart.findUnique({ where: { userId } })
    : await db.cart.findUnique({ where: { sessionId: sessionId! } });

  if (!cart) {
    cart = await db.cart.create({
      data: userId ? { userId } : { sessionId: sessionId! },
    });
  }

  // 이미 있으면 quantity 증가
  return db.cartItem.upsert({
    where: { cartId_variantId: { cartId: cart.id, variantId } },
    create: { cartId: cart.id, variantId, quantity },
    update: { quantity: { increment: quantity } },
  });
}


// 로그인 시 merge
async function mergeGuestCart(userId: string, sessionId: string) {
  const guestCart = await db.cart.findUnique({
    where: { sessionId },
    include: { items: true },
  });
  if (!guestCart) return;

  let userCart = await db.cart.findUnique({ where: { userId } });
  if (!userCart) {
    userCart = await db.cart.create({ data: { userId } });
  }

  // 각 item을 user cart에 merge
  for (const item of guestCart.items) {
    await db.cartItem.upsert({
      where: { cartId_variantId: { cartId: userCart.id, variantId: item.variantId } },
      create: { cartId: userCart.id, variantId: item.variantId, quantity: item.quantity },
      update: { quantity: { increment: item.quantity } },
    });
  }

  await db.cart.delete({ where: { id: guestCart.id } });
}

체크아웃 단계 (이탈 최소화)

📋 코드 (19줄)
1. 장바구니 (Cart) → 체크아웃 시작 (Checkout)

2. 배송지 입력 (or 저장된 주소 선택)
   - 주소 검색 API (Daum 우편번호 등)
   - 신규 입력 시 자동 저장

3. 배송 옵션 (일반/빠른배송) + 요청사항

4. 쿠폰·포인트 적용

5. 결제 수단 선택 + 결제

6. 주문 완료 → /orders/:id


[중요]
- 단계마다 URL 다름 (뒤로가기 가능)
- "주문서 없이 결제" — 비로그인 가능
- 진행 표시 (4/5)

체크아웃 페이지

TSX📋 코드 (48줄)
'use client';

export default function CheckoutPage() {
  const [step, setStep] = useState<'shipping' | 'payment' | 'review'>('shipping');
  const [data, setData] = useState({
    address: null,
    shippingMethod: 'standard',
    couponCode: '',
    paymentMethod: 'card',
  });

  const cart = useQuery({ queryKey: ['cart'], queryFn: fetchCart });
  const summary = useMemo(() => calculateSummary(cart.data?.items ?? [], data), [cart, data]);

  const placeOrder = useMutation({
    mutationFn: () => fetch('/api/checkout/place-order', {
      method: 'POST',
      body: JSON.stringify({ ...data, items: cart.data?.items }),
    }).then(r => r.json()),
    onSuccess: (order) => {
      // PG 결제 페이지로
      window.location.href = order.paymentUrl;
    },
  });

  return (
    <div className="grid md:grid-cols-3 gap-8">
      <div className="md:col-span-2">
        <ProgressIndicator step={step} />

        {step === 'shipping' && <ShippingForm onNext={() => setStep('payment')} />}
        {step === 'payment' && <PaymentForm onNext={() => setStep('review')} />}
        {step === 'review' && (
          <OrderReview
            data={data}
            summary={summary}
            onConfirm={() => placeOrder.mutate()}
            isLoading={placeOrder.isPending}
          />
        )}
      </div>

      <aside className="sticky top-4">
        <OrderSummary cart={cart.data} summary={summary} />
      </aside>
    </div>
  );
}

재고 예약 (reservation)

TYPESCRIPT📋 코드 (30줄)
// 결제 시작 시 재고 임시 차감 (15분)
async function reserveInventory(orderId: string, items: CartItem[]) {
  return db.$transaction(async (tx) => {
    for (const item of items) {
      await tx.inventoryReservation.create({
        data: {
          orderId,
          variantId: item.variantId,
          quantity: item.quantity,
          expiresAt: new Date(Date.now() + 15 * 60 * 1000),
        },
      });
    }
  });
}


// 가용 재고 = 실제 - 예약
async function getAvailableStock(variantId: string) {
  const variant = await db.productVariant.findUnique({ where: { id: variantId } });
  const reserved = await db.inventoryReservation.aggregate({
    where: { variantId, expiresAt: { gt: new Date() } },
    _sum: { quantity: true },
  });
  return variant!.stockQuantity - (reserved._sum.quantity ?? 0);
}


// 결제 성공 시 → 실제 차감 + 예약 해제
// 결제 실패/만료 시 → 예약만 해제

다음 챕터

CH.74 "결제 PG 연동: 토스페이먼츠 실전".


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 프로젝트의 체크아웃 흐름 부분을 분석해서
실전 분석 + 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

체크아웃 흐름 관련 실제 서비스 5개를
비교 분석해서 패턴 추출를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 코드베이스에서 체크아웃 흐름
최적화 가능 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 풀스택 시장의
체크아웃 흐름 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
장바구니와 체크아웃 흐름 이 3가지만 확실히 잡으세요
1.비로그인 cart는 cookie session_id, 로그인 시 merge
2.체크아웃은 단계별 URL — 뒤로가기 + 진행 표시 가능
3.재고 예약 패턴 = 결제 시작 시 임시 차감 (15분), 성공 시 실제 차감


공유하기
진행도 73 / 90