Files
2nd/10_Wiki/Topics/AI_and_ML/전자상거래 플랫폼.md
T
2026-05-10 22:08:15 +09:00

270 lines
8.1 KiB
Markdown

---
id: wiki-2026-0508-전자상거래-플랫폼
title: 전자상거래 플랫폼
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [E-commerce Platform, Shopify, headless commerce, 커머스 플랫폼]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [ecommerce, platform, headless, shopify, checkout]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
language: typescript
framework: shopify-hydrogen-medusa
---
# 전자상거래 플랫폼
## 매 한 줄
> **"매 monolith 의 Shopify vs headless 의 Medusa/Saleor 의 build vs buy spectrum 결정"**. 2026 의 ecommerce 는 SaaS (Shopify, BigCommerce), Composable/MACH (commercetools, Medusa, Saleor), AI-native (agentic shopping, conversational commerce) 3축으로 분기. 매 plat 선택은 catalog scale, custom logic 양, dev resource, 매 international tax 의 4 변수 함수.
## 매 핵심
### 매 platform 분류
- **SaaS Monolith**: Shopify (Plus), BigCommerce, Wix Commerce.
- **Headless / Composable**: Shopify Hydrogen, commercetools, Saleor, Medusa.
- **Open-source**: Medusa.js, Saleor, Sylius (PHP), Spree.
- **Marketplace**: Mirakl, Marketplacer.
- **B2B specialized**: SAP Commerce, Salesforce B2B, Spryker.
### 매 핵심 component
- **Catalog**: product, variant, option, inventory.
- **Cart/Checkout**: session, abandoned cart, taxonomy.
- **Payment**: PSP integration (Stripe, Adyen, Toss).
- **Order/Fulfillment**: state machine, OMS.
- **Tax/Shipping**: TaxJar, Avalara, ShipBob.
- **Customer/Account**: passwordless, social, subscription.
### 매 응용
1. Shopify Hydrogen 의 storefront 와 headless backend.
2. Medusa.js + Next.js 의 fully open-source stack.
3. Composable commerce 의 best-of-breed 조합.
## 💻 패턴
### Shopify Storefront API (GraphQL)
```ts
// app/lib/shopify.ts
import { createStorefrontClient } from "@shopify/hydrogen-react";
export const client = createStorefrontClient({
storeDomain: "your-shop.myshopify.com",
storefrontApiVersion: "2025-01",
publicStorefrontToken: process.env.PUBLIC_STOREFRONT_TOKEN!,
});
const PRODUCTS_QUERY = `#graphql
query Products($first: Int!) {
products(first: $first) {
nodes {
id title handle
priceRange { minVariantPrice { amount currencyCode } }
featuredImage { url altText }
}
}
}`;
export async function listProducts(first = 24) {
const { data } = await client.query(PRODUCTS_QUERY, { variables: { first } });
return data.products.nodes;
}
```
### Medusa.js (open-source backend)
```ts
// medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils";
export default defineConfig({
projectConfig: {
databaseUrl: process.env.DATABASE_URL,
redisUrl: process.env.REDIS_URL,
http: {
storeCors: process.env.STORE_CORS!,
adminCors: process.env.ADMIN_CORS!,
authCors: process.env.AUTH_CORS!,
jwtSecret: process.env.JWT_SECRET!,
cookieSecret: process.env.COOKIE_SECRET!,
},
},
modules: [
{ resolve: "@medusajs/medusa/payment-stripe",
options: { apiKey: process.env.STRIPE_API_KEY }},
{ resolve: "@medusajs/medusa/cache-redis",
options: { redisUrl: process.env.REDIS_URL }},
],
});
```
### Cart state (Zustand + persist)
```ts
import { create } from "zustand";
import { persist } from "zustand/middleware";
type CartItem = { variantId: string; qty: number; priceCents: number };
interface CartState {
items: CartItem[];
add: (i: CartItem) => void;
remove: (variantId: string) => void;
total: () => number;
}
export const useCart = create<CartState>()(persist(
(set, get) => ({
items: [],
add: (i) => set(s => {
const ex = s.items.find(x => x.variantId === i.variantId);
return ex
? { items: s.items.map(x => x === ex ? { ...x, qty: x.qty + i.qty } : x) }
: { items: [...s.items, i] };
}),
remove: (id) => set(s => ({ items: s.items.filter(x => x.variantId !== id) })),
total: () => get().items.reduce((a,b) => a + b.qty * b.priceCents, 0),
}),
{ name: "cart" }
));
```
### Stripe Checkout (server)
```ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET!);
export async function createCheckoutSession(items: CartItem[], userId: string) {
return await stripe.checkout.sessions.create({
mode: "payment",
line_items: items.map(i => ({
price_data: {
currency: "usd",
product_data: { name: i.title },
unit_amount: i.priceCents,
},
quantity: i.qty,
})),
success_url: `${process.env.SITE}/order/success?sid={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.SITE}/cart`,
automatic_tax: { enabled: true },
metadata: { userId },
});
}
```
### Webhook idempotency
```ts
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { redis } from "@/lib/redis";
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature")!;
const body = await req.text();
const event = Stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
// Idempotency: refuse double-processing
const seen = await redis.set(`stripe:evt:${event.id}`, "1", "EX", 60*60*24, "NX");
if (!seen) return new Response("dup", { status: 200 });
if (event.type === "checkout.session.completed") {
await fulfillOrder(event.data.object);
}
return new Response("ok");
}
```
### Order state machine (XState)
```ts
import { createMachine } from "xstate";
export const orderMachine = createMachine({
id: "order",
initial: "pending_payment",
states: {
pending_payment: {
on: { PAID: "paid", CANCELLED: "cancelled", EXPIRED: "expired" }
},
paid: { on: { PACKED: "packed", REFUND: "refunded" } },
packed: { on: { SHIPPED: "shipped" } },
shipped: { on: { DELIVERED: "delivered", LOST: "investigating" } },
delivered: { type: "final" },
cancelled: { type: "final" },
expired: { type: "final" },
refunded: { type: "final" },
investigating: { on: { RESOLVED: "delivered", REFUND: "refunded" } },
},
});
```
### Inventory reservation
```ts
// Atomic reserve via Redis Lua
const RESERVE_LUA = `
local stock = tonumber(redis.call('GET', KEYS[1]))
local need = tonumber(ARGV[1])
if stock and stock >= need then
redis.call('DECRBY', KEYS[1], need)
return 1
end
return 0`;
await redis.eval(RESERVE_LUA, 1, `stock:${variantId}`, qty);
```
### Tax calc (Stripe Tax / Avalara)
```ts
const calc = await stripe.tax.calculations.create({
currency: "usd",
line_items: items.map(i => ({
amount: i.priceCents * i.qty,
reference: i.variantId,
tax_behavior: "exclusive",
})),
customer_details: {
address: { line1, city, state, postal_code, country: "US" },
address_source: "shipping",
},
});
```
## 매 결정 기준
| 상황 | Approach |
|---|---|
| <100 SKU, fast launch | Shopify (Basic/Plus) |
| custom checkout flow | Hydrogen (Shopify) or Medusa |
| 100% control + open-source | Medusa.js |
| enterprise, MACH | commercetools |
| B2B multi-tenant | Saleor / Spryker |
| AI agent shopping | Storefront API + agent layer |
**기본값**: Shopify Plus + Hydrogen for SMB-mid; Medusa.js for full-control startups.
## 🔗 Graph
- 부모: [[Web Platforms]] · [[Commerce Architecture]]
- 변형: [[Headless Commerce]] · [[MACH Architecture]]
- 응용: [[Subscription Billing]] · [[Marketplace Platforms]]
- Adjacent: [[Stripe Integration]] · [[Inventory Management]] · [[Order State Machine]]
## 🤖 LLM 활용
**언제**: product description gen, category mapping, support FAQ from KB, conversational search.
**언제 X**: payment authorization 의 final approval — deterministic system 만.
## ❌ 안티패턴
- **DIY checkout from scratch**: PCI scope blowup.
- **Webhook without idempotency**: double-fulfill, double-refund.
- **Cart in localStorage only**: lost on multi-device.
- **No inventory reservation**: oversell.
## 🧪 검증 / 중복
- Verified (Shopify 2025 dev docs, Medusa v2, Stripe Tax docs, MACH alliance).
- 신뢰도 A.
## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — ecommerce platform decision matrix + patterns. |