WYSIWYGウェブページビルダーを支える技術とSever Driven UIへの拡張

この記事は、Merpay & Mercoin Advent Calendar 2024 の記事です。

はじめに

こんにちは。メルペイ Frontend の @togami です。
私たちのチームでは Engagement Platform、通称 EGP という内製マーケティングツールの開発をしています。ポイントやクーポンなどのインセンティブの配布、LP の作成と公開、キャンペーンの作成など CRM 関連のことをマーケターや PM がコーディングの知識なしで行えるようにするための社内ツールです。EGP はメルカリ US を除く全てのプロダクトで使われている会社全体の共通基盤となっています。 本記事ではこの中でも LP 作成機能、通称 EGP Pages について紹介します。また、 EGP Pages の拡張版であり Server Driven UI の実装である EGP Cards について紹介します。

EGP Pages とは

EGP Pages は WYSIWYG コンポーネントエディタです。LP の作成に特化しています。

  • Text
  • Image
  • Layout
  • Lottie
  • Entry Button

など全部で 28 種類のコンポーネントが用意されており、それらを組み合わせて LP を作成します。このツールを使って公開された LP は月間で 5000 万 PV ほどあり、月によっては 100 を超える LP が新たに公開されています。

EGP Pages の使い方

どのように機能するのか簡単なサンプルの作成を通してみていきます。 このサンプルでは Web ブラウザで閲覧している時は QR コードとテキストを、Mobile で閲覧している時はテキストカスタムボタンを表示するようにします。

①まずメニューから Layout コンポーネントを選択します(図1.1)。

コンポーネントメニュー
図1.1 コンポーネントメニュー

左側の Tree View に Layout が追加されました。 ②この Layout は Web 向けのコンテンツのコンテナとして使用するため、右側のタブの設定から”Web Content”と名付けます(図1.2)。

Layout へのネーミング
図1.2 Layout へのネーミング

③続いて QR コードのコンポーネント、Text のコンポーネントを挿入します。Text には"Please install app"というテキストを入れます(図1.3)。

QRとTextの追加と設定
図1.3 QRとTextの追加と設定

④次にアプリで閲覧している時のためのコンテンツを保持するために別の Layout コンポーネントを追加します。”Mobile Content”と名付け、その中にテキストとボタンを追加します(図1.4)。

Mobile 用のコンテンツ追加
図1.4 Mobile 用のコンテンツ追加

⑤全てのコンテンツを作成した後、When コンポーネントを使用して Layout をラップします。このコンポーネントは表示条件を設定でき、その条件が True の時のみ表示されるものです。 ⑥When で作成したコンテンツをラップして表示する条件を追加します。JavaScriptで条件を追加が、多用する条件についてはテンプレートが存在するので利用します。ここでは Mobile でのみ表示したいので、”Is using mobile apps”を選択すると条件が設定されます(図1.5)。

Whenによる条件分岐の設定
図1.5 Whenによる条件分岐の設定

⑦同様に Web 向けにもうひとつ When コンポーネントを追加し、⑧条件を設定します(図1.6)。

Web用コンテンツの分岐
図1.6 Web用コンテンツの分岐

このエディタにはデバイスの設定(Web/iOS/Android)やログイン状態、エントリー状態の有無など、状態をエミュレートする機能が備わっています。⑨iOS を想定して描画結果を確認してみると、このように Mobile Content の When ブロック内の要素のみが描画されているのがわかります(図1.7)。

エディタによるiOS環境のエミュレート
図1.7 エディタによるiOS環境のエミュレート

⓾同様に Web を想定したものです。Mobile のブロックは描画されておらず、Web のコンテンツのみが表示されています(図1.8)。 このように形でコンディショナルレンダリングをし、Web と Mobile で要素を出しわけたページを作成ができます。

エディタによるWeb環境のエミュレート
図1.8 エディタによるWeb環境のエミュレート

簡単なサンプルを通して基本的な使い方を説明しました。今回は触れませんが、これ以外にも LOG の設定や API のモック、開発中のモック機能や Dark モード/Light モードへの対応、日本語以外の英語や台湾華語への設定なども行うことができます。

アークテクチャ

EGP Pages は以下のようなアーキテクチャで構成されています(図2.1, 図2.2)。

EGP Pages の全体アーキテクチャ
図2.1 EGP Pages の全体アーキテクチャ
EGP pages エディタ部分のアーキテクチャ
図2.2 EGP pages エディタ部分のアーキテクチャ

DB は Cloud Firestore を使用しており、ユーザーが作成したコンポーネントやページの情報を保存しています。ユーザーが作成したコンポーネントやページの情報は全て JSON 形式で保存されており、それを元にページの描画しています。

先ほどの Firestore からロードしたスキーマを元にEGPのエディタの state の初期化 と 編集中の LP を React で描画するための transform を行います。 そして描画された結果は iframe を通じてエディタに表示されます。 エディタの右側に各種設定項目があり、変更すると Redux の state が更新されて、ます。その結果がリアルタイムで反映されます。

エディタから LP を公開する時には Cloud Run functions 上で LP を SSG することでで HTML と CSS を生成します。生成された静的なページを CDN 経由で配信しています。 こうして配信された HTML が Mobile なら Webview 経由で、Web ならそのまま描画されます。 最終的には静的な HTML を配信しているだけなのでパフォーマンスについても概ね良好です。

エディタの仕組み

エディタの仕組みについて、もう少し詳しく説明します。 一見複雑そうに見えますが、実際には各要素は決められたスキーマに沿った単純な JSON データです。先ほどのサンプルを例に挙げて説明します。

まず、Text コンポーネントですがこのエレメントは tagName: "Text" を持ち、props として valueclassName を持っています。これらの valueclassName は、エディタの右側にあるパネルから変更できます。

具体的な JSON データは以下のようになります。

{
  "tlVV3bzegT-Pi59b3sJgQ": {
    "id": "tlVV3bzegT-Pi59b3sJgQ",
    "name": null,
    "tagName": "Text",
    "props": {
      "value": "Please install the app!",
      "className": "text-center text-[0.8125rem] text-[#000000]"
    },
    "meta": null
  }
}

次に、QR コード コンポーネントも同様の構造を持っています。ただし、QR コード特有の url や表示に関するプロパティを持っています。 以下がその JSON データです。

{
  "SsUPPIaLOkuh7zUzgIn-S": {
    "id": "SsUPPIaLOkuh7zUzgIn-S",
    "name": null,
    "tagName": "QrCode",
    "props": {
      "url": "https://jp.mercari.com",
      "margin": "4",
      "scale": "4",
      "darkColor": "#000000",
      "lightColor": "#ffffff",
      "className": "self-center"
    },
    "meta": null
  }
}

これらの各エレメントは、それぞれ親子関係を持って構成されています。具体的には、Layout コンポーネントがコンテナとなり、その中に TextQR コード のコンポーネントを子要素として持っています。

以下がその Layout コンポーネントの JSON データです。

{
  "id": "W682tXEHtC1eWgvLMvyYx",
  "name": "Web Content",
  "tagName": "Layout",
  "props": {
    "className": "flex flex-col",
    "children": [
      ":=element.SsUPPIaLOkuh7zUzgIn-S",
      ":=element.tlVV3bzegT-Pi59b3sJgQ"
    ]
  },
  "meta": null
}

children には、このレイアウトの子要素として含まれるコンポーネントの参照が記述されています。":=element.SsUPPIaLOkuh7zUzgIn-S" は先ほどの QR コードコンポーネントを、":=element.tlVV3bzegT-Pi59b3sJgQ" は Text コンポーネントを指しています。 つまり、この Layout コンポーネントは先ほど定義した QR コードと Text のコンポーネントを子要素として持ち、それらを含むコンテナとして機能しています。

これらの JSON データを元に、エディタ内部では React のコンポーネントを再帰的に組み立てていきます。具体的には、tagName に対応する React のコンポーネントを生成し、props の情報をそれぞれに渡します。こうして生成されたコンポーネントツリーが、最終的に画面上に描画されます。

再掲 EGP pages エディタ部分のアーキテクチャ
図2.2(再掲) EGP pages エディタ部分のアーキテクチャ

ユーザーがエディタでコンポーネントの設定を変更すると、その変更は直ちに対応する JSON データに反映されます。そして、その更新された JSON データを元に React コンポーネントが再レンダリングされ、プレビュー画面にリアルタイムで反映されます。

LP のスタイリング

LP のスタイリングの裏側では Tailwind CSS を使っています。 CSS をあまり知らなくてもスタイリングが行えるように UIウィジェットを作成し、そこから Tailwind CSS のclassNameを付与することでスタイリングしています(図2.3)。

Tailwind CSSを付与するためのUIウィジェット
図2.3 UIウィジェット

また最終手段になりますが個々のエレメントに任意のclassNameが付与できるので、細かい調整や少し複雑な表現も Tailwind CSS でできることは全て実現ができるようになっています。

Native Bridge

LP の要件によっては Webview と Mobile 間での通信が必要です。例えばボタンをクリックした時にアプリ内の特定の画面を開いたり、特定のアクションを実行するといった動作です。 Webview と Mobile 間で通信するための方法はいくつかあります。その一つとして例えばカスタム URL スキームがあります。しかし、この方法では単方向でしか通信できなかったりセキュリティリスクがあります。

そこでより安全に Webview と Mobile 間で双方向通信を行うために、私たちは Onix という内製の Native Bridge を作成しました。これはChannel Messaging APIを利用した双方向通信をサポートしています。加えて端末の OS やバージョンによる Webview webview の API の差分のハンドリングや Channel Messaging API をサポートしていないバージョンの場合 Deep link へ fallback なども行います。 この Native Bridge は現在 iOS と Android のみ対応しているため、今後は Flutter の Webview でも対応する予定です。

技術的な課題

これまで一通りエディタについて見てきましたが、もちろん課題もあります。その 1 つとしてこのエディタは複数人によるリアルタイム編集ができません。そのため、予期せぬ上書きが発生してしまうことがありました。 詳しくはこちらのブログをご覧ください

この問題に対してはyjsを使ってリアルタイム編集を実現しようと検証中です。このライブラリは CRDT(Conflict-free replicated data type)と呼ばれるデータ構造の JavaScript による実装で多くの同時編集機能があるアプリケーションで使われています。

運用上の課題

EGP Pages はあくまで LP というドメインに特化したエディタです。そのため、大量の API コールや条件分岐が存在したり、特別なイベントハンドラが必要な複雑なものを作ろうとすると限界があります。
LSP や Linter、Formatter、Syntax Highlight、コードジャンプや検索がない VSCode でいわゆる Web Application チックなものを開発しているのを想像してみてください。エンジニアの方には伝わると思いますが、このような制限された環境では複雑な機能を実装したり大規模な物を管理したりすることが非常に困難だということは想像に難くないでしょう。

加えて、通常のテキストプログラミングと違ってテストを書くことも難しく基本マニュアルで QA しなければなりません。変更によって予期せぬ問題が発生するリスクが常につきまとっているので、小さな修正でさえ慎重に作業を進める必要があります。

また、これはローコードプラットフォームを作っていると直面することが多い問題ですが、できることが増えれば増えるほど、より複雑なデザインや機能要件が求められてしまいます。個々の要件をエンジニアなしで済むような UI や仕組みとして加えていくと、表面上は綺麗でもどんどんコードが複雑化していきます。そのため、どこまでをエディタの機能として提供し、どこからはエンジニアによるカスタマイズが必要かを常に考えていく必要があります。

当初 EGP もエンジニアなしで完結する ノーコード/ローコードプラットフォームのような方向性もありました。しかし今はエンジニアと非エンジニアが効果的に共同作業できるツールを目指すという方向性になっています。 そのため、引き続き 非エンジニア向けに UI ウィジェットを改善していくのはもちろんエンジニア向けにコンポーネントの Encapsulation や KV pair のコンフィグなどの新しいメカニズムを導入してエンジニアと非エンジニアがよりシームレスに協働できる環境を整え複雑な要件にも対応できるようにしていきます。

Server Driven UI

最後に Server Driven UI の実装であり、現在開発中の EGP Cards について解説します。
Server Driven UI とはサーバーからデータを一緒に UI の構造も返却してクライアント(Web / Mobile)でレンダリングする手法です。 2021 年に AirBnB が出したA Deep Dive into Airbnb’s Server-Driven UI System という記事で有名です。

A Deep Dive into Airbnb’s Server-Driven UI System

この手法には次のようなメリットがあります。

  • サーバー側の変更で UI を更新できるため、アプリのアップデートを待たずにリリースできる
  • クライアント側の実装を簡素化できる
  • クロスプラットフォームの一貫性を保ちやすい
  • Native で描画するため Webview と比べてパフォーマンスが良い

採用背景

これまで、マーケターは主に別のツールを用いてパーソナライズされたコンテンツを配信していました。しかし、利用可能なコンテンツカードは 3 種類のみで、そのカスタマイズ性も限定的でした。また、独自コンポーネントを使用しているため実際に配信されるまでコンテンツのプレビューができずテストが非効率 でした。

これに加えて、各プラットフォームでの開発工数の増大が課題となっていました。現在メルカリでは、プロダクトの増加に伴って使用しているプラットフォームが多岐に渡っています。

  • Mercari アプリ:Swift/Kotlin
  • Mercari Shops、はたらくタブ :アプリ内 WebView
  • Mercari Hallo:Flutter
  • Mercari Web:Web

当初、この課題に対して EGP Pages の活用を検討しました。しかし、WebView を用いた実装ではモバイルアプリのパフォーマンスに影響を及ぼす可能性があり、高パフォーマンスを求める要件には適さないという問題がありました。このような状況下で、EGP Pages の利点を活かしつつ課題を克服するために Server Driven UI の採用を決定しました。

EGP Cards

私たちは、Server Driven UI の実装として EGP Cardsと名付けたシステムを開発しました。 まず Mobile と Web 間共用の UI を記述するためにCard UI Protocolという JSON ベースのプロトコルを定義しました。

この Protocol を用いてクロスプラットフォーム UI を記述します。 一例として、以下のような UI を考えます。

  • 上下中央揃え、縦横 200px の Box
  • Box 中に"This is test"という Text が中央揃えで配置

これを Card UI Protocol で記述すると以下のようになります。

{
  "id": "root",
  "type": "Layout",
  // UIの構造を定義
  "styles": {
    "direction": "row",
    "wrap": false,
    "mainAxis": "center",
    "crossAxis": "center",
    "width": {
      "preferred": {
        "v": 200,
        "u": "px"
      }
    },
    "height": {
      "preferred": {
        "v": 200,
        "u": "px"
      }
    },
    "background": {
      "light": "#ffffff",
      "dark": "#222222"
    }
  },
  "children": [
    {
      "id": "SCaWRbQdLtwLoiCuEjh8h",
      "type": "Text",
      // UIの構造を定義
      "styles": {
        "fontSize": {
          "v": 13,
          "u": "px"
        },
        "textAlign": "center"
      },
      "props": {
        "value": "This is Test"
      },
      "children": []
    }
  ]
}

この JSON をもとにレンダリングエンジンで描画すると以下のような UI になります(図3.1)。

Card UI Protocolで記述したコンポーネントの描画結果
図3.1 Card UI Protocolで記述したコンポーネントの描画結果

EGP Cards Editor

このプロトコルで UI を簡単に作成できるように、EGP Pages のエディタを拡張しました。エディタ上で スタイリングされたコンポーネントを Card UI Protocol に変換して保存できるようにしています(図3.2)。

エディタの Cards 向け拡張
図3.2 エディタの Cards 向け拡張

EGP Pages では HTML ファイルと CSS ファイルを生成していましたが、EGP Cards ではコンポーネントを Card UI Protocol の形式に変換し最終的には JSON ファイルがデータベースに保存されます。

この JSON ファイルをクライアントに送信し、各プラットフォームでネイティブの UI として描画します。レンダリングエンジンは各プラットフォームの言語(Swift/Kotlin/Flutter/JavaScript)で実装されています。以下のように、単一の JSON ファイルから各プラットフォームで共通の UI を描画します(図3.3)。

単一のJSONファイルをもとにしたクロスプラットフォームレンダリング
図3.3 単一のJSONファイルをもとにしたクロスプラットフォームレンダリング

さらに、この EGP Cards をセグメンテーションサービスと組み合わせることで、ユーザーごとにパーソナライズされたコンテンツの配信や、A/B テスト の実施などユースケースの拡大を図っています(図3.4)。

セグメンテーションサービスと連携した例
図3.4 セグメンテーションサービスと連携した例

まとめ

本記事では内製マーケティングツール Engagement Platform(EGP)と、その中でも特に EGP Pages および Server Driven UI の実装の EGP Cards について紹介しました。 今後も技術的・運用上の課題に取り組みつつ、EGP を進化させていくことでマーケティング活動の効率化と効果向上を目指していきます。

次の記事は @poohさん です。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加