0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

電子商取引システムの構築と最適化 | 第4部: Shopify Storefront APIによるカートとチェックアウトの実装

Posted at

はじめに

ヘッドレスE-commerceにおいて、カートチェックアウトは購買プロセスの中心であり、ユーザー体験(UX)とコンバージョン率に直接影響します。この第4部では、Shopify Storefront APIGraphQLを使用して、Next.jsでカート管理とチェックアウトフローを実装する方法を解説します。スケーラブル高性能な購買体験を構築するため、状態管理(Zustand)、ミューテーション、およびサーバーレスAPIを活用します。開発者向けに、パフォーマンス最適化エラーハンドリングを重視したコードスニペットを提供します。

カートとチェックアウトの重要性

E-commerceの購買フローでは、以下の要素が重要です:

  • スムーズなカート操作: 商品の追加、更新、削除が直感的。
  • 高速なチェックアウト: 最小限のステップで支払い完了。
  • 状態の一貫性: クライアントとサーバー間のデータ同期。
  • エラーハンドリング: 在庫不足や支払いエラーの適切な処理。

Shopify Storefront APIは、カートとチェックアウトを管理するための強力なGraphQLミューテーションを提供し、Next.jsAPI Routesと組み合わせることで柔軟な実装が可能です。

カート管理の実装

カートは、ユーザーが商品を選択し、購入に進む前の中間ステップです。Zustandを使用してクライアント側の状態を管理し、Shopify Buy SDKGraphQLでバックエンドと同期します。

1. Zustandでのカート状態管理

Zustandは軽量でシンプルな状態管理ライブラリです。カートの状態を管理:

# Zustandのインストール
npm install zustand

カートストアの設定(lib/cart-store.ts):

import { create } from 'zustand';

interface CartItem {
  id: string;
  title: string;
  quantity: number;
  price: { amount: string; currencyCode: string };
}

interface CartState {
  items: CartItem[];
  checkoutId: string | null;
  addItem: (item: CartItem) => void;
  updateQuantity: (id: string, quantity: number) => void;
  removeItem: (id: string) => void;
  setCheckoutId: (id: string) => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  checkoutId: null,
  addItem: (item) => set((state) => ({
    items: [...state.items.filter((i) => i.id !== item.id), item],
  })),
  updateQuantity: (id, quantity) => set((state) => ({
    items: state.items.map((item) =>
      item.id === id ? { ...item, quantity } : item
    ),
  })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter((item) => item.id !== id),
  })),
  setCheckoutId: (id) => set({ checkoutId: id }),
}));

2. Shopify Buy SDKでのカート操作

Shopify Buy SDKを使用して、カートを作成し商品を追加:

# Shopify Buy SDKのインストール
npm install @shopify/buy-button-js

カート初期化(lib/shopify-cart.ts):

import Client from 'shopify-buy';

const client = Client.buildClient({
  domain: 'my-ecommerce-dev.myshopify.com',
  storefrontAccessToken: process.env.SHOPIFY_STOREFRONT_API_TOKEN,
});

export async function createCart() {
  const checkout = await client.checkout.create();
  return checkout;
}

export async function addToCart(checkoutId: string, variantId: string, quantity: number) {
  const lineItems = [{ variantId, quantity }];
  const updatedCheckout = await client.checkout.addLineItems(checkoutId, lineItems);
  return updatedCheckout;
}

export async function updateCartItem(checkoutId: string, lineItemId: string, quantity: number) {
  const updatedCheckout = await client.checkout.updateLineItems(checkoutId, [
    { id: lineItemId, quantity },
  ]);
  return updatedCheckout;
}

export async function removeCartItem(checkoutId: string, lineItemId: string) {
  const updatedCheckout = await client.checkout.removeLineItems(checkoutId, [lineItemId]);
  return updatedCheckout;
}

3. カートUIの実装

カートコンポーネントを作成し、Zustandで状態を管理:

// components/Cart.tsx
import { useCartStore } from '../lib/cart-store';
import { addToCart, updateCartItem, removeCartItem } from '../lib/shopify-cart';

export default function Cart() {
  const { items, checkoutId, addItem, updateQuantity, removeItem, setCheckoutId } = useCartStore();

  const handleAddToCart = async (variantId: string, title: string, price: { amount: string; currencyCode: string }) => {
    if (!checkoutId) {
      const checkout = await createCart();
      setCheckoutId(checkout.id);
      const updatedCheckout = await addToCart(checkout.id, variantId, 1);
      addItem({ id: variantId, title, quantity: 1, price });
      return;
    }
    const updatedCheckout = await addToCart(checkoutId, variantId, 1);
    addItem({ id: variantId, title, quantity: 1, price });
  };

  const handleUpdateQuantity = async (id: string, quantity: number) => {
    const updatedCheckout = await updateCartItem(checkoutId!, id, quantity);
    updateQuantity(id, quantity);
  };

  const handleRemoveItem = async (id: string) => {
    const updatedCheckout = await removeCartItem(checkoutId!, id);
    removeItem(id);
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">カート</h1>
      {items.length === 0 ? (
        <p>カートは空です</p>
      ) : (
        <div className="space-y-4">
          {items.map((item) => (
            <div key={item.id} className="flex justify-between border-b py-2">
              <div>
                <h2 className="text-lg font-semibold">{item.title}</h2>
                <p>{item.price.amount} {item.price.currencyCode}</p>
              </div>
              <div className="flex items-center space-x-4">
                <input
                  type="number"
                  value={item.quantity}
                  onChange={(e) => handleUpdateQuantity(item.id, parseInt(e.target.value))}
                  className="w-16 border p-1"
                  min="1"
                />
                <button
                  onClick={() => handleRemoveItem(item.id)}
                  className="text-red-600 hover:text-red-800"
                >
                  削除
                </button>
              </div>
            </div>
          ))}
          <a href={checkoutId ? `/checkout/${checkoutId}` : '#'} className="btn-primary mt-4">
            チェックアウト
          </a>
        </div>
      )}
    </div>
  );
}

このコンポーネントは、カートの内容を動的に表示し、数量の更新や削除をサポートします。

チェックアウトフローの実装

Shopify Storefront APIGraphQLミューテーションを使用して、チェックアウトプロセスを実装します。

1. チェックアウトの作成

チェックアウトを作成し、配送先情報を追加するミューテーション:

mutation CreateCheckout($input: CheckoutCreateInput!) {
  checkoutCreate(input: $input) {
    checkout {
      id
      webUrl
    }
    checkoutUserErrors {
      code
      field
      message
    }
  }
}

# 変数
{
  "input": {
    "lineItems": [
      { "variantId": "gid://shopify/ProductVariant/123456789", "quantity": 1 }
    ],
    "shippingAddress": {
      "firstName": "太郎",
      "lastName": "山田",
      "address1": "東京都渋谷区1-2-3",
      "city": "渋谷区",
      "country": "JP",
      "zip": "150-0001"
    }
  }
}

Next.js API Routeでチェックアウトを作成(pages/api/checkout.ts):

import type { NextApiRequest, NextApiResponse } from 'next';
import { gql } from '@apollo/client';
import { client } from '../../lib/apollo-client';

const CREATE_CHECKOUT = gql`
  mutation CreateCheckout($input: CheckoutCreateInput!) {
    checkoutCreate(input: $input) {
      checkout { id webUrl }
      checkoutUserErrors { code field message }
    }
  }
`;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { items, shippingAddress } = req.body;

  try {
    const { data } = await client.mutate({
      mutation: CREATE_CHECKOUT,
      variables: {
        input: {
          lineItems: items.map((item: any) => ({
            variantId: item.id,
            quantity: item.quantity,
          })),
          shippingAddress,
        },
      },
    });

    if (data.checkoutCreate.checkoutUserErrors.length > 0) {
      return res.status(400).json({ errors: data.checkoutCreate.checkoutUserErrors });
    }

    return res.status(200).json({ checkout: data.checkoutCreate.checkout });
  } catch (error) {
    return res.status(500).json({ error: 'Failed to create checkout' });
  }
}

2. チェックアウトページ

チェックアウトページで配送先を入力し、ShopifyのチェックアウトURLにリダイレクト:

// pages/checkout/[id].tsx
import { useRouter } from 'next/router';
import { useState } from 'react';

export default function CheckoutPage() {
  const router = useRouter();
  const { id } = router.query;
  const [shippingAddress, setShippingAddress] = useState({
    firstName: '',
    lastName: '',
    address1: '',
    city: '',
    country: 'JP',
    zip: '',
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const response = await fetch('/api/checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ items: useCartStore.getState().items, shippingAddress }),
    });
    const { checkout, errors } = await response.json();
    if (errors) {
      console.error(errors);
      return;
    }
    window.location.href = checkout.webUrl; // Shopifyチェックアウトにリダイレクト
  };

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">チェックアウト</h1>
      <form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
        <input
          type="text"
          placeholder=""
          value={shippingAddress.firstName}
          onChange={(e) => setShippingAddress({ ...shippingAddress, firstName: e.target.value })}
          className="border p-2 w-full"
        />
        <input
          type="text"
          placeholder=""
          value={shippingAddress.lastName}
          onChange={(e) => setShippingAddress({ ...shippingAddress, lastName: e.target.value })}
          className="border p-2 w-full"
        />
        <input
          type="text"
          placeholder="住所"
          value={shippingAddress.address1}
          onChange={(e) => setShippingAddress({ ...shippingAddress, address1: e.target.value })}
          className="border p-2 w-full"
        />
        <input
          type="text"
          placeholder="市区町村"
          value={shippingAddress.city}
          onChange={(e) => setShippingAddress({ ...shippingAddress, city: e.target.value })}
          className="border p-2 w-full"
        />
        <input
          type="text"
          placeholder="郵便番号"
          value={shippingAddress.zip}
          onChange={(e) => setShippingAddress({ ...shippingAddress, zip: e.target.value })}
          className="border p-2 w-full"
        />
        <button type="submit" className="btn-primary">支払いに進む</button>
      </form>
    </div>
  );
}

エラーハンドリングとテスト

以下のテストを実施:

  • カートテスト: 商品の追加、更新、削除が正しく同期するか。
  • チェックアウトテスト: 配送先入力後のリダイレクトが正常か。
  • エラーハンドリング: 在庫不足や無効なバリエーションIDの処理。
  • パフォーマンス: APIレスポンス時間(目標:200ms以下)。

エラーハンドリング例(lib/shopify-cart.tsに追加):

export async function addToCart(checkoutId: string, variantId: string, quantity: number) {
  try {
    const lineItems = [{ variantId, quantity }];
    const updatedCheckout = await client.checkout.addLineItems(checkoutId, lineItems);
    return updatedCheckout;
  } catch (error) {
    console.error('[Shopify Cart Error]:', error);
    throw new Error('カートへの追加に失敗しました');
  }
}

まとめ

この第4部では、Shopify Storefront APINext.jsを使ったカートチェックアウトの実装を解説しました。Zustandで状態管理を行い、GraphQLミューテーションAPI Routesでスムーズな購買フローを構築しました。次の第5部では、パフォーマンスSEOの最適化を掘り下げます。

Qiitaの皆さん、この記事が役に立ったら「いいね」や「ストック」をお願いします!コメントで技術的な質問や提案もお待ちしています!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?