Server-Driven UI System を読んでみた
アーキテクチャを調べたりするのが結構好きなので漁っていたら、以下の記事が出てきたのでまとめる。
従来のクライアントドリブンUI
サーバードリブンUI(SDUI)の実装に深く入る前に、一般的なSDUIの概念と、それが従来のクライアントドリブンUIに対してどのような利点を提供するのかを理解することが重要になってくるのでまずはそこの認識から合わせます。
一般的にデータはバックエンドによって駆動され、UIは各クライアント(Web、iOS、Android)によって駆動されます。
例として、Airbnbのリスティングページを取り上げてみましょう。
ユーザーにリスティングを表示するために、バックエンドからリスティングデータをリクエストするかもしれません。このリスティングデータを受け取った後、クライアントはそのデータをUIに変換します。
問題点
クライアントドリブンUIにはいくつかの問題があります。
リスティングデータを変換し、レンダリングするためのリスティング固有のロジックが各クライアントに組み込まれる。
このロジックはすぐに複雑になり、リスティングの表示方法を変更すると柔軟性が失われます。
各クライアントは互いにロジックを統一する必要がある。
先述のように、この画面のロジックはすぐに複雑になり、各クライアントは状態の管理、UIの表示などのための独自の詳細と特定の実装を持っています。クライアントがすぐに互いから逸脱することは容易です。
モバイルにはバージョニングがある。
リスティングページに新機能を追加するたびに、最新の体験を得るためにユーザーにモバイルアプリの新バージョンをリリースする必要があります。ユーザーがアップデートするまで、新機能がユーザーによって使用されているか、または新機能に対して良い反応を示しているかを判断する方法はほとんどありません。
解決するには
UIとデータを一緒に渡し、クライアントはそれが含むデータに関係なく表示することで解決しようとするのがSDUIになります。
Airbnbの特定のSDUI実装では、バックエンドがデータとそのデータの表示方法をすべてのクライアントで同時に制御できます。画面のレイアウト、そのレイアウト内でセクションがどのように配置されるか、各セクションに表示されるデータ、さらにはユーザーがセクションと対話したときのアクションまで、すべてがWeb、iOS、Androidアプリを通じて一つのバックエンドレスポンスによって制御されます。
SDUIシステム
SDUIシステムはサーバーから送られるJSONレスポンスを元にUIを動的に生成します。これによりアプリの更新を待たずにUIの変更や新機能の追加が可能となります。
具体的なコード例を説明します。
まず、サーバーから送られるJSONレスポンスはこんな感じ。
{
"screen_type": "form",
"sections": [
{
"section_type": "user_info",
"fields": [
{
"field_type": "text",
"id": "first_name",
"title": "First Name"
},
{
"field_type": "text",
"id": "last_name",
"title": "Last Name"
},
{
"field_type": "date",
"id": "birthdate",
"title": "Birthdate"
}
]
},
{
"section_type": "submit",
"fields": [
{
"field_type": "button",
"id": "submit",
"title": "Submit"
}
]
}
]
}
このJSONは、フォームの画面("screen_type": "form")を表現しています。
フォームには2つのセクションがあり、1つ目のセクションはユーザー情報("section_type": "user_info")を入力するフィールドを持ち、2つ目のセクションは送信ボタン("section_type": "submit")を持っています。
次に、このJSONを受け取ったクライアントがどのようにUIを生成するかはこんな感じ。
func buildUI(from response: JSON) {
let screenType = response["screen_type"]
switch screenType {
case "form":
let formView = FormView()
let sections = response["sections"]
for section in sections {
let sectionType = section["section_type"]
switch sectionType {
case "user_info":
let userInfoSection = UserInfoSection()
let fields = section["fields"]
for field in fields {
let fieldType = field["field_type"]
switch fieldType {
case "text":
let textField = TextField()
textField.id = field["id"]
textField.title = field["title"]
userInfoSection.addField(textField)
case "date":
let dateField = DateField()
dateField.id = field["id"]
dateField.title = field["title"]
userInfoSection.addField(dateField)
default:
break
}
}
formView.addSection(userInfoSection)
case "submit":
let submitSection = SubmitSection()
let fields = section["fields"]
for field in fields {
let fieldType = field["field_type"]
switch fieldType {
case "button":
let buttonField = ButtonField()
buttonField.id = field["id"]
buttonField.title = field["title"]
submitSection.addField(buttonField)
default:
break
}
}
formView.addSection(submitSection)
default:
break
}
}
self.view = formView
default:
break
}
}
モバイルアプリの時にこの手法を使われること多そうなのでSwiftで書いたつもり。この関数は、サーバーからのJSONレスポンスを引数として受け取り、その内容に基づいてUIを動的に生成します。
このように、AirbnbのサーバードリブンUIシステムは、サーバーからのレスポンスに基づいてUIを動的に生成することで、アプリの更新を待つことなくUIの変更や新機能の追加を可能にします。
webアプリの時はこんな感じ
サーバーからのレスポンスをReactのコンポーネントに変換する関数を作成します。
const createComponentFromResponse = (response) => {
const { screen_type, sections } = response;
switch (screen_type) {
case 'form':
return (
<Form>
{sections.map(section => {
switch (section.section_type) {
case 'user_info':
return (
<UserInfoSection>
{section.fields.map(field => {
switch (field.field_type) {
case 'text':
return <TextField id={field.id} title={field.title} />;
case 'date':
return <DateField id={field.id} title={field.title} />;
default:
return null;
}
})}
</UserInfoSection>
);
case 'submit':
return (
<SubmitSection>
{section.fields.map(field => {
if (field.field_type === 'button') {
return <Button id={field.id} title={field.title} />;
}
return null;
})}
</SubmitSection>
);
default:
return null;
}
})}
</Form>
);
default:
return null;
}
}
次に、この関数を使用してサーバーからのレスポンスを元にUIを動的に生成します。
const App = () => {
const [response, setResponse] = useState(null);
useEffect(() => {
fetch('/api/response')
.then(res => res.json())
.then(setResponse);
}, []);
if (!response) {
return <div>Loading...</div>;
}
const Component = createComponentFromResponse(response);
return <div>{Component}</div>;
}
ABテストをしたい時はこんな感じ
import React from 'react';
const MyComponent = ({ variant, data }) => {
switch (variant) {
case 'A':
return <VariantA data={data} />;
case 'B':
return <VariantB data={data} />;
default:
return <DefaultVariant data={data} />;
}
};
サーバーから受け取ったUIのバリエーション(この例ではvariant)に基づいて異なるUIを表示するようなコンポーネントを挟めばできるのではなかろうか・・・・
SDUIの課題と解決策
課題
パフォーマンス: サーバーからのレスポンスを待つ時間がユーザー体験に影響を与える可能性があります。これは特にネットワーク接続が不安定な場合や、サーバーが遅延している場合に顕著です。
複雑さ: サーバードリブンUIは、クライアントとサーバーの間でUIの状態を同期させる必要があります。これは、特に複雑なUIや動的なUIの場合、開発の複雑さを増加させる可能性があります。
フレキシビリティ: サーバーがUIを制御すると、クライアントのフレキシビリティが制限される可能性があります。これは、特にデバイス固有の機能やユーザー体験を最適化するためのカスタマイズが必要な場合に問題となる可能性があります。
解決策
パフォーマンスの最適化: Airbnbは、サーバーからのレスポンスを待つ代わりに、クライアントが初期UIをすぐに表示できるように、スケルトンスクリーンを使用しています。これにより、ユーザーはアプリが反応していると感じ、待機時間が短く感じます。
複雑さの管理: Airbnbは、サーバーとクライアント間のUI状態の同期を管理するために、強力な型システムとスキーマ駆動のアプローチを使用しています。これにより、開発者はUIの状態をより簡単に理解し、エラーを防ぐことができます。
フレキシビリティの確保: Airbnbは、クライアントがデバイス固有の機能を利用し、ユーザー体験を最適化するためのフレキシビリティを確保するために、サーバーが送信するUIの指示を抽象化しています。これにより、クライアントはサーバーからの指示を解釈し、最適なUIを表示することができます。
上記の解決策を適用する手段としてGraphQLを使用したサーバードリブンUIの実装が考えられそう。
まとめ
SDUIはバックエンドがUIとデータを一緒に制御するパラダイム。
AirbnbのSDUIシステムは、この概念を具現化しWeb、iOS、Androidの各クライアントで迅速にイテレーションを行い、安全に機能をリリースすることを可能にしています。
SDUIはクライアントがデータをUIに変換するための複雑なロジックを持つ必要がなくなるため開発の複雑さを軽減します。また、ABテストを行う際にも大きな利点を提供します。
しかしSDUIにはパフォーマンス、複雑さ、フレキシビリティといった課題がありますが、Airbnbはこれらの課題を解決するための具体的な手法を提供しています。
これらの点を理解し、適切に対応すれば、SDUIはアプリケーション開発の新たな可能性を広げることができます。
この記事が気に入ったらサポートをしてみませんか?