ヤフーショッピングのフロントエンドを支える共通配信技術について、「共通UI配信サービス誕生までの経緯」と「共通UI配信サービスを支える技術」の2部構成で紹介します。
共通UI配信サービス誕生までの経緯
ヤフーショッピングには、トップ、検索、商品詳細、カート、レビュー、問い合わせ、製品、キャンペーン、ランキング、注文履歴などさまざまな画面があり、それに合わせてさまざまな開発チームが存在しています。
各チームでアプリケーションは独立していて、それぞれリンクで接続されています。共通UIのHTMLもアプリケーションが異なれば、それぞれに記述されます。同じアプリケーション内で共通UIを実装するのは簡単ですが、アプリケーションが複数にまたがると、同じUIでもアプリケーションごとの実装が必要でした。
例えば、トップからキーワードを入力して検索すると、検索チームが製作している検索ページに遷移し、商品をクリックすると商品詳細チームが製作している商品詳細ページに遷移します。各アプリケーションは、昔からよくある構成で、ハイパーリンクによってのみ接続されます。以下の図の赤枠で囲われている同じレイアウトの部分は、それぞれ実装が必要でした。
共通UIはヘッダーや共通の告知領域、検索窓などで構成されており、ページ横断で表示されます。共通UIを修正するケースを考えてみます。
まず関係者を集めて仕様の説明が必要ですが、大人数の担当者の洗い出し、全員の空き時間の確保が困難ですし、個別に説明のミーティングを繰り返すのも効率的ではありません。ミーティングにこぎ着け仕様を説明できても、各チームは他のタスクも抱えているため、リソースの空き具合も、開発時期もバラバラになってしまい、リリース時期を揃えるのが難しくなります。工程が進めば、伝達が漏れていたチームがあるかもしれませんし、全チームリリースできても、各ページで微妙にレイアウトが違うケースも起こりえます。こうなると簡単な修正をするだけでも一苦労です。
やや大げさですが、同じような経験をしたことはないでしょうか。Webサービスの規模が大きくなると、アプリケーションを分割したくなり、今回のような問題が発生する可能性が大きくなります。
ヤフーでも、全社でシステムの大規模なリファクタリングの号令がかかりました。これは旧システムから脱却し、コンピューティングや言語の見直し、生産性を高めるきっかけになりました。このタイミングでフロントエンドも見直し、ヤフーショッピング内の共通UI配信システムを構築しました。そこで誕生したのが、共通UI配信システム「Ptah(プター)」です。
共通UI配信システム「Ptah」
Ptahには、「ptah-commons」「ptah-v2」「module-service」の3つのサービスがあります。このシステムは、運用を開始してから約3年経過していて、何度も改善を繰り返しています。
ptah-commons
例えばユーザー情報の取得や加工、ログイン判定など、UI構築に密接に関わるロジックをライブラリ形式で配信しています。ptah-v2
ほぼ全画面共通のUI配信サービスです。ヘッダーやフッター、検索窓などを配信しています。v2という名前からも推測できると思いますが、旧バージョンから大きく変更した点があります。module-service
全画面ではないものの、主にトップや検索ページ、アプリなどで利用される共有の回遊モジュールを配信する、後発のサービスです。回遊モジュールとは、ページの行き止まりを撤廃し、サイト内回遊を促進するモジュールです。特定のおすすめ商品や開催中の特集、キャンペーンなどを並べるモジュールがあります。
今回はその中でもptah-v2について詳しく説明します。
ptah-v2の作成では、「工数を削減できる」「レイアウト/仕様を統一できる」「フロントの作業ゼロで変更できる」「フロントの言語/フレームワークを制限しない」「Reactを使う」「UXを損なわない」の6つの要件を満たしたいと考えました。
工数を削減できる
共通配信により、調整や多重開発にかかるコストを削減することが狙いです。レイアウト・仕様を統一できる
調整ミスや実装ミスによるズレをなくす狙いです。フロントの作業ゼロで変更できる
工数削減にも関係しますが、共通UIの修正でフロント側と共同作業する機会をできる限りなくすことが主な目的です。各チームで一度導入すれば、インターフェースに変更がない限り、お互いを意識する必要がないように考慮しました。フロントの言語/フレームワークを制限しない
共通配信システムの都合で言語フレームワークを制限しないように配慮しました。Reactを使う
ここは他よりも具体的ですが、ヤフーショッピングのアトミックのデザインパーツ、いわゆるデザインシステムがReactライブラリとして配信されていたので、その恩恵を受けるべく採用しました。UXを損なわない
共通UI配信を導入することで、パフォーマンスが極端に下がったり、レイアウトシフトが発生してユーザー体験を悪くさせたりしないように意識しました。
これらの要件をクリアするために採用したのが、「マイクロフロントエンド」です。マイクロフロントエンドは、複数のプロダクトで一つのページを構成し、各プロダクトチームがそれぞれ好きなフレームワークを使える仕組みです。段階的に変更できること、シンプルで疎結合なこと、独立したデプロイができることがメリットです。バックエンド領域のマイクロサービスのメリットを、フロント領域にも適用する試みです。
これまでの構造も、大枠で捉えるとマイクロフロントエンドの1つのパターンです。
トップ、検索、商品詳細画面は、チームもアプリケーションも別ですし、採用しているアーキテクチャも言語も違います。それぞれ独立してデプロイが可能です。
構造がシンプルなことがメリットですが、共通UIの重複開発がデメリットとなっていました。そこで、これまでの構造は維持したまま、共通UI部分は共通配信チームで用意したものを活用しました。これを先ほどの6つの要件にあてはめてみます。
工数削減とレイアウト仕様の統一は、1カ所で作ることで自然と達成できます。また、マイクロフロントエンドを使えば疎結合で独立したデプロイが可能なので、フロントの作業はゼロで変更できるでしょう。1つのページ上で複数のフレームワークが使えるため、フロント側の言語/フレームワークを制限することはなく、Reactを使えます。そして、フロント側にReactを要求する必要はありません。
UXを損なわないかについては、別のアプローチを考える必要があるので、今回は触れませんが、マイクロフロントエンドのアプローチを使うだけで、かなりの要件をクリアできそうだと考えました。こうした経緯を経て、共通UIをマイクロフロントエンド的にReactで作ることにしました。
共通UI配信サービスを支える技術
マイクロフロントエンドの話題では、シングルページアプリケーション(以下、SPA)が前提のものが多く、ヤフーショッピングのように非SPAのケースはあまり取り上げられていないように思います。
そこで、「非SPAでマイクロフロントエンドをやってみた」例を、実際に実装されているサンプルコードなどを交えて紹介します。サンプルコードを見ていただければ、同じように実装できると思います。
まずはシステム構成から紹介します。Ptahの要素は、以下の図の青枠の部分です。
上図の通りCDN上にJS/CSSがあり、共通以外のコンテンツデータを返す専用BFF(Backend for Frontend)があります。BFFは、単純なREST APIになっています。
ブラウザからフロントがリクエストを受けて、ページHTMLを返します。ブラウザからCDNに配信されたJS/CSSがロードされ、Reactコンポーネントが描画される流れです。コンテンツデータは、専用BFF経由で取得できる構成です。
これだけでは分かりにくいので、中身をもう少し詳しく見ていきます。まずフロントでは、以下のようなHTMLを返します。
Ptahはフロントに作業してもらう部分なので、なるべく簡単にしておきたいところです。そこで次の構成を考えました。まず、linkタグで共通CSSをロードします。ここに共通UIのスタイルが定義されています。次に、scriptタグでPtahのコアJavaScript(以下、コアJS)のロードと初期化処理を追加。コアJSには、共通UIで共有されるReactのインスタンスやUIで横断的に使う処理が記載されています。
初期化処理では、ページごとに異なる値を設定します。最後にUIコンポーネントのタグを追加します。UIを表示したい位置に、Custom Elementsのタグと、UI用のJavaScriptをロードするscriptタグのセットを追加します。パラメータが必要な場合は、hoge=”fuga”のように属性で指定します。ここまで説明してきたセットが、各ページに必要となる共通UIの数だけ追加されます。フロント側がフレームワークを追加する必要はなく、導入コストも高くありません。
Custom Elementsというと、Webコンポーネントを使っているように感じますが、実際のUIの実装には使っていません。要件通りUIはReactで作っています。 実際の処理については、ページのHTMLを受け取った後、ブラウザ上ではJS/CSSがロードされます。その後はReactのコンポーネントが描画され、各コンポーネントからコンテンツデータが取得されます。
Ptahの中の実装について説明します。フロントにCustom Elementsで配置したUIをReactにする必要があり、その部分のコードは下図です。
このコードが、各UI用のJavaScriptがロードされた直後に実行されます。
Custom Elementsクラスを定義して、define()で追加します。するとconnectedCallback()が呼ばれるので、Reactをマウントするためのコンテナを動的に作成し、Reactからのパラメータでpropsを属性から作成します。最後にReactを描画して、それをCustom Elementsの下に追加します。
このような流れで、フロントがタグを配置した場所にUIが描画される仕組みになっています。Reactのコンポーネント自体は、実装の際に特別なルールやノウハウは必要ありません。これだけでも、マイクロフロントエンドの実装はできていると言えると思います。
Ptahの特徴は、ランタイムでCustom ElementsとReactによってUIを描画することです。ブラウザ上でUIを構築することで、マイクロフロントエンドの実装パターンとしては一般的な部類に入るでしょう。 フレームワークフリーなので、マイクロフロントエンドを実現するためのフレームワークやライブラリなどは採用していません。また、Reactのフロントからは隠しているので、フロント側ではフレームワークを導入する必要はありません。ヤフーショッピングのフロント、言語、フレームワークなどは多様なので、余計なものは入れたくありませんでした。
まだクリアできていない要件があります。「UXを損なわない」という要件です。UXで最も大きな問題なのは、ページがガクッと崩れるレイアウトシフトです。特にファーストビューの共通UIがレイアウトシフトすると、ヤフーショッピング全てのページに同様の事態が起こることになってしまいます。これを防止するには、「サーバーサイドレンダリング(SSR)」が必要です。この課題をどうするかマイクロフロントエンドの周辺を調べたのですが、参考になりそうなものは見つかりませんでした。
検討の末、ファーストビューのUIには「Isomorphic(同型写像)」を取り入れました。サーバーサイドとクライアントサイドで同じUIが描画される仕組みです。 「Next.js」などの有名なフレームワークでも使われていますが、フレームワークがなかったとしても、Reactであれば比較的実装しやすいと思います。SSRされるので、レイアウトシフトの問題は解決でき、ブラウザ上では通常のReactコンポーネントとして動いてくれます。これを実現するために、元のシステム構成にSSR APIを追加しました。
フロントでは、対象のUIがSSRされたHTMLフラグメントを取得します。このHTMLフラグメントは、ブラウザで使っているReactコンポーネントと同じものを使って描画しています。これで、SSRとCSRで同じものが描画されるようになりました。
しかしこの場合、下図のようにフロントでのページHTMLの生成方法が少し変わります。
SSR APIのリクエストの追加と、HTMLフラグメント、初期化データを貼り付ける処理を追加します。Custom Elementsタグの属性、hoge=”fuga”で指定したパラメータは、SSR APIのリクエストパラメータとして使われます。SSR APIからはHTMLフラグメントと初期化データが返ってくるので、それぞれ所定の方法で貼り付けてもらいます。そこまで大きくは変わっていないと思います。
ブラウザ側のReactのコンポーネント部分も、以下の図の通り少し変わります。
最も大きな違いは、Reactの出すパラメータのpropsを、初期化データから生成していることです。初期化データにはSSRのときに使われたパラメータとコンテンツデータが含まれているので、これを使うことで、CSRでもSSRと同じものが描画されます。
Isomorphic側のコード下段では、SSRしたものをCSRで置き換えています。SSRされたものが貼ってあるので、置き換えなくてもいいのではと思うかもしれません。しかしSSRされたものはスタティックなHTMLになり、たとえReactでイベントハンドラーが実装されていても全くなくなってしまいます。それではUIの表現力に限界があるので、これをReactコンポーネントで置き換えて、イベントハンドラーが動作するように工夫を施しました。
Reactの少し細かい話になりますが、IsomorphicではReactのハイドレートを使うイメージがあるかもしれません。ここではcreateElement()を使っていますが、フロントの作業をゼロで変更できるように整えるには重要なポイントです。そのために、PtahのJavaScriptはバージョンを上げることなく上書きする運用としていることが関係しています。
ハイドレートではSSRとCSRでDOM構造が同じであることが前提です。しかし、Ptahの変更をリリースするとき、SSR APIとCDNのJavaScriptで使われているReactコンポーネントは、同時には更新できません。その結果、SSRとCSRのDOM構造がずれることがあります。ハイドレートは扱いにくい側面があるのです。
このようにIsomorphicによるSSRという特徴も追加して、最終的な構成を組み立てることができました。SSRは非SPAだからこそ必要になったという面もありますが、Ptahの特徴的な部分だと思います。SSR APIを追加すると、フロントでやってもらうことが増えたり、結合が強くなったりするデメリットもありますが、これに関しては要件を達成するためのトレードオフだと思っています。
技術的な詳細は以上です。SSRの部分で少し複雑さが出たかなと思いますが、全体的にそこまで難しいことはしていません。
実践の効果
最後に、こうした取り組みによってどのような効果があったか簡単に紹介します。2021年度実績における共通UIに関わる開発コストは8分の1になり、年間約20人/月の削減に成功しました。コミュニケーションコストは工数に表れにくいのですが、定性的な評価では削減できていると実感しています。他にも、例えばリンクURLの変更であれば、開発からリリースまで1時間以内という速さで実装できるようになるなど、リードタイムの短縮も達成できました。
フロントのデプロイ件数も増加させることができました。フロント側でもフレームワークの刷新をしているので、一概にPtahだけの効果とは言えません。しかし、フロント側が本来注力したいところにリソースを使えるようにすることがPtahの存在意義なので、良い結果が出ているのは喜ばしいことです。今回の取り組みは「共通UI」に関わる部分なので、スケールしやすく、成果も出やすかったと考えています。
より踏み込んでマイクロフロントエンドを行い、チーム構成の変更や最適化まで検討するべきだという考えもあるでしょう。しかし、ヤフーショッピングは規模が大きく、そこまでは踏み込みませんでしたが、そこまでがっつりではなくともマイクロフロントエンド的なアプローチを「うまく」取り入れると幸せになれるケースはわりとありそうだなというのが率直な感想です。「うまく」というのがポイントです。それぞれの事情によって最適解は異なると思いますが、1つのサンプルとして皆さんの役に立てていただけますと幸いです。
アーカイブ動画
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました
- 田中 久志
- フロントエンドエンジニア
- ヤフーショッピングのWebフロント・BFFの開発に従事。今の仕事はTypeScript、Reactと、少しCSS、たまにJava。
- 小仲 博晃
- フロントエンドエンジニア
- ショッピングフロントエンドの共通配信サービスや大規模リファクタを手掛けるチームのリーダーとして、ZCP(k8s)のアーキテクチャを用いたアプリケーションの設計・開発・運用保守を行っている。