ECショッピングサイト開発事例 — 決済連携からUI設計まで
プロジェクト概要
あるアパレルブランドのクライアント企業から、自社ECサイトの構築を依頼されました。既存のモール型ECサイトへの出店から脱却し、独自ドメインでのD2C(Direct to Consumer)販売を実現するプロジェクトです。
決済にはStripeを採用し、Next.jsベースのフロントエンドと組み合わせることで、高速かつセキュアなECサイトを構築しました。本記事では、要件定義からリリースまでの全プロセスを解説します。
要件定義 — 独自ECサイトに求められるもの
クライアントと複数回のヒアリングを重ね、以下の要件を確定しました。
- 商品管理: SKU(Stock Keeping Unit)単位での在庫管理。カラー・サイズのバリエーション対応
- 決済: クレジットカード決済(Visa / Mastercard / JCB / AMEX)、コンビニ決済
- UI/UX: モバイルファーストのレスポンシブデザイン。商品画像のズーム表示対応
- 注文管理: 管理画面での注文一覧・ステータス管理・出荷連携
- セキュリティ: PCI DSS準拠の決済フロー。個人情報保護法対応
- メール通知: 注文確認、発送通知、レビュー依頼の自動送信
- パフォーマンス: ページ読み込み3秒以内、年間稼働率99.9%
技術選定
フロントエンド: Next.js + TypeScript
ECサイトにNext.jsを採用した理由は以下の通りです。
- SEO: 商品ページのSSRにより、検索エンジンでの商品インデックスを最適化
- パフォーマンス: ISRで商品ページを静的生成し、在庫情報のみリアルタイムで更新
- 型安全性: TypeScriptにより、商品データや注文データの型をフロントエンドからバックエンドまで一貫して管理
決済: Stripe
決済基盤にはStripeを選定しました。Stripeは日本国内のクレジットカード決済はもちろん、コンビニ決済やApple Payにも対応しています。
- Stripe Checkout: Stripeがホストする決済ページにリダイレクトする方式。PCI DSSの負荷を最小化
- Payment Intent: 自社サイト内に決済フォームを埋め込む方式。Stripe Elementsでカード情報を安全に処理
- Webhook: 決済完了・失敗などのイベントをリアルタイムで受信し、注文ステータスを自動更新
バックエンド: Supabase
商品管理、在庫管理、注文管理のデータベースにはSupabaseを使用しました。PostgreSQLの柔軟なクエリ機能とRLS(Row Level Security)による細かなアクセス制御が、ECサイトのデータ管理に適しています。
Stripe決済連携の実装
Stripe Checkoutフロー
本プロジェクトでは、ストレスのない決済体験のためにStripe Checkoutを中心に実装しました。ユーザーは商品選択からカート確認を経てStripeの決済ページにリダイレクトされ、決済完了後に自社サイトの完了ページに戻ります。
// Stripe Checkoutセッション作成(APIルート)
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const { items } = await request.json()
// カート内の商品からStripeの明細を生成
const lineItems = items.map((item: CartItem) => ({
price_data: {
currency: "jpy",
product_data: {
name: item.name,
images: [item.imageUrl],
},
unit_amount: item.price, // 日本円は小数なし
},
quantity: item.quantity,
}))
// Checkoutセッションを作成
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: lineItems,
mode: "payment",
success_url: `${process.env.NEXT_PUBLIC_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`,
shipping_address_collection: {
allowed_countries: ["JP"],
},
})
return Response.json({ url: session.url })
}
Webhook検証によるセキュアな注文処理
決済完了後の注文処理は、StripeのWebhookで受け取ったイベントに基づいて実行します。Webhookの署名検証により、不正なリクエストを確実に排除しています。
// Stripe Webhook処理(APIルート)
import { headers } from "next/headers"
export async function POST(request: Request) {
const body = await request.text()
const headersList = await headers()
const signature = headersList.get("stripe-signature")!
// Webhookの署名を検証(改ざん防止)
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
// 決済完了イベントの処理
if (event.type === "checkout.session.completed") {
const session = event.data.object
// 注文レコードの作成、在庫の減算、確認メール送信
await createOrder(session)
await updateInventory(session)
await sendConfirmationEmail(session)
}
return Response.json({ received: true })
}
商品管理・在庫管理の設計
データモデル
商品データは、商品(products)、バリエーション(variants)、在庫(inventory)の3層構造で設計しました。
-- 商品テーブル
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
description TEXT,
base_price INTEGER NOT NULL, -- 基本価格(税抜)
category_id UUID REFERENCES categories(id),
is_published BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 商品バリエーション(カラー × サイズ)
CREATE TABLE variants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES products(id),
color TEXT NOT NULL,
size TEXT NOT NULL,
price_override INTEGER, -- 基本価格と異なる場合
sku TEXT UNIQUE NOT NULL,
UNIQUE(product_id, color, size)
);
-- 在庫管理
CREATE TABLE inventory (
variant_id UUID PRIMARY KEY REFERENCES variants(id),
quantity INTEGER NOT NULL DEFAULT 0,
reserved INTEGER NOT NULL DEFAULT 0, -- カートに入っている数
updated_at TIMESTAMPTZ DEFAULT now()
);
在庫のリアルタイム更新
在庫数は、カートに商品を追加した時点で「予約済み」として確保し、決済完了時に実際に減算するTwo-Phase方式を採用しています。カートに入れたまま放置された場合は、30分経過後に予約を解放する仕組みです。
レスポンシブなカートUI/UXの設計
モバイルファーストのUI設計原則
ECサイトのトラフィックの約70%はスマートフォン経由です。以下の設計原則を適用しました。
- サムゾーンの活用: 購入ボタンやカートアイコンを画面下部に配置し、片手操作でもアクセスしやすく設計
- ステッキーカートバー: スクロールしても画面下部にカート情報(商品数と合計金額)を固定表示
- スワイプ操作: 商品画像のギャラリーは左右スワイプで切り替え可能。ピンチイン・ピンチアウトによるズームにも対応
- ワンタップ購入: 以前の注文情報を保存し、リピート購入をワンタップで完了できる機能
決済フローのUX最適化
決済プロセスは3ステップに分割し、プログレスバーで進捗を可視化しています。
- Step 1 — カート確認: 商品名、数量、小計を一覧表示。数量変更と削除が可能
- Step 2 — 配送先入力: 住所の自動補完(郵便番号から)、以前の配送先からの選択
- Step 3 — 決済: Stripe Checkoutへリダイレクト(または埋め込みフォーム)
各ステップ間の離脱率を計測し、離脱が多いステップのUIを重点的に改善しました。特にStep 2の住所入力フォームは、郵便番号入力だけで都道府県・市区町村が自動補完される仕組みにしたことで、入力完了率が20%向上しました。
セキュリティ対策
PCI DSS準拠
クレジットカード情報のセキュリティについて、Stripe CheckoutまたはStripe Elementsを使用することで、カード情報が自社サーバーを経由しないアーキテクチャを実現しています。これにより、PCI DSSのSAQ-A(最も簡略化された自己評価)での準拠が可能です。
その他のセキュリティ施策
- CSRF保護: Next.jsのServer ActionsでCSRFトークンを自動検証
- 入力検証: zodスキーマによるサーバーサイド・クライアントサイド双方でのバリデーション
- レート制限: APIルートにレート制限を実装し、ブルートフォース攻撃を防止
- HTTPSの強制: Vercelのデフォルト設定ですべての通信がHTTPS化
- CSP(Content Security Policy): 外部スクリプトの実行を制限し、XSSリスクを低減
メール通知フロー
決済完了後から商品到着後まで、以下のタイミングで自動メールを送信しています。
- 注文確認メール: Webhook受信直後に送信。注文番号、商品明細、合計金額を記載
- 発送通知メール: 管理画面で出荷処理を行った際に送信。追跡番号と配送業者のリンクを含む
- レビュー依頼メール: 商品到着予定日の3日後に送信。購入した商品のレビューを促す
メール送信にはResend(APIベースのメール送信サービス)を使用し、ReactベースのメールテンプレートでHTMLメールをレンダリングしています。
具体的な成果
ECサイト公開後3ヶ月間での成果は以下の通りです。
- コンバージョン率: 従来のモール型EC比で25%向上。サイト内でのブランド体験が統一されたことで、購入意欲が高まった
- カート離脱率: 業界平均(約70%)を大幅に下回る55%を達成。3ステップの決済フロー最適化が効果的だった
- モバイル経由売上: 全売上の68%がモバイル経由。モバイルファーストの設計方針が数値に反映された
- 平均ページ読み込み時間: 1.8秒。目標の3秒以内を大幅に上回る速度を実現
- リピート率: 初月購入者の22%が2ヶ月以内に再購入。ワンタップ購入機能が貢献
まとめ
ECサイトの構築において、Next.js + Stripeの組み合わせは非常に強力です。SSRによるSEO最適化、ISRによるパフォーマンスの両立、StripeのPCI DSS準拠のセキュアな決済基盤、そしてTypeScriptによるフルスタックの型安全性。これらの技術要素が組み合わさることで、ユーザー体験と運用効率の両方を高い水準で実現できます。
ECサイトの新規構築やリニューアルをご検討の方は、ぜひ当社までお気軽にご相談ください。