目次
この記事は、丁寧に学ぶフロントエンドアーキテクチャの第3章です。
いいね・ストックをよろしくお願いします!
はじめに
本章では、いよいよフロントエンドの複雑さの本質に迫ります。
ユーザーインタラクションによる動的なアプリケーションの操作は、保守性を下げる要因です。対して、情報を単に提示するだけのアプリケーションは比較的保守が簡単です。なぜなら、状態を持たず、複雑な機能を持たず、テストシナリオも1通りだからです。
アーキテクチャの力で、複雑なインタラクションを整理し、簡潔かつ効果的に実装できることを目指します。
ユーザーインタラクションの概要
動的なWebアプリケーションは、単にユーザーに情報を表示する以上の機能を持ちます。例えば、ECサイトであれば商品をカートに追加する処理は、フロントエンドとバックエンドが協調し、データとUIを変更します。
ViewとBFFの仕組みをうまく活用して、適切な設計を行いましょう。
ユーザーインタラクション
ユーザーのブラウザ上の何らかの操作に応じてフロントエンド・バックエンドが協調し、処理の実行・UIの変更を行うこと
インタラクションの種別
単にインタラクションと言っても、その複雑度は様々です。設計上のレイヤーと対応付けながら、複雑度ごとに最適な設計を探りましょう。
まず、すべてのインタラクションはブラウザから始まります。したがって、インタラクションの複雑度は、どこまで「深く」影響するかによって決まります。
物理画面型
アコーディオンの開閉は、物理画面内でのインタラクションです。インタラクションの有無は表示内容のみに影響し、論理画面に影響しないからです。したがって、影響範囲はViewのみであり、比較的単純です
論理画面型
ユーザーが画面のリストビューとグリッドビューを変更した場合、表示内容は変わらないのでバックエンドでは何ら変更はされませんが、論理画面の構造は大きく変わるため、BFFが必要です。この場合の影響範囲はViewとBFFの両方です
バックエンド型
ブックマークボタンのチェックの切り替えは、サーバーサイドの処理を含むインタラクションです。単に表示が切り替わるだけでなく、システム内部での状態変更(具体的には、ユーザーとアイテム間のブックマーク関係の更新)によって、必然的に論理画面の状態が変更され、結果的に物理画面の内容も変わった、という仕組みになっているはずです
まとめ
インタラクションの種別
- 物理画面型:Viewのみで完結する
- 論理画面型:View、BFFで完結する
- バックエンド型:バックエンド含めすべてが影響される
インタラクションの基本アーキテクチャ
すべてのインタラクションの種別において、各レイヤーが果たす役割は同じです。異なっているのは、どこまで深くまで影響するかだけです。そこで、最も複雑なバックエンド型のインタラクションを題材に、インタラクションの基本アーキテクチャを説明します。
基本的な考え方はこれまで通りです。Viewがブラウザを意識した描画、BFFが画面へと整形・加工、バックエンドがシステムの整合性を担保、という分担をします。
1. イベントの捕捉
イベントとは、いわゆるaddEventListener
におけるイベントの意味です。ブラウザがユーザーの何らかの操作に応じて発生させるシグナルのことです。ブラウザは開発されているアプリケーション上の意味を知らないので、単に「この<button>
が押された」というようなイベントを発行します。Viewの役割は、これに意味を与えることです。
すでに説明した通り、Viewは画面構造の変更を行いません。単に指示された論理画面通りの描画を行うためのHTMLを生成するのみです。動作確認や単体テストが難しいレイヤーであるため、シンプルに保つことが重要だからです。したがって、イベントを補足しても即座に画面を変更することはできません。Viewは、ブラウザからのイベントを、「ユーザー登録ボタンが押された」のように、論理画面にとって意味のある単位に変換し、BFFに伝えます。このような、アプリケーション上の意味を明確に持った論理画面上のインタラクションをアクションと呼びます。
単に変換するだけで条件分岐も繰り返しもない処理であるため、Viewをシンプルに保つことができます。
BFFは、ブラウザのことを知らないので<button>
のイベントと言われても理解できないので、Viewによる変換が必要なのです。
Viewは、ブラウザ上のイベントを解釈して、論理画面上のアクションに変換する
2. アクションへの対応
アクションが得られれば、その画面で何をするべきかが定まります。アクションに対応する処理は「登録画面の作成ボタンを押した」のように、画面に関連したものです。したがって、BFFで処理される必要があります。
バックエンドは、特定の画面に依存しない汎用的なシステムの機能を提供するため、バックエンドを適宜呼び出すことで、アクションが実現したいことを行います。
例えば、ユーザー作成APIを呼び出し、作成したユーザーの認証関連のAPIを呼び出し…といった内容を行います。
BFFは、論理画面上のアクションに対応して、適切にバックエンドを使ってアクションに対応する処理を実現する
3. 論理画面の構成
BFFは、バックエンドの呼び出し結果を用いて、新たに論理画面を構成します。論理画面は、最初の描画のように画面全体をすべて返すのではなく、アクションによって更新される画面の一部のみを表現するものを返します。
この処理については、基本的に通常の描画とインタラクションで同一の内容になります。
BFFは、アクションの実行結果に対応する論理画面を作成し、Viewに返す
4. 結果の反映
Viewは、通常通り論理画面を物理画面に変換します。ただし、初期描画とは異なり、実行場所は必ずクライアントサイドです。アクションでは画面全体を生成せず、必要に応じて一部分を書き換えるため、DOM操作が必要になります。SSRのような、最初にすべてのHTMLを生成し、静的な内容だけを返す仕組みでは実現できず、クライアントサイドスクリプトが必須です。
ここでも、Viewは論理画面を物理画面に変換するだけなので、条件分岐や繰り返しを避けられて、シンプルに保つことができます。
Viewは、BFFの結果に基づいて物理画面を更新する
物理画面型インタラクション
すでに説明した基本的なインタラクションの処理アーキテクチャでは、Viewのシンプルさが重視されていました。しかし、物理画面で完結するインタラクションでは、BFF以降に処理を任せることができないため、Viewが主要な役割を担うことになります。
例えば、ホバーによるツールチップの表示、カルーセルの移動操作などは、本質的に表示している内容の論理的な意味は変わらず、単に画面が見やすさのために書き変わっているだけです。
物理画面型インタラクションは、装飾しか行うことができません。しかし、装飾には最適です。なぜなら、他のインタラクションとは違って、サーバーと通信しないため、高速だからです。装飾に求められる快適な速度が実現できます。さらに、失敗してもデータ整合性に問題がないため、セキュリティ上の考慮をする必要もありません。
物理画面型インタラクションは、動的な画面装飾機能において中心的な役割を担い、ユーザービリティ向上に役立つ
状態の設計方針
物理画面型インタラクションでは、ブラウザで完結する操作を行います。動作が複雑になってくると、コンポーネントが状態を持つ必要があります。例えば、アコーディオンを実装する場合は開閉状態を保持しておく必要があります。
しかし、状態を持つことはViewの複雑性を高めるため、丁寧に設計する必要があります。
まず、ブラウザが無いと検証できない(単体テストが困難な)複雑性を抱え込んでしまいます。
次に、UIの更新は複雑です。なぜなら、画面上に存在する大量のDOM要素を、状態に応じて適切に更新する必要があるからです。考える要素が多いだけでなく、前回の状態とか前回のDOMの場所とか差分更新とかが複雑に絡み合うため、余計に複雑です。
Reactは、useState
と仮想DOMによってこの複雑性を緩和してくれます。
ただし、テストの困難性という問題はReactでは解決できません。そのため、状態を持つUIコンポーネントの設計方針は次のようになります。
UIコンポーネントの状態
- UIコンポーネントが状態を持つと、保守性が下がる。なるべく別の方法で実現できないかを模索するべきである
- UIコンポーネントに状態を持たせる場合、なるべくReactを使うべきである
Astroのコンポーネント
Astroでは、UIコンポーネントを2種類用意しています。
Astroコンポーネントは、サーバー上でHTMLとして完成させ、静的なページとして返却されるUIコンポーネントです。簡単に言えばサーバーサイドレンダリングです。
UIフレームワークコンポーネントは、Reactなど、様々なフレームワークによって管理されるUIコンポーネントです。実装上は、UIフレームワークがブラウザ上に用意され、クライアントサイドでDOMの生成が行われます。
Astroコンポーネントの方が単純かつ標準的な記法であり、動作が予測しやすく、動作も高速です。ほとんど標準的なサーバーサイドTypeScriptとHTMLのみで作成され、独自の文法は少ないです。したがって、状態を持たないコンポーネントであれば、Astroコンポーネントで記述するのが望ましいでしょう。
しかし、状態を持つコンポーネント、つまり、物理画面型インタラクションで内容を動的に変更させる、状態を持つコンポーネントであれば、Reactで書くべきです。Astroでは、必要な部分に限ってUIフレームワークコンポーネントを使えるようになっています。
これは、Astroの設計思想であるIslandsアーキテクチャの概念図です。色が付いたコンポーネントに限り、動的にフロントエンドで内容を生成(ハイドレーション)しています。図は以下のサイトからの引用です。
Islandsアーキテクチャ
Astroでは、状態を持たないコンポーネントと持つコンポーネントで最適なコンポーネントを選択し、必要に応じて切り替えられるようになっている。
初期描画とインタラクション
ここまで、ユーザー起点のインタラクションについて説明してきました。
実は、フロントエンドアーキテクチャの視点からすると、初期描画(画面を読み込んだときに最初に表示する内容を作成する処理)とユーザーインタラクションは同様のものとして考えられます。ある種の一般化ができるのです。
- ブラウザがWebページのリクエストというイベントを発生させる
- 自動的にWebページのレンダリングというアクションが発生する(Viewによるイベントからアクションへの変換は自動的に行われる)
- BFFにより論理画面が作成され、AstroコンポーネントがSSRによって物理画面を作成する
- クライアントサイドでUIフレームワークコンポーネントが物理画面を作成する
初期描画とインタラクション
処理の流れだけを考えれば、初期描画の処理もユーザーインタラクションの一種と考えられる。
異なるのは、サーバー側でのHTML生成(SSR)が実行されることと、イベントからアクションへの変換が自動化されていることである。
Astro Actionsによるアクションの実装
Astro Actionsは、Astroが提供するフロントエンドからレンダリングサーバーの関数を呼び出すための仕組みです。基本的にレンダリングサーバーの機能を呼びだすためにはHTTP通信が必要なため、煩雑なコードが必要なのですが、Astro Actionsを使うことで、次の利点が得られます。
- ボイラープレートのコードが自動生成され、HTTP通信を意識せずに済む
- zodで型が付き、補完が効く
詳細は公式ドキュメントを参照してください。
Astro Actionsを利用することで、ユーザーインタラクションにおいてアクションを呼び出すことができます。つまり、クライアントサイドスクリプトがイベントを検知し、論理画面上のアクションに対応したAstro Actionsを呼び出すのです。
import { defineAction } from 'astro:actions';
import { z } from 'astro:schema';
export const server = {
// ここでアクションに対応するAstro Actionsを定義
myAction: defineAction({ /* ... */ })
}
クライアントサイドスクリプトからは次のようにアクションを呼び出します。
<script>
// このスクリプトがブラウザのイベントに合わせて実行される
import { actions } from 'astro:actions';
async () => {
// 論理画面上の意味を与え、アクションを呼ぶ
const { data, error } = await actions.myAction({ /* ... */ });
}
</script>
煩雑な部分を肩代わりし、ただの関数呼び出しのように見えるため、自然とアーキテクチャ境界のことだけ考えればよいことになります。技術的にどの部分で実行されるかよりも、処理の内容がViewかBFFかを意識することの方が重要です。
おわりに
この章では、ユーザーインタラクションの扱い方を説明しました。
これで、システムからユーザーへの情報提示(バックエンドからフロントエンドへの情報伝達)とユーザーインタラクション(フロントエンドからバックエンドへの情報伝達)の両方を説明したことになります。
通常の動作をする範囲でのフロントエンドアーキテクチャの基礎はこれで完成しました。
ここからは、テスト時の動作や高速化のための工夫などが中心になっていきます。