diff --git "a/source/_posts/20241111a_\343\203\225\343\203\255\343\203\263\343\203\210\343\202\250\343\203\263\343\203\211\343\203\225\343\203\254\343\203\274\343\203\240\343\203\257\343\203\274\343\202\257\343\201\213\343\202\211\343\202\265\343\203\274\343\203\220\343\203\274\343\201\253\343\202\242\343\202\257\343\202\273\343\202\271\343\201\231\343\202\213\343\203\221\343\202\277\343\203\274\343\203\263.md" "b/source/_posts/20241111a_\343\203\225\343\203\255\343\203\263\343\203\210\343\202\250\343\203\263\343\203\211\343\203\225\343\203\254\343\203\274\343\203\240\343\203\257\343\203\274\343\202\257\343\201\213\343\202\211\343\202\265\343\203\274\343\203\220\343\203\274\343\201\253\343\202\242\343\202\257\343\202\273\343\202\271\343\201\231\343\202\213\343\203\221\343\202\277\343\203\274\343\203\263.md" new file mode 100644 index 00000000000..3429a5f5f01 --- /dev/null +++ "b/source/_posts/20241111a_\343\203\225\343\203\255\343\203\263\343\203\210\343\202\250\343\203\263\343\203\211\343\203\225\343\203\254\343\203\274\343\203\240\343\203\257\343\203\274\343\202\257\343\201\213\343\202\211\343\202\265\343\203\274\343\203\220\343\203\274\343\201\253\343\202\242\343\202\257\343\202\273\343\202\271\343\201\231\343\202\213\343\203\221\343\202\277\343\203\274\343\203\263.md" @@ -0,0 +1,180 @@ +--- +title: "フロントエンドフレームワークからサーバーにアクセスするパターン" +date: 2024/11/11 00:00:00 +postid: a +tag: + - フロントエンド + - React + - Next.js + - サーバーアクション + - サーバーコンポーネント +category: + - Programming +thumbnail: /images/20241111a/thumbnail.png +author: 澁川喜規 +lede: "僕が触り始めた頃のウェブフロントエンド開発はデバッガーもなく、ダイナミックHTMLと呼ばれて文字をチカチカさせたりするようなものでした。IE6という超安定ブラウザが出てきたり" +--- +僕が触り始めた頃のウェブフロントエンド開発はデバッガーもなく、ダイナミックHTMLと呼ばれて文字をチカチカさせたりするようなものでした。IE6という超安定ブラウザが出てきたり(Netscape 4.xも7.xも不安定だった)その後jQueryが登場したときは、天使が降臨したように思えたものです。 + +そこから長い年月が経ち、ウェブフロントエンドの比重が大きくなるにつれ、フロントエンドのコードはどんどん複雑化しました。OpenAPIなどのコードジェネレータなども普及した結果、通信というものが隠され、イベントの中で``await``や``.then()``で呼ばれる何か、みたいな理解をしているメンバーも今後増えていくのではないかという懸念があります。 + +現在ではウェブフロントエンド開発はReactやVueといったフレームワーク上で行われ、イベントというのはそのフレームワークの提供するライフサイクルイベントに対応付けられて処理されます。この手の原理原則の理解というと「フレームワークを全部ひっぺがせ!そしたらシンプルだ!」みたいな言説もよく見かけますし、ブラウザを実装しよう的な記事も増えました。しかし、藤井九段を理解するのに、コマの動かし方の本を読んでも遠すぎるし、恋愛小説について解説するのに「ドーパミンとアドレナリンが」という話をすると1000年の恋も動物的物語になって幻滅してしまうのと同様(そちらの方が趣向に刺さる人もいるかもしれませんが)、いくら勉強しても普段の実装力がつくかというと微妙な気がしています。 + +# 現代のウェブフレームワークの構成 + +ブラウザが持っている基本機能は「HTMLを表示する」しかないため、雑な表現をすれば、動的なウェブアプリケーションは最終的に「HTMLを組み立てる」のが仕事です。HTMLはWebサイトのテキスト表現なので、正確にはHTMLを読み込んで作られるDOMという内部の木構造のドキュメントのオブジェクトツリーを動的に書き換えます。 + +現代のウェブフレームワークであるReact, Vue, Angular, SolidJS, Svelteなど、ほとんどのフレームワークはどれも同じ構成をしています。コンポーネントと呼ばれる部品を作り、それを階層構造に並べていきます。そのコンポーネントにはHTMLタグへの変換ルールが記述されており、処理の中でHTMLが作られて表示されます。コンポーネントの親子関係とHTMLの親子関係は基本的に一致しています。 + +## ライフサイクルメソッド + +フレームワークは初期化時に指定された特定のDOM要素以下を自分の管轄下の自由にしていい階層としてDOMを操作していきます。そのDOMのところにルートのコンポーネントを配置します。配置された後にいくつかのライフサイクルのタイミングでコールバックが呼ばれます。 + +通常は初期化時は以下のようなイベントが順番に自動で呼ばれます。だいたいどのフレームワークでも共通です。 + +1. createイベント: 初期化時に呼ばれる +2. renderイベント: ここでDOMの設計図を作る +3. mountイベント: DOMが反映されてブラウザ上に表示される + +DOMを作るときに、ボタンなどのフォーム要素にもイベントが設定されます。そのボタンをクリックすると、何かしら情報更新が行われたりページ遷移などが発生したりします。 + +4. unmountイベント: コンポーネントがこれから削除される(DOMはまだある) +5. deleteイベント: コンポーネントがDOM上から削除される + +Vue.jsは公式にこのライフサイクルをドキュメントに乗せています。 + + + +https://vuejs.org/assets/lifecycle.MuZLBFAS.png + +Reactの以前のクラスコンポーネントはVueそっくりな感じでした。現在の関数型コンポーネントのReactの公式では図はないのですが、だいたいこんな感じです。`useInsertionEffect()`はCSS in JSライブラリがスタイルを挿入する目的、`useLayoutEffect()`はサイズの変更など、描画後に実行すると画面のちらつきに影響するような特殊ケースで使うので、基本的には`useEffect()`だけをみておけば問題ありません。`useInsertionEffect`はドキュメントではDOM操作の前後どちらかで実行とありますが、ここでは前の方に書いています。 + + + +Reactにおいては、`useEffect()`はコンポーネント初期化時だけではなく、特定の状態に関連して引き起こされる汎用な「(副)作用」を表します。コンポーネントの状態変化も、属性の状態変化も両方等しく扱う、ジェネリックな作用となっています。1つのコンポーネント内部で複数の作用を定義できますし、ドキュメントを見ると「コンポーネントのライフサイクル」と表現するのは不適切で、効果自身がそれぞれライフサイクルを持っていて、コンポーネントはそれらが属している物、ぐらいの扱いになっています。 + +## ``fetch()`` + +``fetch()``がデータ取得の基本要素です。時代を作ったのはXMLHttpRequest(XHR)ですが、今後は``fetch()``だけをみておけば良いでしょう。 + +ウェブアプリケーション開発でHTML生成がサーバー側の役割で、作成するコードのほとんどのがJavaだったりした時代だと、POSTメソッドで情報取得をするといったものも過去ありましたが、現在では少数派でしょう。 + +何か通信を行うというときは基本的にはこの`fetch()`が最後に呼ばれます。このエントリーでは触れませんが、サーバーからストリーミングで結果を随時受け取るような場合には`fetch()`の最近の更新で追加されたStream対応でもできますが、WebSocketやServerSentEventも使われます。 + +# 初期化ライフサイクル + +ウェブ画面の表示時に最新情報を取得してそれを詰めこんだ画面を表示します。いくつかの作戦があります。 + +## 初期化時のライフサイクルメソッドの中から情報取得 + +一番シンプルなのが初期化のライフサイクルメソッドからのサーバーアクセスです。Reactでは`useEffect(処理, [])`でコンポーネントが画面に表示された直後に呼ばれるロジックが記述できるので、ここでサーバーのデータアクセスを行います。Vue.jsのComposition APIだと`onMounted(処理)`ですね。 + + + +「コンポーネントの初期化のイベントハンドラで必要な情報を取得する」というのはコンポーネント単位で見れば独立性が高く、一番ナイーブでソフトウェアの設計としては正しい姿ではありますが、ユーザーがレンダリング結果を見るまでの行程が長く、時間がかかります。特に通信待ちが2往復あります。レスポンスの結果に画像データのリンクが含まれていて、それが画面に``タグとして置かれてからブラウザがその情報を取得しにいくとしたらさらに1往復追加されます。 + +1. フロントエンドのJSコードの取得(多いと数MB) +2. JSをロードしてコンポーネントの描画(First Paint) +3. サーバーへのデータリクエストとレスポンス待ち +4. 再描画 + +Reactを開発していると「`useEffect()`の使用は最小限に」と言われます。最上位の親コンポーネントを除けば、公式ドキュメントの[この場合は使うなユースケース集](https://ja.react.dev/learn/you-might-not-need-an-effect#how-to-remove-unnecessary-effects)にコンポーネント単位でのこの方法は載っていません。Reactは初期化の後処理などが怪しいアプリケーションを炙り出すためにStrictModeかつ開発モードでは[マウント時の`useEffect()`が2回呼ばれる](https://ja.react.dev/reference/react/useEffect#connecting-to-an-external-system)ようになっていたりする点は要注意です。 + +## Stale-While-Revalidate + +ブラウザ本体のキャッシュ戦略のStale-While-Revalidateのアイディアをウェブフレームワークに取り入れたデータ取得のライブラリに、Vercelが開発した[SWR](https://swr.vercel.app/ja)があります。Vueにもそれをインスパイアして作られた[swrv](https://docs-swrv.netlify.app)があります。 + + + +Stale-While-Revalidateはキャッシュがあればまずはそれを返して表示してしまって、裏でこっそりとサーバーにあとから情報を取得しにいき、変更があれば更新をするといった動きをします。キャッシュはLocalStorageなどにも入れられますし、メモリにキャッシュする場合でも、他の箇所で同じURLにアクセスしていた場合はそれを即座に返します。 + +最初に紹介したライフサイクルメソッドと比べると、ロード中かどうかの管理やとってきた情報をstate化するところもSWRが面倒をみてくれるため、コードはシンプルになります。なお、キャッシュがない場合の最悪ケースのパフォーマンスは、ライフサイクルメソッドからの`fetch()`と同じです。 + +## サーバー側で取得して一緒に送信 + +Next.jsやNuxt.jsで一般的になったサーバーサイドレンダリング(SSR)はさらに攻めた最適化を行います。前述の2つはウェブサイトが初回レンダリングされてから初めてサーバー通信を開始します。スタートが遅くなれば最終的な結果が得られるのも遅くなります。サーバー上で必要なコンテンツを全て集めてそれを初回のレスポンスに一緒に返してしまえばよい、レンダリングも終わらせて完成系のHTMLを返せばSEO上も良いと考えられたのがSSRです。 + +サーバー上でAPI呼び出しを行いその結果を使ってページに必要な情報をまずはまとめて取得します。Next.jsであれば`getServerSideProps`を、Nuxt.jsは`useFetch()`や`useAsyncData()`をつかってサーバー上でデータアクセスを行います。 + + + +図では便宜上サーバーを1つしか書いてないですが、他のAPIサーバーがある場合なども同様です。ブラウザ↔︎サーバー間よりも、サーバーから他のAPIサーバーや自分自身の方が距離の方が一般的に近いため、往復の時間ロスが少ないため、ブラウザから情報をわざわざ取りに行くよりも高速です。現代のフロントエンドアプリはサイズも大きいため、JSがロードされて結果が表示されるのも時間がかかります。SSRでは初回はレンダリング済みのHTMLを返すため、初回表示が最速です。あと、一度結果を表示させたあとにReactコンポーネントをロードしてレンダリングして返していますが、これはハイドレーションという処理になっており、フロントでReactで作り直すことで、イベントハンドラ類が全部きちんと設定された完全なアプリケーションになります。 + +初回表示はサーバー側でレンダリングしたHTMLを返しますが、そこからページ遷移して新しいページを表示するときは、レンダリングに必要な、`getServerSideProps`や`useFetch()`や`useAsyncData()`をサーバーで実行し、結果のJSONだけをブラウザに送って描画します。 + +問題点としては、サーバー側の技術スタックがNode.jsなどのJavaScript系の処理系にする必要があったりします。Next.jsではページトップのコンポーネントにしか`getServerSideProps`が書けないため、親子関係の依存が強くなりがちといった問題もあります。 + +ブログやニュースなど閲覧者ごとに違いがないページでしか使えませんが、Next.jsではさらに情報の取得を事前に静的に行ってHTMLを生成しておく、静的サイトジェネレーション(SSG)もあります。 + +## サーバーコンポーネント + +Reactがパフォーマンス改善のキーとして現在取り組んでいるのがサーバーコンポーネントです。Vue.js本体が取り入れるかはわかりませんが、Nuxt.jsも[実験的に取り組んで](https://nuxt.com/docs/guide/directory-structure/components#standalone-server-components)います。 + +Reactはコンポーネントは再描画のたびに実行される前提であったため、通信コードは`useEffect()`などのライフサイクルメソッド側に書く必要がありました。しかし、Reactサーバーコンポーネントが登場したことで、「必ず一度だけサーバー側でレンダリングされるコンポーネント」が登場しました。通常はコンポーネント定義では仮想DOMの構築以外はせず、サーバーアクセスは`useEffect()`でやるのが通例でしたが、サーバーコンポーネントでは直接サーバー通信コードが書けます。 + +```tsx +export function AsyncComponent() { + const data = use(fetchMessage()) // サーバー通信呼び出し + return ( +