--- 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()(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 - 변형: [[Headless Commerce]] ## 🤖 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. |