f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.9 KiB
7.9 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-전자상거래-플랫폼 | 전자상거래 플랫폼 | 10_Wiki/Topics | verified | self |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
전자상거래 플랫폼
매 한 줄
"매 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.
매 응용
- Shopify Hydrogen 의 storefront 와 headless backend.
- Medusa.js + Next.js 의 fully open-source stack.
- Composable commerce 의 best-of-breed 조합.
💻 패턴
Shopify Storefront API (GraphQL)
// 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)
// 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)
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)
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
// 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)
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
// 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)
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
🤖 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. |