---
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
- 변형: [[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. |
