yasutomogのブログ

Software Engineerの雑記

JamstackなECサイトの考察

概要

  • Jamstack Advent Calendar 2020 - Qiita の22日目の記事です。
  • 株式会社トライビート| TRIBEAT CO., LTD. という会社でBtoCや業務システムのWebアプリケーション構築や運用・保守をしています。
  • これまでJamstackという言葉が出てくる前から、社内独自のSSGの仕組みで試行錯誤して対応することがありました。
  • Jamstack関係の環境やサービスが充実してくる中で、ECサイト構築が現実的に良い選択になりそうかを実際に試してみたので、考察やハマったポイントを備忘録としてまとめる。(まだ検証中)

アーキテクチャの構成

  • microCMS
  • Firebase hosting
  • Firestore
  • Firebase functions
  • Firebase Authentication
  • Nuxt.js
  • Algolia
  • Stripe
  • GitHub Actions

※Next.jsとVercelへの移行も試したいが、データ層の設計がまとまったタイミングで検討する。

画面構成

ECサイト

  • 商品一覧
    • サイトのtopページ
    • algoliaから商品一覧を取得して表示
    • 検索を可能にするため、商品一覧情報を組み込んだ静的ページ化はせずSPAとする
    • 商品をクリックすることで、商品詳細画面へ遷移
  • 商品詳細画面
    • SSGで静的ファイル化したページ
    • 数量変更とカート追加が可能
    • カート追加ボタンをクリックすることで、カート画面へ遷移
  • カート画面
    • カートに追加された商品一覧を表示
    • 商品の数量を変更することが可能
    • 買い物を続けるボタンをクリックすることで商品一覧画面へ遷移
    • 購入者情報とカード情報を入力すると購入ボタンが活性化
    • 購入ボタンクリックすると、在庫チェック後、決済用のCloud Functionsをリクエス
    • カード情報入力部分はStripeのvueコンポーネントを使用
  • アカウント画面(未実装)
    • いつでも作成できるので、後回しにして進めていた画面
    • 取り急ぎは、Firebase Authentication上でユーザー作成し、簡易画面からログイン処理とログアウト処理を実装
    • アカウント情報の作成や更新できるようにする
    • ログイン、ログアウトの組み込み
    • 認証はあくまでも便利機能として考え、未認証でも購入できる想定

管理者サイト

  • プレビュー画面
    • microCMS管理画面の画面プレビューからリクエストされる想定
    • リクエストURLからmicroCMSのAPIを叩き、動的にページ作成
  • 在庫管理画面
    • 商品データに在庫数を付与したリスト画面
    • 商品データの一覧はSSGビルドで静的ファイルとして作成
    • 在庫数のみ、描画時にFirestroeから取得して当て込む
    • 在庫数の変更が可能(Firestoreを更新する)
  • 販売管理画面
    • 販売データ(日付、合計金額)とステータスを表示したリスト画面
    • FireStoreから動的にデータを取得するので、静的化せずSPAとする
    • 詳細ボタンをクリックすると、購入者情報や購入商品一覧をオーバーレイで表示

Functions(API

  • 決済用関数
    • 在庫チェックと金額改ざんチェック後、StripeのAPI呼び出し
    • 在庫数変更(Firestore)
    • 販売情報の登録(Firestore)

現状の簡易フロー図

f:id:yasug:20201218003238j:plain
jamstack_ec_flow

実装時の考察

ECサイトの機能について

  • 当初、商品一覧画面はページング付きですべてSSGしたもので進めていたが、やはり検索したいということからAlgoliaを利用したSPAとした
  • ログイン情報の状態管理はVuexとlocalstorageを同期させた管理方法で実装(詳細は下に別途書く)
  • ブラウザから直接Firestoreへwriteすることはやめ、Cloud Functionsを間に挟むように実装(ReadはOK)
  • カートの状態管理も認証と同様にVuexで管理
  • カート画面から購入時の在庫チェックは、その後のCloud Functions内でもするが一応フロント側でも見るようにした(要検討)

管理者サイトの機能について

  • リアルタイム性が必要になるデータを扱うことが多く利用ユーザも限定されるため、SSGに意固地になりすぎずSPAでの設計も積極的に検討する
  • Firestoreへのwrite系処理もCloud Functionsが間に挟めなくて良しとする(要検討)
  • プレビュー画面ではmicroCMSのAPI_KEYがソースから見えるが良しとする(要検討)

データ層について

  • ここが一番迷っていて検討要素が多い(Stripeでどこまでデータ管理すべきか見えていない)
  • 在庫データ、商品データ、販売データをFirestoreで管理して作成を進めた(要検討)
    • 在庫データと販売データはStripe側で持たせるほうがいいのか?
    • 商品データはStripeに連携してそちらでも管理する方がいいのか?
  • 商品データはmicroCMSからAPIで取得もでき2重管理になるがFirestoreにも入れた(要検討)
    • 決済時のCloud Functionsで金額チェックをしたい 具体的には、商品マスタ情報の金額と画面から渡された数量が、画面から渡された合計金額に一致するかチェックしたい
    • 上記チェック時の金額判断するためには、microCMSから更新された商品データの履歴が必要になるため、履歴管理をどこかで持たせたい
    • 商品データもStripeでもたせる方がいいのか?
  • 顧客データ(要検討)
    • 認証周りは簡易実装しかしていないので、現状はFirebase Authenticationに固定ユーザー作って試している ユーザー情報が登録されたときに、StripeとFirestoreどちらで管理すべきか?

SSG(ビルド)について

  • 各商品ページをSSG
  • 上記で、microCMSのAPIから商品データ一式取得するので、AlgoliaとFirestoreへ必要なデータ更新するように実装
    • Firestoreへの更新をトリガーにしたCloud Functionsを用意してAlgoliaのデータと同期するようにした方がいいか迷っている。
    • microCMSから商品データ更新(追加・削除)された場合に、Firestoreのデータを全て変更する必要はないので、microCMSで設定するカスタムのWebhookでやってもいいものか迷っている(詳細は下に別途書く)

ハマったポイントと解決案

1. GitHub Actionsでデプロイ時にfirebaseコマンドがないと言われた

背景

  • GitHub ActionsのFirebaseデプロイでは、よく以下を利用したサンプルを見かけるが、まずは理解のためにも自分でfirebaseコマンドを叩くように書いてみた

    github.com

  • デプロイ用のymlでは以下のように、stripeのシークレットKEYを設定し、デプロイコマンドを叩いていた

- name: deploy to Firebase Hosting
      run: |
        firebase functions:config:set stripe.secretkey=${{secrets.STRIPE_SECRET_KEY}}
        firebase deploy
      env:
        FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
  • GitHub Actions内のデプロイ処理で、firebase command not foundエラーが発生していた

解決案

  • デプロイの前処理でビルド処理が実行されるので、そこの npm install 時にfirebase-toolsを、-gでインストール
npm install -g firebase-tools

2. ある日突然、firebaseのデプロイがコケるようになった

背景

  • 12/16になったらデプロイがコケるようになった。
  • GitHub Actionsのエラー内容は以下
Error: An unexpected error has occurred.
Error: Process completed with exit code 2.

解決案

  • firebase-toolsのバージョンチェック

github.com

  • プロジェクト生成時は8系で進めていたが、12/16に9系に上がっていた!
  • とりあえずバージョンを8系にするように変更して正常動作することを確認できた
npm install -g firebase-tools@^8.0.0
  • 9系になったことで設定ファイル系のフォーマットなど変わったのかも、まだ詳細調査はできていない。後日する予定。

3. Firebase Auth使った認証状態の保持

背景

  • 認証自体はFirebase Authenticationを利用して簡易実装していたが、どう状態保持するか悩んだ
  • 最初、middlewareにauth処理を実装してvuexで状態保持を検討した
    • npm run devで確認しているとうまく動くように見えるが、npm run generate & npm run startだとうまくいかない
    • middlewareはF5(page refresh)時には呼ばれないため、Jamstack構成では使えないと判断
  • 次に、共通のヘッダコンポーネントを用意していたので、そこのbeforeCreateでfirebaseの認証チェックするようにした
    • 商品ページの切り替え時に、その都度認証確認リクエストが飛ぶことにモヤモヤ
    • 今後ページが増えていく中で、ヘッダが全て共通になるのかモヤモヤ
    • カートの状態も何かしらの方法で保持しなくてはいけないことに気づき、使えないと判断

解決案

  • 最終的には、以下のpluginを導入しlocalstorage管理することで対応

github.com

4. GitHub Actionsのパフォーマンス検討

背景

  • GitHub Actionsの処理全体で約4〜5分掛かっている
  • 内訳としては、ビルド(npm install含む)とfirebaseのデプロイでそれぞれ2分くらい
  • ビルド処理は常に1からインストールしているので、対策ないか調査

解決案

  • 探すとすぐに以下が見つかった。

github.com

  • ただ、使ってみたけど特に大きくパフォーマンスが改善されることはなく、ここについては後日調査

5. nuxt run generate 時にFirestoreへデータ入れようとして発生したワーニング

背景

  • build処理時、nuxt.config.jsのgenerateで以下のような感じでFirestoreにデータ更新していた
  generate: {
    async routes() {

      const { data } = await axios.get(
        `https://xxx.microcms.io/api/v1/items?limit=100`,
        { headers: { 'X-API-KEY': API_KEY } }
      )

      const fsStocks = firestore.collection('items')
      const batch = firestore.batch()

      const pages = []
      data.contents.forEach((content) => {
        pages.push({
          route: `/${content.id}`,
          payload: content
        })

        const nycRef = fsStocks.doc(content.id)
        batch.set(nycRef, content)
      })
      await batch.commit()

      return pages
    }
  },
  • npm run generate すると一応SSGは正常完了するものの、以下のようなワーニングが表示される
⚠ Nuxt Warning

The command 'nuxt generate' finished but did not exit after 5s
This is most likely not caused by a bug in Nuxt
Make sure to cleanup all timers and listeners you or your plugins/modules start. 
Nuxt will now force exit

DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error

解決案

  • generate完了時に明示的にFirestoreを終了させてやる必要がある
  • nuxt.config.jsのhooksに以下を追記
  hooks: {
    generate: {
      done(builder) {
        firestore.terminate().then()
      }
    }
  },

6. Firestoreへデータ登録かmicroCMSのWebhookで迷った

背景

  • ビルド時にmicroCMSから全データ取得しているので、その流れでFirestoreも一律全データ更新するようにしていた
  • microCMSで商品データ更新(追加、削除)された場合に、全データ更新ではなく、更新データのみFirestoreに反映した方が効率よいと思った
  • microCMSではWebhookにカスタム通知が設定できるので、それを利用しCloud Functionsを呼び出せば、1件ごとのFirestore更新ができるのではないかと思った

解決案

  • 以下のようなCloud Functionsを用意し、カスタム通知にURL設定することでWebhook連携の接続までは簡単にできることを確認
exports.updateCms = functions.region('asia-northeast1').https.onCall(async (data, context) => {
  functions.logger.log("called updateCms.")
})

exports.updateCms2 = functions.region('asia-northeast1').https.onRequest((request, response) => {
  functions.logger.log("called updateCms2.")
})
  • 関数内でリクエストパラメータを取得しFirestore更新まではいけるものの、セキュリティが心配に。
  • microCMSのカスタム通知で渡せるパラメータは決まっているため、認証用トークンなどは含めれないはず?
  • カスタム通知に設定するURLにクエリストリングを付与して、それをトークンとして扱うことも検討したが、一旦はそのままビルド時に全データ更新で落ち着いた。

結論

  • 細かい部分、特にデータ層の役割分担については検討も作り込みも必要だが、実際に書いてみた感触的には中小規模のECサイトであれば可能。
  • Jamstackな構成にすることで、様々なSaaSを利用することになるため、それについても少し考えてみた。
    • SaaS利用について、慎重に選択する必要はあるが利用そのものに消極的になりすぎる必要はない
    • パフォーマンス、スケーラビリティ、アベイラビリティ等の非機能要件への対策を、SaaS側に移譲することは合理的に感じている
      • SaaS提供企業側としては、如何にパフォーマンスよく安定したサービスを提供するかがビジネスに直結している
      • SaaS利用企業側からすると、対策にコストが掛かる非機能要件を移譲することで本来の目的となるビジネスロジックに集中することが可能