目次
この記事は、丁寧に学ぶフロントエンドアーキテクチャの第2章です。
いいね・ストックをよろしくお願いします!
はじめに
前章では、画面に対応するデータを実際のブラウザの表示につなげるまでの部分をViewとして定義しました。
この章では、ViewとBFFについてより深く解説し、的確に設計できるような知識を解説します。
レイヤー分割の方針
設計原則
保守性を高めるうえでは、レイヤー分割を考えることが重要です。
レイヤー分割の設計原則
- 重たい部分を分離することで、テストをしやすくする
- 汎用的な部分を分離することで、再利用性を高める
- 分かりやすい責務とインターフェースをレイヤーに割り当てることで、読みやすさと書きやすさを高める
特に、どのような処理をどのレイヤーに書くべきかを明確に判断可能であることが重要です。
あまりに細かいレイヤー分割をすると、追加したい処理がどのレイヤーに所属するのか判別しにくいケースが発生してしまいます。その結果、レイヤー分割を無視した実装が横行し、分割の意味を失ってしまいます。
単にレイヤーを増やせば手の込んだよい設計になるというわけではない
フロントエンドにおけるレイヤー
設計原則に基づいて、レイヤー分割の必要性を考えます。
- 重たい部分の分離:ブラウザを起動する必要がある処理は重い
- 汎用的な部分の分離:画面の論理的な構造は、具体的なUIの構成方法・見た目よりも汎用的である
このような分析により、ブラウザを必要とする部分と、ブラウザを必要としない画面構造を担当する部分をレイヤー分割する必要性を認識できます。これは前章で説明したViewとBFFに対応しています。
これに加えて、レイヤーの責務を明確にすることで、開発者がどの処理をどこに書くべきかを判断できるようにする必要があります。そのための概念が、次節で説明する論理画面と物理画面です。
先に、全体像を表した図を示します。
論理画面
論理画面とは、画面に表示する情報・意味内容を、画面の構造に即してそのまま表現したデータ構造である
端的に言えば、画面の構造を表現するJSONのスキーマが論理画面です。
論理画面に含まれるのは、HTMLなどのWebフロントエンド特有の技術が含まれないデータであるため、バックエンドエンジニアでも容易に理解することができます。
BFFは、論理画面を作成するための処理です。
具体的な論理画面の例
QiitaのユーザーランキングのUIを例に考えましょう。
この画面に対応する論理画面は、次のようになります。
{
"week": [
{
"id": "KNR109(かずのり)",
"icon": "ユーザーアイコンのURL",
"contributionCount": 1729,
},
その他ユーザー情報
],
"month": [
ユーザー情報が10人分
],
"all": [
ユーザー情報が10人分
]
}
論理画面の特徴は、UIに書かれている情報だけを含んでいることです。実際には、ユーザー情報には連携しているSNSのリンクやパスワード、プロフィールなどが存在していますが、論理画面では画面に表示するものだけを含めるべきです。
論理画面の状態
論理画面は、表示機能を持たない画面です。そのため、システムのユーザーが画面単位で保持したい情報は、論理画面の状態になります。BFFは、このような状態を保持しつつ、論理画面を作成します。
論理画面の状態の性質
- 接続しているユーザーを意識した状態であるため、バックエンドシステムの状態ほどの汎用性がない
- 特定の画面または複数の画面にわたって広く意味を持つ状態であるため、特定のUIコンポーネントだけで閉じていない
論理画面の状態の典型的な例は、ログイン状態の管理です。ログイン状態は、接続しているユーザー単位の情報であり、バックエンド全体で意味を持つ汎用性はありません。しかし、複数の画面間にわたって利用される情報です。
技術的には、論理画面の状態はセッションとして管理されます。つまり、Cookieを使ってセッションIDを保持し、BFFがセッションIDと論理画面の状態の対応関係を参照するのです。
サーバーサイドとの違い
サーバーサイドでは、整合性やセキュリティが重視されます。したがって、サーバーサイドの状態は整合性を保ったDBに正規化された状態で保持され、データに対する操作はドメインを用いて正しい方法のみが許可される場合が多いです。
対して、フロントエンドでは、ユーザビリティが重視されます。そのため、論理画面におけるデータは、より画面に即した形で保持されます。
論理画面におけるデータ型
論理画面のデータ型は、UI単位、表示される内容単位で作成される。
(サーバーサイドのデータ型は、意味単位で作成される)
例えば、求人サイトを作成していたとします。求人情報はサーバーサイドでは基礎的な情報なので、全ての情報を持ったJobテーブルやJob型が作成され、それがユースケースで使われていることが多いです。ユースケースでJobの情報の内いくつかのプロパティしか使わないとしても、全ての情報を持ったドメインオブジェクトを引数や戻り値に使います。なぜなら、ドメインオブジェクトを使うことで、Job全体の整合性を担保できるからです。
しかし、論理画面では、整合性は重要ではありません。大切なのは、画面構造と対応する情報が適切な場所に存在することです。求人サイトで求人情報を表示するUIがあったとして、求人一覧画面と求人詳細画面で、表示する情報が違うのであれば、同じ「Job」という名前で会っても、要求されるデータ型は異なります。
同じ名前だからと言って無理に統一しようとすると、不自然な画面が作成されてしまいます。
論理画面における型定義
論理画面では、UIで表示する内容ごとにデータ型が決まるため、サーバーサイドほど一貫した型を使うことはできない。
物理画面
物理画面とは、画面に表示する内容をブラウザが解釈できる形にしたデータ構造である
端的に言えば、ブラウザ上にダウンロードされている各種アセットが物理画面です。
Viewは、物理画面を作成するための処理です。物理画面はブラウザによるレンダリングの必要があるため、フロントエンドエンジニア以外にとって理解が難しいコードを記述する必要があります。
具体的な物理画面の例
QiitaのUIは、次のようなHTMLの構造を持つことが予想できます。このような、ブラウザが直接解釈できる内容が物理画面です。
<div class="container">
<!-- Weekly Section -->
<div class="section">
<h2>週間ランキング</h2>
<ul class="user-list">
<!-- ユーザー情報の例 -->
<li>
<img src="ユーザーアイコンのURL" alt="User Icon">
<div class="user-info">
<span class="user-id">@KNR109(かずのり)</span>
<span class="contribution-count">Contributions: 1729</span>
</div>
</li>
</ul>
</div>
<!-- Monthly Section -->
<div class="section">
<h2>月間ランキング</h2>
<ul class="user-list">
<!-- ここに月間ユーザー情報を追加 -->
</ul>
</div>
<!-- All-Time Section -->
<div class="section">
<h2>全期間ランキング</h2>
<ul class="user-list">
<!-- ここに全期間ユーザー情報を追加 -->
</ul>
</div>
</div>
具体的なViewの例
画面を意識した処理を、BFFに配置するべきかViewに配置するべきかを明確にするため、Viewに対応する処理の具体例を挙げます。
- レイアウトを意識した処理
- ピクセルという単位を使う処理はViewに配置するべきです
- 大きさが画面構造に論理的な意味を持っている場合でも、論理画面では
Large
やSmall
として扱い、物理画面では24px
や16px
になります
- ブラウザ上の操作に反応するが、アプリケーション上の意味は存在しないUIの処理
- バックエンドAPIを呼び出す必要はない、単にユーザビリティを向上させるためのブラウザ上で動作するスクリプトです
- 例えば、アコーディオンの開閉状態によってDOMを変更する処理です
物理画面の状態
物理画面レベルでも状態を管理する必要があります。具体的には、単一のUIに影響範囲が閉じるような汎用性が低い状態は、物理画面の状態です。
物理画面の状態の性質
- 特定のUIのみで使われる
- 状態が変更されても、論理画面レベルでの影響がない
- たいていの場合、ユーザーのブラウザ上での操作のユーザビリティを高めるためだけの状態
- 最悪の場合、なくても動作する
例えば、アコーディオンは開いていようが閉じていようが、表示したい内容は変わらないため、論理画面レベルの影響がないと言えます。
物理画面の状態は、技術的にはJavaScriptで管理されます。特に、React
のuseState
などのUIフレームワークによって管理される場合が多いです。
モダンなフロントエンド開発では、状態を管理する必要がある程度に複雑なコンポーネントでは、UIフレームワークを使うことで複雑性を緩和できるため、望ましいです。
状態の三層
ここまでで解説したレイヤー分けでは、処理、状態を適切に配置することが重要です。
ここでは、状態の性質と重視される品質ごとに、どのような技術選定を行うことが合理的であるかを解説します。
まずは、全体像を示す図を示します。
バックエンドのデータストア
バックエンドでの状態はDBに保持されます。
RDBでは、正規化されたテーブル構造とACID特性を持つトランザクションにより、整合性が保たれます。これは、バックエンドで要求される品質特性と相性が良いです。また、専用のDBサーバーとして稼働し、複数人による並列アクセス制御ができるため、バックエンドの重要な品質特性を満たしやすいです。
BFFのデータストア
BFFの状態とは、論理画面の状態です。BFFはサーバー上で実行されます。
複数のユーザーで保持されるほどではないですが、複数の画面間で保持されるような内容です。
データの性質上、データの耐久性はそこまで重視されません。
そこで、Redisなどを使ってセッションIDと論理画面の状態の対応関係を保存することが多いです。
Viewの状態
Viewの状態は、物理画面の状態です。これは、ブラウザのメモリ上に保持されます。物理画面の状態は、それがどのような値を取ったとしても、論理画面に影響しません。したがって、失われたときの影響は軽微です。しかし、物理画面の状態はユーザ体験と密接に関係しているため、高速な反応が求められます。したがって、通信を発生させずに高速に対応できるブラウザのメモリ上に保持するのがふさわしいのです。
ガイドライン
状態設計のガイドライン
- DBには、複数ユーザーで共有されるシステム単位の情報を保持するために、DBを使う
- 論理画面の状態には、複数画面で共有されるユーザー単位の情報を保持するために、サーバー上でのインメモリキャッシュを使う
- 物理画面には、単一UIで保持されるUI単位の情報を保持するために、ブラウザのメモリを使う
レイヤー間のインターフェース
設計原則
BFFとViewの違いを明確に理解するために、その論理的な接続内容である物理画面と論理画面について説明しました。
さて、論理的に渡すべき情報が決まったところで、次は技術的にどのようにしてそれを送信するべきかを決める必要があります。例えば、バックエンドとフロントエンドのインターフェースとして、REST APIのOpen API Specを記述することがあります。
レイヤー間のインターフェースは、アーキテクチャ上重要です。まずは、インターフェースの設計原則の重要な点を3つ説明します。
標準的な技術であること
レイヤー分割の設計原則に、汎用的な部分の分離がありました。その目的は、再利用性の向上です。そのため、レイヤー分割されたインターフェースの内容は、様々な関数から呼び出せるように、汎用的な意味を持つことが多いです。
できる限り標準的なインターフェースとすることで、システム内外で再利用しやすくなる可能性が高まります。
静的検証が可能であること
インターフェースは明確であることが求められます。
静的検証が可能な形式にするには、形式言語で明確にインターフェースを定義している場所が必要です。
例えば、Web APIを作るときに、Open API Specを書いて、接続部分のコードを自動生成させる場合、APIのパスや引数をコード内でバラバラに記述する場合に比べて、インターフェースが分かりやすいです。
モックが容易であること
レイヤー分割の設計原則に、重たい部分の分離がありました。その目的は、テストです。分割された重たいレイヤーをモックすることで、その他のレイヤーを高速にテストできるのです。
フロントエンドにおけるインターフェース
フロントエンドのレイヤーはBFFとViewです。そのため、設計するべきインターフェースはViewと人間およびBFFとViewの2種類です。
Viewと人間
Viewはレイヤー分割のユーザー側の終端であり、これより先は人間であるため、インターフェースは「見た目」とならざるを得ません。これは、人間の視覚に由来するため非常に標準的であり、画像やプロトタイピングツールを使うことで容易にモック可能ですが、静的検証は難しいです。
BFFとView
BFFとViewの間のインターフェースは、開発者が技術を選定できます。
BFFでは、論理画面を作成します。意味内容は画面を意識していても、データ構造自体は素朴なオブジェクトであるため、技術的な選択肢が豊富です。
本連載はAstroを題材としているため、Astroがどのようなインターフェースを提供しているのか、設計上の観点から解説します。
Astroコンポーネント
Astroコンポーネントは、ViewとBFFの接続を端的に示したものです。Astroコンポーネントの構造は次のようになっています。
---
// この部分はコードフェンスと呼ばれており、自由にTypeScriptを記述できる
---
<p>
この部分はHTMLを記述する。
標準的なHTMLはすべて有効なAstroコンポーネントである
必要に応じてコードフェンスで定義した変数を使った簡単なテンプレートを書くことができる
</p>
普通のTypeScriptとHTMLが記述できるので、非常にシンプルかつ標準的です。コードフェンスでは、通常のTypeScriptと同じように型が使え、モジュールのインポートができます。したがって、基本的な実装方法は次のようになります。
- BFFはTypeScriptの関数として実装する
- コードフェンスでBFFの関数をインポートし、呼び出す
- BFFの呼び出し結果の論理画面を利用して、テンプレートを記述する
TypeScriptの型付きで表現でき、静的解析が強いです。
また、関数の呼び出しを行っているだけなので、関数を別のモックで差し替えることができ、モックも簡単です。
Astroコンポーネントの構造は、BFFとViewの標準的かつ合理的な接続方法である
技術的には、Astroコンポーネントはサーバー上で実行され、ブラウザにはテンプレートエンジンの実行結果のHTMLだけが転送されます。したがって、Astroコンポーネントを利用する場合は、Viewは完全にサーバーで実行され、クライアントサイドではコードが実行されません。
おわりに
この章では、フロントエンドで必要な処理や状態について、適切にレイヤー分けを行うために必要な知識を整理しました。論理画面と物理画面の分離という発想を持っていると、既存のコードベースを読むときにも有用です。たいていの場合、論理画面の生成ロジック(BFFに相当する部分)を理解すれば、物理画面の生成ロジックは抽象化できるからです。
次章以降で、このレイヤー分けを具体的にどのように実現するかを、実装ベースで説明していきます。