Webサービス「ワインスプリッティ」の技術トピックス

自社でサーバーインフラを構築しなくても従量課金で利用できるクラウドサービスを利用すれば、小規模な事業者でも容易にWebサービス・アプリの構築が出来るようになりました。
Webサービス「ワインスプリッティ」もGoogle社のクラウド環境を利用して開発・実装し運用を行っています。

本投稿はWebサービス「ワインスプリッティ」の開発・実装で使用した技術トピックスを列挙しました。
クラウドを活用する上でどんな課題があるのか、予備知識なしには意思決定も計画も進みません。「気付いていない課題」は事前に調べることが出来ませんが、採用前でも課題に気付いていればネットを使って調べられるのでほぼ何かしら対策が見つかります。
個々の詳細は割愛していますが、類似のサービスをこれから初めて開発される方はぜひこれらのポイントをご参考ください。
(なお、進行中の未実装機能とマネタイズ関連も今回は割愛しました)

はじめに

ワインスプリッティは2023年9月にベータ版公開したWebサービスです。
簡単に言えば、飲食店で複数のお客さんがボトルワインを相乗りで注文してシェアするためのマッチングを行う仕組みです。
安価なハウスワインはグラス単位で飲めますが魅力的な銘柄のワインはふつうボトル単位であり、量的にも費用的にも手を出しづらいです。でも、同じく飲みたい数人がタイミングよく居合わせれば、ボトルをシェアすることでワングラスずつ飲めるはずだ、その機会を意図的に創出しようという企画です。

詳しくはリリースをご参照ください。


このシステムのエッセンスは以下のようなものです。

  • シェアを呼びかける人(主催者)と、それに応じる人の2種類のユーザーが使う
  • スマホやPCの画面から操作してインターネットを介してマッチングする
  • マッチング条件を設定することによって成立・不成立をシステムが判定してフィードバックする
  • ワインという商材の特徴に特化することで使い勝手を良くする
  • ユーザー登録していない人へも募集情報を公開できる

これらを実現するために大手クラウドサービスのもつ各種機能を組み合わせてシステムを開発しています。

1.サービスの類型

 主催者と応募者という2種類の異なる役割を持つユーザーを仲介する「2サイドプラットフォーム」のパターンになります。
 場としての仕組みは提供しますが、価値の主体(ワインを飲む体験)は、主催者と応募者の間でやりとりされます。

 ニュースサイトのようにサービス自体が情報価値を提供する主体者となり、多数のユーザーがサービスに登録することで多対1の関係でつながるシステムとは異なる課題があります。
 投稿者と閲覧者が別にいるブログのプラットフォームに近いところがあります。

2.実現形態

 サービスはWebサイトとして提供しスマートフォンとPCへレスポンシブに対応します。
 主催者、応募者で異なるURLにアクセスすることで2種類のWebアプリを提供します。
 この形を採った理由は、なるべく少ないリソースで、広くカバーしたかったからです。
 お店に出かけるためのサービスですからスマートフォンがメインである一方、主催者になって欲しいお店や中高年が多く想定される客層にはPC利用者も多いと思われたこと。
 応募者用はアプリストアから配信するスマホアプリにしてAndroid版、iPhone版を作り、主催者はPCからWebサイトにアクセスする(Windows、Mac共通)案も検討しましたが、ソフト変更・アップデートのしやすさからなるべくシンプルな構成に落ち着きました。
 スマホアプリにしないとユーザーへの通知が出来ない懸念がありましたが後述のとおり一応解決しています。

3.実現手段

 クラウド基盤は Google Firebase を使用しています。
 Firebase Hosting でカスタムドメインの静的WebサーバーにWebアプリをデプロイします。
 フロントエンドのWebアプリは ReactStudioを用いて作成したREACTのGUIと、NoSQLのデータベースである Firebase Firestoreおよびサーバーレス実行環境 Firebase Functions を呼び出すJavaScriptコードです。
 サーバーサイド処理は Firebase Functions (Node18)で実行します。
 Webアプリを実行するクライアントのブラウザへは、Firebase Messagingで通知を送っています。

 クラウドの選択に当たっては、Amazon AWS、Microsoft Azure、Google Cloud で比較検討を行いました。
 AWSやAzureは道具立てが大きすぎて今回開発のシステムの規模感にはToo Muchでしたので、オールインワンで機能がコンパクトに揃っていて立ち上げまでの期間短縮が期待できる Firebase を採用しました。
 なお、開発時点ではまだ生成系AIがGoogleクラウドのAPIで提供されておらず、外部サービスである OpenAIのAPIを利用している個所があります。

以下、この実現手段を取った上で発生する技術イシューになります。

なおコード例等は省いていますが、この知識をもとに最小限のテンプレートから始めても Github copilot や ChatGPT を活用しつつ必要に応じてネット検索で補完すれば、かなりのところまで必要な実装が可能だと思います。
(但し実際にコードを動かしてサービスを稼働させるまでには大量のイシューがあります)

4.ユーザー登録の仕組み

 Firebase Authentication を使用しています。
 Firebaseのシステムがユーザー認証と情報管理を肩代わりしてくれるので自前でユーザー管理を作らなくても自分のサービスにユーザー登録画面を設置することができます。

 具体的にはFirebase AuthenticationをラップしたREACTのコンポーネントをWebページに置きます。ユーザーはそのUIパーツを通してFirebaseが実装した認証処理を通ってこちらのサービスに入ってきます。
 サービスの実装としては、ユーザーのパスワードの文字列を暗号化してどこかに保存したり比較したりする必要がありません。正しく認証された場合のみユーザーを特定するID値とメールアドレス等の附帯情報が得られるので、以後はそのID値で紐づけたユーザーデータを管理してサービスを提供するだけです。
 しかもFirebaseが管理してくれるユーザーアカウント側に、このサービスにとって必要なユーザーのRole等の識別情報CustomUserClaimsを埋め込んで保管することができます。これはそのユーザーアカウントがFirestoreのデータベースレコードにアクセスする権限を規定するときに役立ちます。

5.データベースの構成

 Firebase Firestore を使用しています。
 NoSQLのKey:Value型データベースです。フォルダとファイルのように階層化してデータをもつことができます。フォルダに相当するものがCollection、ファイルに相当するのがDocumentで、Document内に複数のKey:Valueを保存します。Valueには文字列、数値、日付、配列、マップなどのデータ型があります。データ型にはその特徴にあった機能が提供されています。たとえば数値のカウントアップダウン、日付の前後比較、配列要素への排他的な追加・削除などです。ArrayUnion/ArrayRemoveといった配列要素を重複なく追加・削除する機能などピンポイントですが便利なAPIもあったりします。
 また、複数の操作の一貫性を保つ(並行に複数の処理が走ったときに他方が割り込んで不整合が起きないようにする)ために、一連の排他的実行を保証するトランザクションやバッチの機能があります。
 ワインスプリッティは主催者が開催するイベント毎にワイン等の画像をアップロードして添付できます。これは画像データをFirebase Storageに保存し、保存したときに作られるファイル識別IDを、Firestoreの開催イベント用ドキュメントに保存することで紐づけています。

6.データベースのセキュリティ

 Firestoreはアクセスルールをプログラム的に組み合わせて定義することができます。
 これにより、各登録ユーザーのアカウントに自分のプロファイルのみ読み書き可能にしたり、主催者のアカウントからは応募者のニックネームを知ることができるがユーザーを一覧することはできず、応募者のアカウントからは主催者や募集情報を一覧できる等の制限を設定できます。また、サインインしていない訪問者にはリンク(複雑で長い文字列による)を知っている募集情報しか読み取ることが出来ない等のガードが実現できます。
 自分のWebアプリのコード自体は、当然ルールを守るようにプログラムしますが、第三者が悪意でデータベースにちょっかいを出さないようアクセス権限の設定には気を使うところです。

7.Webサイトのホスティング

 Firebase Hostingを使用してREACTのSPA(Single Page Application)をホストします。
 主催者用Webアプリを提供するURLはFirebase Hostingの提供するドメインをそのまま使って
 https://winesplity-host.web.app とし、ユーザーの大半を占めることになる応募者用Webアプリはカスタムドメインを取得して https://winesplity.jp を Firebase HostingのURLにオーバーライドしています。
 サービスを実行する処理の半分は、SPAによりクライアントのブラウザ上でJavaScriptコードが実行されることで機能します。但し、それだとブラウザ上のソースを見ることで、プログラムソースがすべて開示されてしまうので、差しさわりのないコードだけをブラウザで実行し、重要な部分はFirebaseのサーバーサイドの関数 Firebase Funtions(後述)内で実行することで隠蔽しています。
 このSPAを送出する動作は静的Webサーバーですが、Firebase Hostingでは特定のURLへのアクセスのみ、Firebase Functions に接続するrewrite 機能があり、これも重要な役割をもちます。

8.サーバーサイドで実行する処理

 上記7のとおり Firebase Functionsに実装しています。
 これは技術的にはAPIのゲートウエアを通じてカスタムコードが呼び出されるとき(つまり必要になったとき)だけサーバーのメモリに必要な実行イメージがロードされて実行されるDockerプロセスの一種です。(サーバーで実行するのになぜか「サーバーレス」と呼ぶのが違和感ですが)
 このサーバーサイド処理として3種類の実装を行っています。

(1)ブラウザでのユーザーのGUI操作に応じで実行する処理
 たとえばユーザーがID”abcd0123″の募集イベントに参加を申し込む場合、ブラウザで「申し込む」ボタンを押すことでJavaScriptはこの募集IDとユーザーIDを用いて応募用APIをコールします。このAPI接続によって、サーバー側で応募処理が走り、当該募集情報の応募者リストにユーザーIDを追加したり、最小催行人数に達したかどうかをチェックしたりが行われます。

(2)ユーザーの操作に関係なく定期的にサーバー側で実行する処理
 クラウド側の機能として所定の時刻や一定時間間隔でタスクを実行することができます。
 これを用いて、募集の期限がきたら最小催行人数に達しているか自動的に判定して結果をユーザーに通知したり、開催決定しているイベントの前日にリマインダーの通知を出すなどを行っています。

(3)ユーザーでもシステムでもない外部からのトリガーで情報を返す処理
 Firebase Hostingは特定のURLへのアクセスを、静的Webサーバーで応答せず、Firebase FunctionsのAPIに接続することで動的に情報を生成して返すことが可能です。
 これを用いて後述するOGPを実装しています。

9.プッシュ通知機能

 ワインスプリッティではFirebase Messagingを使って複数の通知機能を実装しています。
 たとえばフォローしているお店が新しい募集を作成したとき、フォロワーに通知を送ります。
 締切期限日にその募集が成立したか見送りになったか、成立したイベントの前日のリマインダーなども通知します。
 Webサイトにサービスワーカーというコードを置いてNotificationの処理を実装することでブラウザにバックグラウンドの機能が提供され、そのサイトを開いていないときでもサービスからの通知をポップアップすることが可能です。
 通知はクライアントを識別するTokenというIDに紐づいて行われ、Tokenはクライアントで生成されるため、クライアントのWebアプリのコードで自分のTokenをサーバーに送って保管させ、通知のトリガーを検出した Firebase Functions はサーバーにあるすべての通知対象Tokenに対して通知を送信するようプログラムします。

 ちなみに、ユーザーはプロフィール設定ページで通知のオプションを選択できるようにしているので、Functionsは、Tokenがあってかつそのトリガーに対する通知をONしているクライアントにのみ通知を送ります。募集情報の通知の場合は、通知をタップすることでその募集情報のページを開きます。
そのためのリンクが次にのべるコンテンツへの直接リンクです。

10.コンテンツへの直接リンク

 主催者はサインアップしてお店情報を登録し、そのお店情報にひもづく募集情報を次々と作成します。
 応募者はサインアップしてお客情報を登録することで、サービスに登録されているお店の一覧や、お店が募集しているイベント情報の一覧を見ることができます。
 これだけだと、イベントの募集情報は、サービスに登録済みの応募者にワインスプリッティのWebアプリを通してしか知らせる方法がありません。
 まだサインアップしていない人にも、データベース内の指定した募集情報を見ることができるURLを提供できるようにして、そのURLをSNSやメールに挿入して送れることが必要です。
 そのため、募集情報の1つ1つに割り当てられたIDをQuery文字列に埋め込んだURLを作成します。
 SPAは要求されたURLにQuery文字列があれば該当する募集情報をダイレクトに表示する仕組みにします。
 また、URLに対応して次にのべるOGPを併せて実装することで、SNSに投稿されたときに関心を引く表示がなされて、クリックによるサイトへの訪問からサインアップへと誘導が可能です。

11.OGPの実装

 SNSのフィードなどにURLを投稿すると、URLの文字列だけでなく、そのリンク先のWebページのサムネイルやタイトル、要約などの情報が一緒に表示されるのが一般的です。
 これは表示しているソフトと、表示される側のサイトが、OGP(Open Graph Protocol)をサポートしているからです。OGPの実装は、サービス提供側がWebページのリクエストに対して返されるヘッダー部分に、そのリンクに対応して表示して欲しいサムネイルやタイトル、要約などの情報を埋め込んで返すことです。これにより、呼び出し側がフィードを表示するアプリであればOGPの情報を表示し、URLをページとして表示しようとするブラウザであればそのままそのリンク先のコンテンツが表示されます。

 Firebase Hostingで困るのは、コンテンツへの直接リンクに対して、そのURLごとに異なる内容(募集の日時やワイン名などが異なる)のOGPを返すためにFirebase Functions のAPIに接続して動的ページ生成で返すようにすると、そのURLではブラウザで表示するSPAページが静的Webサイトとして返らないため、OGP対象のURLはブラウザで表示できないことです。
 そのためワインスプリッティでは、動的ページ生成の処理の中で、本来返したいSPAページを自分自身で一旦Fetchして取り込んで、そのヘッダーにOGPで返したい動的生成した内容を埋め込んでクライアント側へ返しています。

12.PWA化する

 ワインスプリッティはWebサイトですが、PWA(Progressive Web Application)の仕様に準拠することで、クライアントからはWebアプリとして見えるようになります。
 これはWebサイトのルートにマニフェストファイルを含む必要な情報一式を配置することです。
 これを行うと単にWebサイトへのリンクアイコンを置くよりもアプリっぽい「インストール」という形式をとってホーム画面やデスクトップへアイコンが作成されるようになります。

 この最大のメリットはサービスワーカーによるWebブラウザのバックグラウンド処理に紐づいた通知機能をActivate出来る点です。標準ではブラウザ通知を受け取れないiPhoneでも「インストール」することでAndroidやPC(Win/Mac)と同様に、ネイティブアプリのように通知を出すことができます。

13. アプリ内ブラウザー問題への対処

 上記10、11ではSNSを活用して募集のリンクを(登録済みユーザーかどうかに関係なく)シェアするケースを記載していますが、実はここにまだ大きな問題があります。
 PCではシェアされたリンクをクリックすると、システムに標準として設定されたブラウザ(Edge、ChromeあるいはSafari)が起動するため問題はありません。
 スマートフォンのSNSアプリでは表示されたフィードの中のリンクをクリックすると、オプショナルな操作をしない限り、そのスマホの標準のブラウザで開くのではなく、そのSNSアプリに内蔵されたブラウザで開いてしまいます。Nativeなスマホアプリと異なり、Webアプリはブラウザが異なると同じ端末内でも別のコンテキストで動作してしまう上に、アプリ内ブラウザからはFirebaseのユーザー認証が出来ません。つまりリンク先のイベント情報を見ることは出来てもサインインして申し込むことが出来ないのです。

 LINEにはこれを回避する固有の仕様があって、フィードに投稿するURLに所定のQuery文字列を付けておくと、そのリンクを開くときに外部ブラウザを起動してくれます。これは親切設計で助かりますが他のSNSアプリには同様の機能がありません。そこでワインスプリッティのWebアプリでは自分がアプリ内ブラウザで動作中かどうかを判定し、アプリ内ブラウザだった場合には、外部ブラウザで開いてもらうように案内する画面へ遷移するようにしました。

この判定にはブラウザのUserAgent、AgentDataを見るのですがこれがまた複雑怪奇なものになっていてシンプルなロジックがないのが実情です(独立したブラウザだと誤認させるようウソをつく仕様が実装されているアプリ内ブラウザがほとんど)。

14 AIを使ったアシスト機能

 ワインスプリッティは主催者の手間を軽減するため、募集情報の入力時にAIを使ったアシスト機能を設けています。募集のタイトル欄にワインの銘柄名を入力して「案文自動生成」を押すと、紹介文の欄にそのワインの説明とそれらしい勧誘文を自動でフィルするものです。内部実装は、そのワインの特徴とお客さんへのお勧め文を提案するようアプリでプロンプトを作成し、OpenAIのChatGPTのAPIに投げることで案文を作っています。驚くほどそれらしい文面を作ってくれますが、赤ワインと白ワインを間違えるようなミスもあるため、あくまで内容を確認のうえ採用してもらうようワーニングを加えています。
 提案する機能、は単に文章に留まらず、将来的には過去の実績を踏まえて魅力的なイベントそのものを提案できるようになるのではないかと考えています。

15 フィードバックの仕組み

 アプリストアからの配信ではないので利用者からの何らかのフィードバック収集手段を独自に設ける必要がありました。
 Googleフォームから投稿してもらってGoogleシートに蓄積する方法を取りました。
 設定が簡単でかつ十分実用性があります。必要に応じて Google Apps Scriptでデータを処理できます。
 アンケート用URLはランダム文字列を含んだ長いものになるのと、あとでアプリを変更することなく投稿先を切替えたくなるケースを想定し、一旦自社ドメイン内のURLで受けて、そこからフォームのURLへリダイレクト設定しました。
 主催者用と応募者用に別々のフォームを作り、それぞれのアプリのオープニング画面の中からリンクしています。

16 その他

 Firebase固有の仕様や使いこなしがあるため、一旦Firebaseで開発が進むと、AWSやAzureに切り替えるのは困難でしょう。逆もまたしかりですので最初によく検討すべきです。まず手早く作ってみて見直すと言うのは簡単ですが、やはり最初にしっかり考える必要があります。
 またFirebaseの初期設定で Regionがありますが、これもユーザーIDとデータIDの紐づきがあるので一旦データが入ってしまうとRegionを後から変更するのは大きな手間を伴います。迂闊にデフォルトで作成しないよう注意してください。
 クラウド上のコードである Firebase Functionsのデバッグや改良には FunctionsのNodeをローカルPC上で模擬動作させるFirebase Emulatorの利用が必須です。コード量が増える前に早い段階でテスト環境を確立しておくと良いです。

こうしてみると、個々の要素自体はさほど難度の高いものはありませんが、時間の余裕と経験値がないと躓くネタも多く、全体として集積すると、ちょっとしたサービスをローンチするのも結構なノウハウの塊になるのかもしれません。

以上

Webサービス「ワインスプリッティ」の技術トピックス” に対して1件のコメントがあります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です