🛒 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에 남겨주세요!