stack-analysis
CHAPTER 73 / 90
읽기 약 2분
FUNCTION
장바구니와 체크아웃 흐름
핵심 개념
cart 저장·세션·체크아웃 단계·재고 검증 — 이탈률 30% 감소.
본문
장바구니 저장 전략
[비로그인]
- localStorage / cookie
- merge on login
[로그인]
- DB 저장
- 디바이스 간 동기화장바구니 데이터 모델
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
// 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 } });
}체크아웃 단계 (이탈 최소화)
1. 장바구니 (Cart) → 체크아웃 시작 (Checkout)
2. 배송지 입력 (or 저장된 주소 선택)
- 주소 검색 API (Daum 우편번호 등)
- 신규 입력 시 자동 저장
3. 배송 옵션 (일반/빠른배송) + 요청사항
4. 쿠폰·포인트 적용
5. 결제 수단 선택 + 결제
6. 주문 완료 → /orders/:id
[중요]
- 단계마다 URL 다름 (뒤로가기 가능)
- "주문서 없이 결제" — 비로그인 가능
- 진행 표시 (4/5)체크아웃 페이지
'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)
// 결제 시작 시 재고 임시 차감 (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