🛒 E-commerce Stack
이커머스 플랫폼을 위한 검증된 기술 스택 - 결제, 재고, 주문 관리 최적화
🛒 E-commerce Stack
이커머스 플랫폼을 위한 검증된 기술 스택 결제, 재고, 주문 관리까지 - 빠르게 시작하고 확장 가능하게
🎯 이 스택이 적합한 경우
✅ 추천하는 경우
- 커스텀 이커머스 플랫폼 구축
- Shopify 이상의 커스터마이징 필요
- 헤드리스 커머스 아키텍처 원함
- D2C 브랜드 또는 마켓플레이스
- 국제화 및 다중 통화 지원 필요
❌ 다른 스택을 고려해야 할 경우
- 빠른 출시가 최우선 → Shopify, WooCommerce
- 소규모 판매 → Gumroad, Lemon Squeezy
- 복잡한 B2B 커머스 → Salesforce Commerce
🧩 핵심 구성요소
🖥️ Frontend
Framework: Next.js 14 (App Router) Language: TypeScript Styling: Tailwind CSS Components: shadcn/ui State: Zustand (장바구니) Images: next/image + Cloudinary 🛍️ Commerce Engine (선택)
| 옵션 | 특징 | 추천 규모 | | ---------------------- | ---------------- | --------- | | Medusa.js | 오픈소스, 풀기능 | 중~대규모 | | Saleor | GraphQL 기반 | 대규모 | | Shopify Storefront | 헤드리스 API | 모든 규모 | | Custom (Prisma) | 완전 제어 | 소규모 |
💳 결제 시스템
Primary: Stripe (카드, Apple Pay, Google Pay) Korea: 토스페이먼츠 / 아임포트 Crypto: Coinbase Commerce (옵션) Tax: Stripe Tax / TaxJar 🗄️ Backend & Database
Database: PostgreSQL (Supabase/PlanetScale) ORM: Prisma Cache: Redis (Upstash) Search: Algolia / Meilisearch Queue: Inngest / BullMQ 💰 비용 예상
MVP 단계 (월 주문 100건)
| 항목 | 서비스 | 비용 | | ----------- | ----------------- | ------------------------ | | 호스팅 | Vercel | $0-20 | | DB | Supabase Pro | $25 | | 이미지 | Cloudinary | $0 | | 검색 | Meilisearch Cloud | $0-30 | | 이메일 | Resend | $0-20 | | 결제 수수료 | Stripe 2.9%+30¢ | 변동 | | 합계 | | ~$75/월 + 결제수수료 |
성장 단계 (월 주문 1000건)
| 항목 | 서비스 | 비용 | | -------- | ------------ | ------------------------- | | 호스팅 | Vercel Pro | $20 | | DB | Supabase Pro | $25 | | 이미지 | Cloudinary | $89 | | 검색 | Algolia | $35 | | Redis | Upstash | $10 | | 합계 | | ~$180/월 + 결제수수료 |
🛠️ 프로젝트 구조
ecommerce-app/ ├── app/ │ ├── (shop)/ # 쇼핑 페이지 │ │ ├── products/ │ │ │ ├── [slug]/page.tsx │ │ │ └── page.tsx │ │ ├── cart/page.tsx │ │ ├── checkout/page.tsx │ │ └── orders/page.tsx │ ├── (admin)/ # 관리자 대시보드 │ │ ├── products/ │ │ ├── orders/ │ │ └── analytics/ │ └── api/ │ ├── webhooks/stripe/ │ └── checkout/ ├── components/ │ ├── product/ │ │ ├── ProductCard.tsx │ │ ├── ProductGallery.tsx │ │ └── AddToCart.tsx │ ├── cart/ │ │ ├── CartDrawer.tsx │ │ └── CartItem.tsx │ └── checkout/ ├── lib/ │ ├── stripe.ts │ ├── cart-store.ts │ └── db/ └── prisma/ └── schema.prisma 🗃️ 데이터 모델
Prisma Schema (핵심)
model Product { id String @id @default(cuid()) name String slug String @unique description String? price Int // cents로 저장 comparePrice Int? // 할인 전 가격 images String[] // URL 배열 inventory Int @default(0) status ProductStatus @default(DRAFT) categories Category[] variants ProductVariant[] createdAt DateTime @default(now()) } model Order { id String @id @default(cuid()) orderNumber String @unique userId String? email String status OrderStatus @default(PENDING) items OrderItem[] subtotal Int shipping Int tax Int total Int shippingAddress Json createdAt DateTime @default(now()) } model OrderItem { id String @id @default(cuid()) orderId String order Order @relation(fields: [orderId], references: [id]) productId String product Product @relation(fields: [productId], references: [id]) quantity Int price Int // 주문 시점 가격 저장 } 🛒 장바구니 구현
Zustand 스토어
// lib/cart-store.ts import { create } from "zustand"; import { persist } from "zustand/middleware"; interface CartItem { productId: string; quantity: number; price: number; name: string; image: string; } interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (productId: string) => void; updateQuantity: (productId: string, quantity: number) => void; clearCart: () => void; total: () => number; } export const useCartStore = create<CartStore>()( persist( (set, get) => ({ items: [], addItem: (item) => set((state) => { const existing = state.items.find( (i) => i.productId === item.productId, ); if (existing) { return { items: state.items.map((i) => i.productId === item.productId ? { ...i, quantity: i.quantity + 1 } : i, ), }; } return { items: [...state.items, { ...item, quantity: 1 }] }; }), // ... other methods total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0), }), { name: "cart-storage" }, ), ); 💳 결제 플로우
Stripe Checkout (권장)
// app/api/checkout/route.ts import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const { items } = await req.json(); const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: items.map((item: any) => ({ price_data: { currency: "krw", product_data: { name: item.name, images: [item.image], }, unit_amount: item.price, }, quantity: item.quantity, })), mode: "payment", success_url: `${process.env.NEXT_PUBLIC_URL}/orders/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`, }); return Response.json({ url: session.url }); } Webhook 처리
// app/api/webhooks/stripe/route.ts export async function POST(req: Request) { const payload = await req.text(); const sig = req.headers.get("stripe-signature")!; const event = stripe.webhooks.constructEvent( payload, sig, process.env.STRIPE_WEBHOOK_SECRET!, ); switch (event.type) { case "checkout.session.completed": // 주문 생성 await createOrder(event.data.object); break; case "payment_intent.succeeded": // 결제 완료 처리 break; } return Response.json({ received: true }); } 🔍 검색 & 필터링
Meilisearch 설정
// lib/search.ts import { MeiliSearch } from "meilisearch"; const client = new MeiliSearch({ host: process.env.MEILISEARCH_HOST!, apiKey: process.env.MEILISEARCH_API_KEY, }); // 상품 인덱싱 export async function indexProducts(products: Product[]) { await client.index("products").addDocuments(products); } // 검색 export async function searchProducts(query: string, filters?: string) { return client.index("products").search(query, { filter: filters, facets: ["category", "price_range"], }); } 📦 재고 관리
재고 차감 로직
// 주문 생성 시 재고 차감 (트랜잭션) await prisma.$transaction(async (tx) => { for (const item of orderItems) { const product = await tx.product.update({ where: { id: item.productId }, data: { inventory: { decrement: item.quantity }, }, }); if (product.inventory < 0) { throw new Error(`Insufficient inventory for ${product.name}`); } } await tx.order.create({ data: orderData }); }); 🇰🇷 한국 결제 연동
토스페이먼츠
// 토스페이먼츠 결제 위젯 (프론트엔드) const tossPayments = TossPayments(clientKey); await tossPayments.requestPayment("카드", { amount: 50000, orderId: "ORDER_123", orderName: "상품명", successUrl: `${window.location.origin}/success`, failUrl: `${window.location.origin}/fail`, }); 아임포트 (포트원)
// 아임포트 통합 결제 IMP.request_pay( { pg: "kakaopay", pay_method: "card", merchant_uid: `order_${Date.now()}`, name: "상품명", amount: 50000, buyer_email: "buyer@example.com", buyer_name: "구매자", buyer_tel: "010-1234-5678", }, callback, ); 📊 분석 & 추적
이커머스 이벤트
// 상품 조회 analytics.track("Product Viewed", { product_id: product.id, product_name: product.name, price: product.price, category: product.category, }); // 장바구니 추가 analytics.track("Product Added", { product_id: product.id, quantity: 1, cart_id: cartId, }); // 구매 완료 analytics.track("Order Completed", { order_id: order.id, total: order.total, products: order.items, }); 🚀 추천 스타터
오픈소스 보일러플레이트
- Medusa.js - 풀 기능 오픈소스 커머스
- Saleor - 헤드리스 GraphQL 커머스
- Next.js Commerce - Vercel 공식
SaaS 플랫폼
- Shopify Storefront API - 가장 안정적
- BigCommerce - 엔터프라이즈급
- Swell - 헤드리스 특화
🔗 관련 콘텐츠
이커머스 스택 관련 질문은 Issues에 남겨주세요!