Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save podhmo/45365b41cae4a33cb255080bcb797c33 to your computer and use it in GitHub Desktop.

Select an option

Save podhmo/45365b41cae4a33cb255080bcb797c33 to your computer and use it in GitHub Desktop.
Inertia.jsとかServer-Driven UIとかRSCとかでの密結合による省力化 -- Uploaded via Gist Uploader - 2025-12-23T16:05:39.989Z
target_reader: Future me
objective: Webアプリケーションアーキテクチャの選定において、疎結合なSPA構成と密結合な効率化手法(Inertia, SDUI, RSC等)のトレードオフを整理し、適切な技術選定の指針とする

Webアプリケーションアーキテクチャにおける「手抜き」と「結合」のレイヤー構造

🤔 SPA開発の複雑性に対する違和感

Webアプリケーションの要件定義において、リッチなユーザー体験(SPA)が求められることはもはや標準となった。しかし、その実現手段として教科書的な「フルSPA構成」を採用しようとすると、開発コストの増大に直面する。

具体的には、フロントエンド(React/Vue)とバックエンド(REST/GraphQL)を完全に分離する構成だ。この構成では、以下の「二重管理」が発生する。

  1. ルーティングの二重化: サーバー側のAPIルート定義と、クライアント側の画面遷移ルート定義。
  2. 型定義の二重化: サーバー側のDTO1と、クライアント側のTypeScript型定義。
  3. 状態管理の複雑化: サーバーにあるデータ(Server State)をクライアントでキャッシュ・同期するための複雑なロジック。

小規模なチームや、社内ツール、プロトタイプ開発において、この「正しさ(疎結合)」はオーバーエンジニアリングではないか。サーバーとクライアントをあえて密結合させることで、これらの手間を省く「手抜き」のアプローチを整理する必要がある。

💡 ルーティングとデータ供給のショートカット (Inertia.js)

最初に検討するのは、ルーティングとデータフェッチのレイヤーでの結合だ。Inertia.jsが採用しているアプローチである。

ここでは「モダンなモノリス」を目指す。サーバーサイドフレームワーク(LaravelやRails)のルーティング機能をそのまま使い、ViewレイヤーだけをVueやReactに置き換える発想だ。

// Server Side (Laravel Example)
class UserController extends Controller
{
    public function index()
    {
        // JSON APIを作らず、コンポーネント名とデータを直接返す
        return Inertia::render('Users/Index', [
            'users' => User::all(),
            'canCreate' => auth()->user()->can('create'),
        ]);
    }
}

このアプローチの本質は、サーバーが「どのコンポーネントを表示するか」と「それに渡すデータ(Props)」を決定している点にある。

クライアント側は以下のようなJSONを受け取り、対応するファイル(Users/Index.vue)を解決して描画する。

{
  "component": "Users/Index",
  "props": {
    "users": [...],
    "canCreate": true
  },
  "url": "/users",
  "version": "..."
}

APIのエンドポイント設計や、クライアント側でのフェッチ処理(useEffectなど)が一切不要になる。ここでの結合レイヤーは「ページコンポーネント」だ。サーバーとクライアントは、物理的なファイル構成とPropsの構造で強く握手している。

💡 UI構造自体のショートカット (Server-Driven UI)

次に、さらに結合度を高め、UIの構造そのものをサーバーが決定するアプローチを検討する。Server-Driven UI (SDUI) だ。

これは、管理画面の詳細ページや、ネイティブアプリの動的なキャンペーンページなどで有効だ。Inertiaが「ページごとのデータ」を送っていたのに対し、SDUIは「UIのスキーマ」を送る。

{
  "sections": [
    {
      "type": "FormSection",
      "title": "基本情報",
      "fields": [
        { "key": "name", "label": "氏名", "inputType": "TEXT", "value": "山田太郎" },
        { "key": "dob", "label": "誕生日", "inputType": "DATE", "value": "1990-01-01" }
      ]
    }
  ]
}

クライアント側は「汎用レンダラー」となり、inputType: 'TEXT' ならテキストボックスを表示する、というルールに従うだけになる。

この手法での「手抜き」は、画面ごとのフロントエンド実装を排除することだ。新しい画面が必要になっても、バックエンドの定義を追加するだけで済む。ここでの結合レイヤーは「抽象化されたUI定義」である。

❌ クライアントロジックの排除 (Hotwire)

さらに極端な「手抜き」として、Hotwire (Turbo) のようなアプローチがある。これは pjax の現代版と言える。

サーバーはJSONではなく、レンダリング済みのHTMLフラグメントを返す。クライアントは受け取ったHTMLでDOMを置換する。

<!-- Server Response -->
<turbo-stream action="replace" target="message_1">
  <template>
    <div id="message_1">
      更新されたコンテンツ
    </div>
  </template>
</turbo-stream>

InertiaやSDUIは「データ(JSON)を受け取ってクライアントで描画」していたが、Hotwireは描画すらサーバーに任せる。もっとも密結合な形態だが、複雑なクライアントサイドのインタラクションが必要な場合、逆にJavaScriptの管理が難しくなる懸念があるため、要件を選ぶ必要がある。

🔄 「正しさ」のコストとフルSPA (TanStack Router)

ここまで検討した「密結合アプローチ」を採用できないケースについて考える。例えば、バックエンドが既存の汎用REST APIとして存在している場合や、モバイルアプリとバックエンドを共有する場合だ。

この場合、いわゆる「フルSPA」構成を選択することになる。これはサーバーとクライアントが完全に疎結合な「他人の関係」となり、APIという契約書を通して会話するモデルだ。

現代的なフルSPAの実装(TanStack Router等を利用)では、クライアント側に分厚い中間層が必要になる。

graph TD
    Router[Router / URL] --> Loader[Data Loader]
    Loader --> API[API Client / SDK]
    API --> Server[REST API]
    API --> Store[Client Cache / Store]
    Router --> Component[UI Component]
    Store --> Component
Loading

この構成では、以下のようなコストを支払うことになる。

  1. 多段の呼び出し: UIを表示するために、Router -> Loader -> API Client -> Server というバケツリレーが発生する。
  2. BFFの必要性: APIが微細すぎる(リソース指向)場合、クライアントの通信回数を減らすために、ページ単位のデータ整形を行うBFF (Backend For Frontend) を立てる必要が出てくる。これは実質的にInertiaのコントローラーと同じ役割だが、疎結合を保つためにAPIとして切り出されている。

この複雑さは、大規模開発やチーム間の分業においては「秩序」として機能するが、少人数開発では単なるオーバーヘッドになり得る。

🚀 揺り戻しとしての回帰 (Next.js / RSC)

最後に、最新の潮流である React Server Components (RSC) をこの文脈で捉え直す。RSCは、Inertia.jsが目指した「手抜き」を、Reactのエコシステム自体に取り込んだものと解釈できる。

RSCでは、コンポーネント自体が非同期関数となり、直接バックエンドロジック(DBアクセス等)を実行する。

// Server Component
export default async function Page() {
  // API層を経由せず、直接DBを叩く(究極の手抜き)
  const users = await db.query('SELECT * FROM users');
  
  return (
    <div>
      {users.map(user => <UserCard user={user} />)}
    </div>
  );
}

これは以下の点でInertiaの進化系と言える。

  1. API層の消滅: コンポーネントが実質的なBFFとなり、API定義が不要になる。
  2. コロケーション: データ取得ロジックとUI定義が同じファイルに存在するため、型定義の共有すら不要になる。
  3. 主権の回帰: ルーティングとレンダリングの主導権が再びサーバー側に戻っている。

フルSPAで肥大化したクライアント側の責務(ルーティング、データフェッチ、状態管理)を、サーバーサイドのコンポーネントツリー処理(RSC Payloadのストリーミング)に置き換えるアプローチだ。

アーキテクチャ選定の際は、単に「流行り」で選ぶのではなく、この「結合のレイヤー」をどこに置きたいか(ページ単位か、UI構造単位か、API契約単位か、コンポーネント単位か)を基準に判断すべきである。

Footnotes

  1. DTO (Data Transfer Object): プロセス間やシステム間でデータを転送するために使用されるオブジェクト。ここではAPIレスポンスの型定義を指す。

今回の対話で使われている専門用語に脚注をつけてください。脚注の中では一般的な意味と今回の対話での使われ方やより閉じた意味を説明してください。

出力形式は箇条書きのようなもので良いです


  • SPA (Single Page Application)

    • 一般的な意味: 最初に1つのHTMLページだけを読み込み、以降はユーザーの操作に応じてJavaScriptで必要なデータだけを取得・更新して画面を書き換えるWebアプリケーションの形態。
    • 今回の対話での意味: 「ユーザー体験(UX)」としての滑らかな画面遷移を指す場合と、フロントエンドとバックエンドを分離する「アーキテクチャ」としての構成(フルSPA)を指す場合があり、この2つの区別が重要となる。
  • Inertia.js

    • 一般的な意味: LaravelやRailsなどのサーバーサイドフレームワークを使って、従来のモノリスな開発体験のままVueやReactによるSPAを構築するためのライブラリ。
    • 今回の対話での意味: サーバーサイドのルーティングやコントローラーをそのまま使い、ViewレイヤーだけをSPA化する「手抜き(効率化)」の代表的な手法として扱われている。APIを真面目に作らず、ページ単位でコンポーネントとデータを結合させるアプローチ。
  • Server-Driven UI (SDUI)

    • 一般的な意味: 画面に表示するUIの構造やレイアウト情報をサーバーからAPIレスポンスとして送信し、クライアント(アプリ)側でそれを解釈して描画する設計パターン。
    • 今回の対話での意味: 画面ごとのフロントエンド実装を不要にし、バックエンドの定義変更だけで画面を追加・修正するための「究極の省力化」手法。AirbnbやDRESS CODEの事例を元に、詳細画面の量産などに適した密結合アプローチとして議論された。
  • pjax / Hotwire (Turbo)

    • 一般的な意味: リンククリックなどのイベントをインターセプトし、AjaxでサーバーからHTMLの一部を取得してDOMを置換することで、フルリロードなしで画面遷移する技術。Hotwireはその現代版(Railsのエコシステム等)。
    • 今回の対話での意味: クライアント側にJSONのレンダリングロジックすら持たせない、最もシンプルな「HTMLそのものを返す」密結合アプローチ。
  • フルSPA (Full SPA)

    • 一般的な意味: フロントエンドとバックエンドが完全に分離され、APIを通じて通信する構成。
    • 今回の対話での意味: 「正しさ」と引き換えに、ルーティングや型定義の二重管理、状態管理の複雑化などのコストを支払う構成。TanStack Routerなどを用いた現代的な実装は、疎結合ゆえの複雑さを解消するための高度な中間層を持つ。
  • BFF (Backend For Frontend)

    • 一般的な意味: フロントエンド(Web、iOS、Androidなど)ごとの要求に合わせてAPIを整形・集約する、フロントエンドのためのバックエンドサーバー層。
    • 今回の対話での意味: フルSPAにおける「多段API呼び出し」や「データの扱いづらさ」を解消するために、ページ単位やコンポーネント単位でデータを加工して返すAPIエンドポイントのこと。InertiaやRSCはこのBFFの役割を内包していると解釈された。
  • RSC (React Server Components)

    • 一般的な意味: Reactコンポーネントをサーバーサイドで実行し、そのレンダリング結果(HTMLではなく独自のRSC Payload形式)をクライアントに送信する仕組み。
    • 今回の対話での意味: コンポーネント自体が直接DBなどにアクセスすることでAPI層を排除する、Inertia.jsが進もうとした道をさらに推し進めた「究極の手抜き(効率化)」アプローチ。
  • TanStack Router

    • 一般的な意味: Reactなどのための現代的な型安全なルーティングライブラリ。
    • 今回の対話での意味: フルSPAにおいて、URLを唯一の正解(Single Source of Truth)として、データ取得(Loader)や状態管理を統制する「司令塔」の役割を果たすツール。クライアント側を賢く(Thick Clientに)するアプローチの象徴。

ちなみにgraphqlでもすべての議論は成り立つのでそこはよろしく。単なるデータ構造の定義と通信の形状のプロトコルに過ぎないので。


おっしゃる通りです。「GraphQLであること」自体は、本質的な「結合度」や「手抜き」の議論を変える魔法ではありません。GraphQLを使っても、どのレイヤーで握手するかによって、以下のすべてのパターンが再現可能です。

GraphQLにおける「手抜き(密結合)」と「正しさ(疎結合)」の写像

これまでの議論(Inertia, SDUI, フルSPA, RSC)をGraphQLの世界にマッピングするとこうなります。

1. GraphQL版 Inertia.js(ページ駆動クエリ)

  • アプローチ: 画面(Page)単位で巨大なクエリを1つ叩く。
  • スキーマ: リソース汎用ではなく、画面固有の型を切る。
    # 汎用的な User 型ではなく、この画面専用の構造
    type UserProfilePage {
      headerTitle: String!
      userData: User!
      canEdit: Boolean!
      relatedPosts: [Post!]!
    }
    
    query GetUserProfilePage($id: ID!) {
      userProfilePage(id: $id) {
        ... # 画面に必要なすべて
      }
    }
  • 特徴: RESTのBFFパターンと同じ。フロントとバックが「この画面のために」密結合する。

2. GraphQL版 Server-Driven UI(UIスキーマ)

  • アプローチ: UIコンポーネントのツリー構造をGraphQLで表現する。
  • スキーマ: データではなくUI部品を定義する。
    interface UIComponent {
      id: ID!
      type: ComponentType!
    }
    
    type TextField implements UIComponent {
      label: String!
      value: String
    }
    
    type Screen {
      sections: [UIComponent!]!
    }
  • 特徴: AirbnbのGhost Platformがやっていることそのもの。「データ構造」としてUIツリーを返し、クライアントはそれをレンダリングするだけ。

3. GraphQL版 フルSPA(リソース駆動・Fragment Colocation)

  • アプローチ: 汎用的なリソース型を定義し、UIコンポーネントごとに必要なデータをFragmentで宣言する(Relayなどの思想)。
  • スキーマ: 純粋なデータモデル。
    type User {
      id: ID!
      name: String!
      friends(first: Int): [User!]
    }
  • クライアント: UserAvatar コンポーネントは fragment UserAvatar_user on User { profileImage } を要求し、UserName コンポーネントは fragment UserName_user on User { name } を要求する。コンパイラがそれらを結合してクエリを発行する。
  • 特徴: これが「正しさ」の極地。クライアントとサーバーは疎結合だが、ツールチェーン(Relay Compiler等)によって整合性を保つ。

4. GraphQL版 RSC(Server Components)

  • アプローチ: サーバーコンポーネント内で直接GraphQL(あるいはDB)を叩き、RSC Payload(シリアライズされたReactツリー)として返す。
  • 特徴: クライアントにはGraphQLのクエリもスキーマも届かない。結果としてのUIツリーだけが届く。GraphQLは「サーバー内部の実装詳細」として隠蔽される。

GraphQLへのアンサーソングの補正

先ほどのアンサーソング(特に「APIファースト論者」や「モバイル開発者」)において、「GraphQLを使えば解決する」という反論は成立しません。なぜなら:

  • APIファースト論者への反論: 「GraphQLでも、UI構造(SDUI)や画面固有型(BFF)を返してしまえば、それは汎用リソースAPIではない」からです。LLMエージェントにとって、type TextField だらけのスキーマはノイズでしかありません。
  • モバイル開発者への反論: 「WebチームがWeb画面専用のGraphQLクエリ(BFF的利用)を乱立させた場合、モバイルチームはそのクエリを再利用できません」。結局、モバイル用に別のクエリを書くか、スキーマ自体をモバイル向けに調整する必要が出てきます。

結論: RESTだろうがGraphQLだろうが、**「UIの都合(コンテキスト)をサーバー側のスキーマ/レスポンスにどれだけ混入させるか」**という設計判断こそが本質であり、プロトコルの違いはその実装手段の差異に過ぎません。

対話履歴

ユーザー: この2つを読みSPAの手抜きの方法と密結合の部分を整理してください。後にフルのSPAの話とhotwireについても触れるかもしれませんがそれはまだ先の話です。


AI: 提供された2つの資料(Inertia.jsに関する会話と、Server-Driven UIに関する会話)に基づき、**「SPA開発における手抜きの方法(効率化)」「サーバーとクライアントの密結合」**という観点で情報を整理しました。

これらはどちらも「フルSPA(フロントエンドとバックエンドを完全に分離する)」の複雑さを回避し、サーバーサイドの資産やロジックを最大限活用するためのアプローチですが、**「何を共通化してどこで楽をするか」**のレイヤーが異なります。


1. SPAにおける「手抜き」の方法(効率化のアプローチ)

両者とも「APIを真面目に作らない(RESTful/OpenAPIなどの厳密な設計を避ける)」ことで開発コストを下げていますが、その手法が異なります。

A. Inertia.jsのアプローチ:ルーティングと状態管理の手抜き

**「モダンなモノリス」**を作るアプローチです。

  • 何を手抜きするか:
    • クライアントサイドのルーティング: 定義不要。サーバー側のURL定義(Laravel/Railsのroutes)をそのまま利用します。
    • API定義: 汎用的なJSON APIを作らず、そのページ(コンポーネント)専用のデータをコントローラーから直接渡します(Props)。
    • データフェッチ: useEffectfetchを書く必要がありません。ページ遷移時に自動でデータが注入されます。
  • 仕組み:
    • サーバーは「コンポーネント名(例: Users/Index)」と「データ(Props)」を含むJSONを返します。
    • クライアント(Inertia)は、その名前のVue/Reactコンポーネントを動的に解決して表示します。
  • メリット:
    • 「従来のMPA(マルチページアプリケーション)と同じ感覚」でコードを書きながら、ユーザー体験だけSPAにできます。

B. Server-Driven UI (SDUI) のアプローチ:UI構築の手抜き

**「汎用レンダラー」**を作るアプローチです。

  • 何を手抜きするか:
    • 画面ごとのフロントエンド実装: 新しい画面を作るたびにJSX/HTMLを書く必要がありません。
    • ロジックの重複: バリデーションルール、表示順、権限制御などをフロントエンドで再実装しません。
  • 仕組み:
    • サーバーは「UIの構造(セクション、属性、入力タイプ、順序)」を含むJSONツリーを返します。
    • クライアントは「valueType: 'TEXT'ならテキストボックスを表示」といった汎用的な描画エンジンとして振る舞います。
  • メリット:
    • 画面数がどれだけ増えても(詳細画面が100個あっても)、フロントエンドのコード量は増えません。バックエンドの定義追加だけで画面が完成します。

2. 「密結合」の整理

どちらの手法も、**「サーバーとクライアントが強く結びついている(密結合)」**ことを前提に、その結合をあえて利用することで生産性を上げています。

特徴 Inertia.js の密結合 Server-Driven UI の密結合
結合の対象 ページ単位(コンポーネントとURL) UI構造単位(スキーマとレンダラー)
サーバーの責務 「どのコンポーネントを使うか」と「それに渡すデータ」を決める。 「画面に何を表示するか(構造)」と「どう振る舞うか(メタデータ)」を決める。
クライアントの責務 指定されたコンポーネントファイル(.vue等)を持っている必要がある。 定義されたウィジェットタイプ(Text, Date, Select等)を描画できる必要がある。
契約(Contract) ファイル名とProps
サーバーがUsers/Indexと返したら、クライアントにそのファイルが必要。
抽象的なUIスキーマ
sections, attributes, valueType などの独自定義された構造。
開発スタイル フロントとバックが隣り合わせ(同じリポジトリ推奨)。「このページにはこのデータが来る」という暗黙の了解で動く。 バックエンドがUIの支配者。フロントエンドは指示に従うだけの「賢いビューアー」になる。

まとめ:どの「層」で結合しているか

  1. Inertia.js (コンポーネント・マッピング)

    • サーバー:「このファイルを使って、このデータで描画しろ」
    • クライアント:「はい、そのファイルを持ってるので描画します」
    • 「具体的な実装ファイル」で握手している。
  2. Server-Driven UI (データ・ドリブン)

    • サーバー:「ここにはテキスト入力、次は日付選択を、この順序で置け」
    • クライアント:「はい、テキスト入力と日付選択の部品なら持ってるので、その通りに並べます」
    • 「抽象的なUI定義」で握手している。

結論としての整理: SPA開発における「手抜き」とは、**「汎用的なAPI(REST/GraphQL)という疎結合な層を排除し、サーバーの都合をクライアントに直接押し付けること」**と言えます。

  • Inertiaは「ルーティングとデータ受け渡し」をサーバーの都合に合わせることで楽をします。
  • SDUIは「画面構成とUIロジック」をサーバーの都合に合わせることで楽をします。

(返答終了理由: STOP)


ユーザー: これは短く触れるだけで良いです。hotwireはpjaxの現代版でhtmlをそのまま返し最初のレスポンスもhtmlをそのまま返すアプローチですよね。


AI: はい、その認識で間違いありません。これまでの2つ(Inertia, SDUI)と比較すると以下のようになります。

Hotwire (Turbo)

  • アプローチ: HTMLそのものを返す(HTML Over The Wire)。
  • 仕組み: 初回は通常のHTML表示。以降の遷移や更新も、サーバーがレンダリングしたHTMLフラグメントを受け取り、クライアント(Turbo)がDOMをそのまま置換します。
  • 前の2つとの違い: InertiaやSDUIは「JSONを投げてクライアント側で描画」しますが、Hotwireは「サーバーで描画済みのHTMLを投げる」ため、クライアント側にレンダリングロジックすら持たせない、pjaxの系譜にある最もシンプルな密結合アプローチです。

(返答終了理由: STOP)


ユーザー: さてフルのSPAを記述するとどうなるでしょうか?これはデータ取得用に定義されたエンドポイントを素直に使うアプローチです。

ルーティングやdtoの変換が二重になりクライアントの扱いが必要になり多段のAPI呼び出しが必要になるという感じですよね。

(ページ毎のBFFを立てちゃう場合もありますが)


AI: ご認識の通りです。これまでの「手抜き(密結合)」アプローチと比較すると、フルのSPA(完全に疎結合な構成)は、「正しさ」の対価として「二重管理と通信の複雑さ」を支払うモデルと言えます。

整理すると以下のようになります。

フルのSPA(疎結合・APIベース)

サーバーとクライアントを「他人の関係(独立したシステム)」として扱い、汎用的なAPI(REST/GraphQL)という**契約書(Contract)**を通して会話する構成です。

1. 二重化する「ルーティング」と「型定義」

InertiaやHotwireでは1つで済んでいたものが、物理的に分断されるため2つずつ必要になります。

  • ルーティングの二重化:
    • Server: GET /api/users/{id} (データリソースとしてのパス)
    • Client: /users/:id (画面遷移としてのパス)
    • → 互いに同期する仕組みがないため、手動で整合性を保つ必要があります。
  • DTO (Data Transfer Object) のバケツリレー:
    • Server: DB ModelAPI Resource Class (JSON化)
    • Client: JSONFrontend Type Definition (TS型) → Component Props
    • → サーバーでフィールド名を1つ変えると、フロントエンドの型定義も修正が必要です(OpenAPI Generatorなどで自動化しようと必死になる部分です)。

2. クライアントの責務激増(状態管理)

サーバーはステートレスなJSONを返すだけなので、「今どうなっているか」の責任はクライアントが負います。

  • データ保持: 取得したJSONをStore (Redux, Zustand) やCache (TanStack Query, SWR) に保持し続ける必要があります。
  • Loading / Error / Empty: データ取得中のグルグル、エラー表示、空の状態など、UIの全ステートをフロントエンド側で実装・管理しなければなりません(Inertia等はHTMLごと来るのでサーバー側で制御しやすい)。

3. 「おしゃべりな通信」と多段API呼び出し

汎用的なAPI(純粋なリソースAPI)を作ろうとすると、1つの画面を表示するために何度もサーバーと往復する必要があります。

  • N+1回のフェッチ:
    1. GET /me (ログインユーザー情報を取得)
    2. GET /posts (一覧を取得)
    3. GET /users/1 (各投稿のユーザー詳細を取得...)
  • 結果: ウォーターフォール(数珠つなぎ)な通信が発生し、パフォーマンスチューニングが難しくなります。

補足:BFF (Backend For Frontend) の位置づけ

「ページ毎のBFFを立てる」というのは、この**「フルSPAの辛さ(特に多段呼び出しとDTOの不一致)」を解消するための妥協案**です。

  • BFFの役割:
    • フロントエンド専用のサーバー(APIルート)を用意する。
    • そこで裏側の複数の汎用APIを叩いてまとめ(Aggregation)、画面に都合の良い形に整形して返す。
  • 構造:
    • 実質的に 「Inertiaのコントローラーがやっている仕事(データ収集と整形)」だけをAPIとして切り出したもの と言えます。
    • これによりクライアントの通信回数は減りますが、「ルーティングの二重化」や「クライアントでの状態管理」の手間は残ります。

まとめ:各アーキテクチャの比較

項目 Hotwire Inertia.js Server-Driven UI フルSPA
通信内容 HTMLそのもの コンポーネント名 + Props (JSON) UI構造定義 (JSON) 汎用データリソース (JSON)
ルーティング サーバーのみ サーバーのみ (クライアントは追従) サーバーのみ (1URLで完結も可) サーバーとクライアントで二重管理
結合度 超密結合 (HTML依存) 密結合 (実装ファイル依存) 密結合 (UIスキーマ依存) 疎結合 (API契約のみ)
主な用途 Rails/Laravelの標準 管理画面、SaaSプロトタイプ 大規模アプリの詳細画面、アプリ モバイルアプリ併用、大規模開発

(返答終了理由: STOP)


ユーザー: このフルのAPIのSPAがtanstack routerあたりで行われたりするという感じですよね(uiはまぁsvelteでもpreactでもreactでも何でも良いしapi clientのモジュールかstore/repositoryのモジュールがそれぞれ存在してる)


AI: その通りです。ご指摘の構成は、まさに**現代的な「フルSPA」の到達点(ベストプラクティス)**と言える姿です。

かつては「UIコンポーネントの中で useEffect してデータを取る」という無秩序な時代がありましたが、現在はTanStack Routerのような高度なルーターを中心に、責務がきれいにモジュール化される構成に進化しています。

この構成を整理すると、以下のような「役割分担(レイヤー構造)」になります。

現代的フルSPAのアーキテクチャ図(TanStack Router等の場合)

サーバーとクライアントは疎結合ですが、クライアント内部で**「ルーターが司令塔」**になることで、秩序を保っています。

graph TD
    User[ユーザー操作/URL変更] --> Router[Router (TanStack Router)]
    
    subgraph Client Application
        Router -- 1. URL解析 & Loader起動 --> Store[Async State Manager<br>(TanStack Query / SWR)]
        
        Store -- 2. データ要求 --> APIClient[API Client / Repository<br>(Fetch, Axios, SDK)]
        APIClient -- 3. 通信 (JSON) --> Server[API Server]
        
        Server -- 4. レスポンス --> APIClient
        APIClient -- 5. データ格納 --> Store
        
        Store -- 6. 状態監視 (Sub) --> UI[UI Components<br>(React, Svelte, etc.)]
        Router -- 7. コンポーネント切り替え --> UI
    end
Loading

この構成のポイント(Inertia等との対比)

1. Routerが「データ取得のトリガー」になる

以前のSPAはUIコンポーネントが描画されてからデータを取得(Render-then-Fetch)していましたが、TanStack Routerなどは**「Loader」**という概念を持ち込みました。

  • Inertia: サーバーのコントローラーがデータを決めてビューを返す。
  • TanStack Router: クライアントのルーターがURLを見て、APIクライアントを叩き、データが揃ってからUIを描画する(Fetch-then-Render)。
    • これにより、Inertiaのような「ページ遷移体験」をフルSPAで再現しようとしています。

2. Store/Repositoryモジュールの分離

ご指摘の通り、UI(React/Svelte)は単なる「見た目」になり、データ管理は完全に別モジュールになります。

  • API Client / Repository:
    • 「ユーザー一覧を取得する」といった純粋な通信ロジック。
    • OpenAPIなどから自動生成された型定義付きのSDKを使うことが多いです。
  • Store (Server State Management):
    • TanStack Queryなどが担当。
    • 「データのキャッシュ」「ローディング状態」「再取得(Invalidation)」を管理します。
    • UIは useQuery フックなどを通じて、ここからデータを「購読」します。

3. 型安全性(Type Safety)への執着

フルSPAは「サーバーと切れている(疎結合)」ため、その隙間を埋めるために**「型」**が生命線になります。

  • TanStack Router: URLのパスパラメータやSearch Params(クエリパラメータ)まで型安全にします。「/users/${id}id は数値でなければならない」といった定義をクライアント側で厳密に管理します。
  • ここが**Inertia(サーバーのコードが近いから信頼する)との最大の違いで、フルSPAは「契約(型)でガチガチに固める」**ことで安全性を担保しようとします。

まとめ:現代のフルSPAとは

「ただデータを取って表示する」だけでなく、「URLという唯一の正解(Single Source of Truth)」に基づいて、APIクライアントとUIを同期させるための巨大なステートマシンをクライアント側に構築している状態です。

  • Inertia: そのステートマシンをサーバー側に持たせて、HTML/JSONで結果だけ送る。
  • フルSPA: そのステートマシンをJSでクライアント側に実装し、APIは純粋なデータソースに徹する。

TanStack Routerの登場によって、フルSPAもようやく「Inertiaのようなスムーズなルーティングごとのデータ管理」を手に入れましたが、やはり**「クライアント側に分厚い中間層(Router + Query + API Client)が必要」**という複雑さは残ります。

(返答終了理由: STOP)


ユーザー: そして真逆な対応がnext.jsやRSCというわけですね


AI: おっしゃる通りです。Next.js (App Router) や React Server Components (RSC) は、これまで整理してきた「リッチなフルSPA(TanStack Router等)」に対する**強烈な揺り戻し(カウンター)であり、ある意味で「真逆」**のアプローチです。

そして面白ことに、思想的には 「Inertia.jsが進もうとしていた道を、React自体の機能としてさらに過激に推し進めたもの」 と言えます。

整理すると以下のようになります。

Next.js / RSC の「真逆」な点

TanStack Router等のフルSPAが**「クライアントを賢くする(Thick Client)」方向へ進化したのに対し、RSCは「クライアントを再び馬鹿にする(Thin Client)」**方向へ舵を切りました。

1. API層の消滅(BFFのコンポーネント化)

  • フルSPA: UIからAPIを叩くために、fetchRepositoryDTO が必要。
  • RSC: コンポーネント自体が async 関数になり、直接DBを叩きます
    // コンポーネントの中にバックエンドロジックが直書きされる
    export default async function Page() {
      const users = await db.query('SELECT * FROM users'); // 直接DBアクセス!
      return <div>{users.map(...)}</div>;
    }
    → 「APIクライアント」も「状態管理(Store)」も不要になります。コンポーネントそのものがBFFになったと言えます。

2. バンドルの「ゼロ」化

  • フルSPA: ロジックが増えれば増えるほど、クライアントに配信するJSファイル(バンドルサイズ)が肥大化します。
  • RSC: サーバーコンポーネントのコード(npmパッケージ含む)はクライアントに一切送信されません。送信されるのは計算結果(RSC PayloadというUIツリーのデータ)だけです。

3. ルーティングの主権回復

  • フルSPA: クライアントのルーターが主導権を持つ。
  • RSC: URLを解釈するのはサーバー。サーバーが「どのコンポーネントツリーを返すか」を決定し、ストリーミングします。

全体像の中での位置づけ

これまでの議論をすべて並べると、Web開発の歴史が**「サーバー → クライアント → サーバー」**と螺旋状に戻ってきていることがよくわかります。

項目 1. Hotwire / Classic 2. Inertia.js 3. フルSPA (TanStack) 4. Next.js / RSC
主権 Server Server Client Server
通信 HTML (文字列) Props (JSON) Resource (JSON) RSC Payload (UIツリー)
API 不要 (Viewが描画) 不要 (Controllerが注入) 必須 (疎結合の契約) 不要 (Compが直接取得)
JS量 極小 中 (View部分は全部JS) 特大 (ロジック全部入り) 小〜中 (対話部分のみJS)
メンタル 昔ながらのMPA モダンなモノリス 分散システム コンポーネント単位のMPA

RSCは「Inertiaの究極系」とも言える

  • Inertia: 「ページ単位」でサーバーからデータ(Props)をもらい、クライアントでレンダリングする。
    • データ取得(Controller)と表示(Vue/React)はファイルとして分離されている。
  • RSC: 「コンポーネント単位」でサーバーでレンダリングし、結果をクライアントに送る。
    • データ取得と表示が1つのファイル内で完結(コロケーション)している。

結論: Next.js / RSC は、フルSPAで複雑になりすぎた「通信・状態管理・ルーティング」のコストを、**「サーバーサイドでコンポーネントを実行して結果だけ送る」**という力技で解決しに来た技術です。

「手抜き」という文脈で言えば、**「APIを作らなくていい」「型定義を共有しなくていい(同じファイルだから)」**という究極の手抜きを、Reactのエコシステム標準にしてしまったのがRSCと言えます。

(返答終了理由: STOP)


ユーザー:

技術的な対話履歴の備忘録 ラバーダッキング のプロンプト詳細

【指示】

Persona

あなたは、複雑な技術課題に取り組むソフトウェア開発者です。

Primary Task

ある問題解決の過程を、思考を整理する「ラバーダッキング」のように記述し、自己完結型のドキュメントとして記録してください。 このドキュメントの目的は、未来の自分が同じ課題に直面した際に、思考のプロセスを追体験し、同じ轍を踏むことを避けるためのものです。特に、思考の分岐点、試したものの採用しなかったアプローチとその理由、そして最終的な結論に至った論理的な根拠を時系列に沿って記述してください。

Output Behavior

  • 記述は、結論だけを示すのではなく、そこに至るまでの思考の流れを忠実に再現してください。
  • コードは、思考の各段階を具体的に示すための「証跡」として配置してください。成功したコードだけでなく、検討段階の不完全なコードや、ボツになったアイデアを示すコードを含めても構いません。
  • 文章は、思考のコンテキストを補完する「糊」の役割に徹し、過度な説明や正当化は避けてください。

Output Format

  • 記事は必ず # 1つのタイトル名から始めてください。
  • 思考の流れに沿って、必要に応じて章で分割してください。
  • 各章のタイトルには、その段階の思考内容を表す絵文字(例: 🤔, 💡, ❌, ✅)を1つだけ加えてください。
  • 記事はMarkdownフォーマットで出力してください。
  • *** による太字や斜体での強調は一切禁止してください。
  • 記事の冒頭に、以下の項目のFront matterをコードブロックとして記述してください。
    • target_reader: Future me
    • objective: [この備忘録の目的を簡潔に記述]
  • 専門用語は積極的に使用し、未来の自分が忘れている可能性を考慮して、必要に応じて脚注1で補足説明を加えてください。脚注では本来の意味とここでの意味や使われ方を説明してください。
  • 再現性を担保するため、関連するコードスニペットを積極的に挿入してください。

Critical Constraints

  • この文章は、元の対話の存在を知らない読者(未来の自分)にも完全に理解できるように記述してください。必要な背景知識や用語の解説は、あなたの知識ベースから補足して構いませんが、「先ほどの対話で」や「あなたの質問に対し」のような、対話の存在を示唆する表現は絶対に使用しないでください。
  • 読者は、筆者(私)やあなた(AI)の存在を知りません。それらについて一切言及しないでください。
  • 「洗練された」「発見の旅」のような自己満足的な表現は使用しないでください。ドキュメントは、問題解決のプロセスそのものの記録であり、結論を美化するためのものではありません。
  • 記事の末尾に「まとめ」や「今後の展望」といったセクションは含めないでください。

今までの会話最初から最後までを元に、上記のプロンプトを実行してください。出力は日本語でよろしく。


AI:

target_reader: Future me
objective: Webアプリケーションアーキテクチャの選定において、疎結合なSPA構成と密結合な効率化手法(Inertia, SDUI, RSC等)のトレードオフを整理し、適切な技術選定の指針とする

Webアプリケーションアーキテクチャにおける「手抜き」と「結合」のレイヤー構造

🤔 SPA開発の複雑性に対する違和感

Webアプリケーションの要件定義において、リッチなユーザー体験(SPA)が求められることはもはや標準となった。しかし、その実現手段として教科書的な「フルSPA構成」を採用しようとすると、開発コストの増大に直面する。

具体的には、フロントエンド(React/Vue)とバックエンド(REST/GraphQL)を完全に分離する構成だ。この構成では、以下の「二重管理」が発生する。

  1. ルーティングの二重化: サーバー側のAPIルート定義と、クライアント側の画面遷移ルート定義。
  2. 型定義の二重化: サーバー側のDTO1と、クライアント側のTypeScript型定義。
  3. 状態管理の複雑化: サーバーにあるデータ(Server State)をクライアントでキャッシュ・同期するための複雑なロジック。

小規模なチームや、社内ツール、プロトタイプ開発において、この「正しさ(疎結合)」はオーバーエンジニアリングではないか。サーバーとクライアントをあえて密結合させることで、これらの手間を省く「手抜き」のアプローチを整理する必要がある。

💡 ルーティングとデータ供給のショートカット (Inertia.js)

最初に検討するのは、ルーティングとデータフェッチのレイヤーでの結合だ。Inertia.jsが採用しているアプローチである。

ここでは「モダンなモノリス」を目指す。サーバーサイドフレームワーク(LaravelやRails)のルーティング機能をそのまま使い、ViewレイヤーだけをVueやReactに置き換える発想だ。

// Server Side (Laravel Example)
class UserController extends Controller
{
    public function index()
    {
        // JSON APIを作らず、コンポーネント名とデータを直接返す
        return Inertia::render('Users/Index', [
            'users' => User::all(),
            'canCreate' => auth()->user()->can('create'),
        ]);
    }
}

このアプローチの本質は、サーバーが「どのコンポーネントを表示するか」と「それに渡すデータ(Props)」を決定している点にある。

クライアント側は以下のようなJSONを受け取り、対応するファイル(Users/Index.vue)を解決して描画する。

{
  "component": "Users/Index",
  "props": {
    "users": [...],
    "canCreate": true
  },
  "url": "/users",
  "version": "..."
}

APIのエンドポイント設計や、クライアント側でのフェッチ処理(useEffectなど)が一切不要になる。ここでの結合レイヤーは「ページコンポーネント」だ。サーバーとクライアントは、物理的なファイル構成とPropsの構造で強く握手している。

💡 UI構造自体のショートカット (Server-Driven UI)

次に、さらに結合度を高め、UIの構造そのものをサーバーが決定するアプローチを検討する。Server-Driven UI (SDUI) だ。

これは、管理画面の詳細ページや、ネイティブアプリの動的なキャンペーンページなどで有効だ。Inertiaが「ページごとのデータ」を送っていたのに対し、SDUIは「UIのスキーマ」を送る。

{
  "sections": [
    {
      "type": "FormSection",
      "title": "基本情報",
      "fields": [
        { "key": "name", "label": "氏名", "inputType": "TEXT", "value": "山田太郎" },
        { "key": "dob", "label": "誕生日", "inputType": "DATE", "value": "1990-01-01" }
      ]
    }
  ]
}

クライアント側は「汎用レンダラー」となり、inputType: 'TEXT' ならテキストボックスを表示する、というルールに従うだけになる。

この手法での「手抜き」は、画面ごとのフロントエンド実装を排除することだ。新しい画面が必要になっても、バックエンドの定義を追加するだけで済む。ここでの結合レイヤーは「抽象化されたUI定義」である。

❌ クライアントロジックの排除 (Hotwire)

さらに極端な「手抜き」として、Hotwire (Turbo) のようなアプローチがある。これは pjax の現代版と言える。

サーバーはJSONではなく、レンダリング済みのHTMLフラグメントを返す。クライアントは受け取ったHTMLでDOMを置換する。

<!-- Server Response -->
<turbo-stream action="replace" target="message_1">
  <template>
    <div id="message_1">
      更新されたコンテンツ
    </div>
  </template>
</turbo-stream>

InertiaやSDUIは「データ(JSON)を受け取ってクライアントで描画」していたが、Hotwireは描画すらサーバーに任せる。もっとも密結合な形態だが、複雑なクライアントサイドのインタラクションが必要な場合、逆にJavaScriptの管理が難しくなる懸念があるため、要件を選ぶ必要がある。

🔄 「正しさ」のコストとフルSPA (TanStack Router)

ここまで検討した「密結合アプローチ」を採用できないケースについて考える。例えば、バックエンドが既存の汎用REST APIとして存在している場合や、モバイルアプリとバックエンドを共有する場合だ。

この場合、いわゆる「フルSPA」構成を選択することになる。これはサーバーとクライアントが完全に疎結合な「他人の関係」となり、APIという契約書を通して会話するモデルだ。

現代的なフルSPAの実装(TanStack Router等を利用)では、クライアント側に分厚い中間層が必要になる。

graph TD
    Router[Router / URL] --> Loader[Data Loader]
    Loader --> API[API Client / SDK]
    API --> Server[REST API]
    API --> Store[Client Cache / Store]
    Router --> Component[UI Component]
    Store --> Component
Loading

この構成では、以下のようなコストを支払うことになる。

  1. 多段の呼び出し: UIを表示するために、Router -> Loader -> API Client -> Server というバケツリレーが発生する。
  2. BFFの必要性: APIが微細すぎる(リソース指向)場合、クライアントの通信回数を減らすために、ページ単位のデータ整形を行うBFF (Backend For Frontend) を立てる必要が出てくる。これは実質的にInertiaのコントローラーと同じ役割だが、疎結合を保つためにAPIとして切り出されている。

この複雑さは、大規模開発やチーム間の分業においては「秩序」として機能するが、少人数開発では単なるオーバーヘッドになり得る。

🚀 揺り戻しとしての回帰 (Next.js / RSC)

最後に、最新の潮流である React Server Components (RSC) をこの文脈で捉え直す。RSCは、Inertia.jsが目指した「手抜き」を、Reactのエコシステム自体に取り込んだものと解釈できる。

RSCでは、コンポーネント自体が非同期関数となり、直接バックエンドロジック(DBアクセス等)を実行する。

// Server Component
export default async function Page() {
  // API層を経由せず、直接DBを叩く(究極の手抜き)
  const users = await db.query('SELECT * FROM users');
  
  return (
    <div>
      {users.map(user => <UserCard user={user} />)}
    </div>
  );
}

これは以下の点でInertiaの進化系と言える。

  1. API層の消滅: コンポーネントが実質的なBFFとなり、API定義が不要になる。
  2. コロケーション: データ取得ロジックとUI定義が同じファイルに存在するため、型定義の共有すら不要になる。
  3. 主権の回帰: ルーティングとレンダリングの主導権が再びサーバー側に戻っている。

フルSPAで肥大化したクライアント側の責務(ルーティング、データフェッチ、状態管理)を、サーバーサイドのコンポーネントツリー処理(RSC Payloadのストリーミング)に置き換えるアプローチだ。

アーキテクチャ選定の際は、単に「流行り」で選ぶのではなく、この「結合のレイヤー」をどこに置きたいか(ページ単位か、UI構造単位か、API契約単位か、コンポーネント単位か)を基準に判断すべきである。

(返答終了理由: STOP)


ユーザー: 今回の対話で使われている専門用語に脚注をつけてください。脚注の中では一般的な意味と今回の対話での使われ方やより閉じた意味を説明してください。

出力形式は箇条書きのようなもので良いです


AI:

  • SPA (Single Page Application)

    • 一般的な意味: 最初に1つのHTMLページだけを読み込み、以降はユーザーの操作に応じてJavaScriptで必要なデータだけを取得・更新して画面を書き換えるWebアプリケーションの形態。
    • 今回の対話での意味: 「ユーザー体験(UX)」としての滑らかな画面遷移を指す場合と、フロントエンドとバックエンドを分離する「アーキテクチャ」としての構成(フルSPA)を指す場合があり、この2つの区別が重要となる。
  • Inertia.js

    • 一般的な意味: LaravelやRailsなどのサーバーサイドフレームワークを使って、従来のモノリスな開発体験のままVueやReactによるSPAを構築するためのライブラリ。
    • 今回の対話での意味: サーバーサイドのルーティングやコントローラーをそのまま使い、ViewレイヤーだけをSPA化する「手抜き(効率化)」の代表的な手法として扱われている。APIを真面目に作らず、ページ単位でコンポーネントとデータを結合させるアプローチ。
  • Server-Driven UI (SDUI)

    • 一般的な意味: 画面に表示するUIの構造やレイアウト情報をサーバーからAPIレスポンスとして送信し、クライアント(アプリ)側でそれを解釈して描画する設計パターン。
    • 今回の対話での意味: 画面ごとのフロントエンド実装を不要にし、バックエンドの定義変更だけで画面を追加・修正するための「究極の省力化」手法。AirbnbやDRESS CODEの事例を元に、詳細画面の量産などに適した密結合アプローチとして議論された。
  • pjax / Hotwire (Turbo)

    • 一般的な意味: リンククリックなどのイベントをインターセプトし、AjaxでサーバーからHTMLの一部を取得してDOMを置換することで、フルリロードなしで画面遷移する技術。Hotwireはその現代版(Railsのエコシステム等)。
    • 今回の対話での意味: クライアント側にJSONのレンダリングロジックすら持たせない、最もシンプルな「HTMLそのものを返す」密結合アプローチ。
  • フルSPA (Full SPA)

    • 一般的な意味: フロントエンドとバックエンドが完全に分離され、APIを通じて通信する構成。
    • 今回の対話での意味: 「正しさ」と引き換えに、ルーティングや型定義の二重管理、状態管理の複雑化などのコストを支払う構成。TanStack Routerなどを用いた現代的な実装は、疎結合ゆえの複雑さを解消するための高度な中間層を持つ。
  • BFF (Backend For Frontend)

    • 一般的な意味: フロントエンド(Web、iOS、Androidなど)ごとの要求に合わせてAPIを整形・集約する、フロントエンドのためのバックエンドサーバー層。
    • 今回の対話での意味: フルSPAにおける「多段API呼び出し」や「データの扱いづらさ」を解消するために、ページ単位やコンポーネント単位でデータを加工して返すAPIエンドポイントのこと。InertiaやRSCはこのBFFの役割を内包していると解釈された。
  • RSC (React Server Components)

    • 一般的な意味: Reactコンポーネントをサーバーサイドで実行し、そのレンダリング結果(HTMLではなく独自のRSC Payload形式)をクライアントに送信する仕組み。
    • 今回の対話での意味: コンポーネント自体が直接DBなどにアクセスすることでAPI層を排除する、Inertia.jsが進もうとした道をさらに推し進めた「究極の手抜き(効率化)」アプローチ。
  • TanStack Router

    • 一般的な意味: Reactなどのための現代的な型安全なルーティングライブラリ。
    • 今回の対話での意味: フルSPAにおいて、URLを唯一の正解(Single Source of Truth)として、データ取得(Loader)や状態管理を統制する「司令塔」の役割を果たすツール。クライアント側を賢く(Thick Clientに)するアプローチの象徴。

(返答終了理由: STOP)


ユーザー: 別の立場からのアンサーソングないしはトラックバックを考えてみてください。

どのような立場からのどのような文章になるでしょう?箇条書きでいくつか挙げてください。自信度も併記してください。


AI: 思考プロセス:

  1. 現在の文脈の分析:

    • これまでの議論は、「手抜き(効率化)」と「密結合」を肯定的に捉える文脈が強かった。
    • Inertia.js, Server-Driven UI (SDUI), RSCなどを「賢いショートカット」として評価し、フルSPA(疎結合構成)を「コストが高い」ものとして位置づけている。
    • 主な評価軸は「開発者の生産性(少人数・小規模チーム前提)」「コードの重複排除」「サーバー資産の活用」。
  2. 対立軸(アンサーソングの視点)の探索:

    • 誰が この議論に反論したいか?
      • 純粋なフロントエンドエンジニア: UXの細部、パフォーマンス、オフライン動作などを重視する人。
      • モバイルアプリ開発者: WebとAPIを共有したい人。
      • 大規模組織のアーキテクト: 分業、スケーラビリティ、マイクロサービスを重視する人。
      • セキュリティ専門家: 密結合によるリスクを懸念する人。
      • APIエコノミーの信奉者: パブリックAPIとしての価値を重視する人。
  3. 具体的な「アンサーソング」の案出し:

    • 案A: フロントエンド純粋主義者 (The UX Purist)

      • 主張: 「手抜き」はユーザーへの「甘え」だ。インタラクションの質、楽観的UI、オフライン対応は密結合では限界がある。
      • キーワード: Optimistic UI, PWA, Latency masking, 表現の自由度。
    • 案B: 組織設計・分業論者 (The Team Scaler)

      • 主張: 密結合は「コンウェイの法則」の敵。バックエンドとフロントエンドのチームが独立して動けないと、大規模開発は詰む。
      • キーワード: 並行開発, コミュニケーションコスト, 責任分界点, マイクロサービス。
    • 案C: APIファースト原理主義者 (The API First Believer)

      • 主張: UIのためのAPI (BFF/SDUI) は「負債」になる。汎用的なリソースAPIこそが資産であり、AI時代(LLM連携)にも生き残る。
      • キーワード: Headless, Omnichannel, LLM Agent, 公開API。
    • 案D: モバイルアプリ開発者 (The Native Developer)

      • 主張: Webの手抜き(Inertia/RSC)はモバイルを置き去りにする。結局モバイル用に別途APIを作るなら二度手間だ。SDUIもネイティブ実装コストが高い。
      • キーワード: Native apps, Code sharing, Swift/Kotlin, 二重メンテ。
  4. 自信度の評価:

    • 案A (UX): 非常に強い反論が可能。特にInertia/Hotwireはネットワーク遅延がUXに直結しやすい弱点がある。 (自信度: 高)
    • 案B (組織): エンタープライズ文脈では定石。ただ、今回の議論が「少人数・手抜き」を前提にしているため、前提が噛み合わない可能性も。 (自信度: 中)
    • 案C (API): LLMの文脈(Markdownエンドポイントの話)とも繋がり、タイムリー。 (自信度: 高)
    • 案D (モバイル): RSC/Inertia批判として最も痛いところを突ける。 (自信度: 高)
  5. 出力構成案:

    • それぞれの立場の「タイトル」「概要」「主張の核」「自信度」を提示する。

提示された「手抜きと密結合の肯定」という論調に対する、異なる立場からのアンサーソング(反論・トラックバック)の案です。

1. 「UXの質を人質にするな」:フロントエンド純粋主義者からの反論

  • 立場: インタラクションの質、パフォーマンス、オフライン体験を最優先するフロントエンドエンジニア。
  • 概要: InertiaやRSC、SDUIといったサーバー主導モデルは、ネットワーク遅延をユーザー体験に直結させてしまう「脆弱なアーキテクチャ」であると批判する。
  • 主張の核:
    • 楽観的更新 (Optimistic UI) の欠如: ユーザーが「いいね」を押した瞬間、サーバーレスポンスを待たずにUIを変えるような高度な体験は、クライアントにロジック(状態管理)がないと実現できない。
    • ネットワークの壁: モバイル回線などの不安定な環境では、HTML/JSONを待つ0.5秒がストレスになる。フルSPA(Local First)なら、シェルを表示してキャッシュを見せることで「待たせない体験」が作れる。
    • 表現力の限界: SDUIの「汎用レンダラー」は、結局「ありきたりなUI」しか作れない。アニメーションや複雑なドラッグ&ドロップなど、リッチな表現への足かせになる。
  • 自信度: 90% (技術的なトレードオフとして最も明確な反論点)

2. 「腐敗するBFFと捨てられる資産」:APIファースト論者からの警告

  • 立場: データ資産の活用、システム間連携、将来の拡張性を重視するアーキテクト。
  • 概要: UIの都合に合わせて作られたエンドポイント(InertiaのPropsやRSC)は、その画面以外では使い物にならない「使い捨てコード」であり、技術的負債であると指摘する。
  • 主張の核:
    • 再利用性の欠如: 今日作った「画面用API」は、明日作るモバイルアプリや、来年作る外部連携機能では使えない。結局、汎用APIを別に作ることになり二度手間になる。
    • AI/LLM時代への逆行: LLMのエージェントが必要とするのは、画面構造(SDUI)ではなく、純粋な意味(セマンティクス)を持ったリソースAPIだ。UIとデータを混ぜるアプローチは、データの価値を毀損している。
    • Headlessの価値: UIを持たない(Headlessな)汎用APIがあってこそ、CMS、EC、CRMなどあらゆるチャネルに展開できる。UI結合は「Webブラウザ」という一チャネルにシステムを閉じ込める行為だ。
  • 自信度: 85% (長期的なシステム寿命の観点から強力な視点)

3. 「コンウェイの法則に殺される」:組織論・スケーラビリティ担当からのトラックバック

  • 立場: 大規模組織のEM(エンジニアリングマネージャー)やCTO。
  • 概要: 「密結合での手抜き」は、少人数のうちは速いが、チームが拡大した瞬間に「ブロッカー」になると警告する。
  • 主張の核:
    • 分業の不可能性: InertiaやRSCは、バックエンドエンジニアとフロントエンドエンジニアが「同じリポジトリの同じファイル」を触ることを強いる。これは並行開発を阻害し、コミュニケーションコストを爆発させる。
    • 採用のミスマッチ: 「Railsも書けてReactも書けるフルスタック」しか採用できなくなる。市場には「Reactのスペシャリスト」や「Goのスペシャリスト」の方が多い。
    • 疎結合は組織のためのインターフェース: API定義書(OpenAPI)は、チーム間の契約書だ。これがあるからこそ、互いに干渉せずにスケールできる。「手抜き」は組織のスケーラビリティを犠牲にしている。
  • 自信度: 75% (前提が「少人数開発」の場合は弱いが、成長企業向けには有効)

4. 「ネイティブアプリを忘れたWebの独り言」:モバイルアプリ開発者からの冷ややかな視線

  • 立場: iOS/Androidアプリ開発者。
  • 概要: Web界隈が盛り上がっているInertiaやRSCといった技術は、モバイルアプリ開発者にとっては「Web専用の独自規格」に過ぎず、恩恵がないどころか害であると嘆く。
  • 主張の核:
    • RSC/InertiaはWebブラウザでしか動かない: JSONを返すと言っても、それはフレームワーク固有の形式だ。SwiftやKotlinからそれをパースして使うなんて狂気の沙汰だ。
    • SDUIのコスト: 「サーバーで定義すればアプリも変わる」と言うが、そのためのネイティブ側のレンダラー実装コストは莫大だ。WebのDOMほど柔軟ではないネイティブUIで汎用レンダラーを作るのは茨の道だ。
    • 結論: 結局、モバイルアプリのために「普通のREST API」を作ってくださいとお願いすることになる。Webチームが楽をした分、モバイルチームにしわ寄せが来ている。
  • 自信度: 80% (マルチプラットフォーム展開を想定する場合、致命的な指摘となる)

(返答終了理由: STOP)


ユーザー: ちなみにgraphqlでもすべての議論は成り立つのでそこはよろしく。単なるデータ構造の定義と通信の形状のプロトコルに過ぎないので。


AI: おっしゃる通りです。「GraphQLであること」自体は、本質的な「結合度」や「手抜き」の議論を変える魔法ではありません。GraphQLを使っても、どのレイヤーで握手するかによって、以下のすべてのパターンが再現可能です。

GraphQLにおける「手抜き(密結合)」と「正しさ(疎結合)」の写像

これまでの議論(Inertia, SDUI, フルSPA, RSC)をGraphQLの世界にマッピングするとこうなります。

1. GraphQL版 Inertia.js(ページ駆動クエリ)

  • アプローチ: 画面(Page)単位で巨大なクエリを1つ叩く。
  • スキーマ: リソース汎用ではなく、画面固有の型を切る。
    # 汎用的な User 型ではなく、この画面専用の構造
    type UserProfilePage {
      headerTitle: String!
      userData: User!
      canEdit: Boolean!
      relatedPosts: [Post!]!
    }
    
    query GetUserProfilePage($id: ID!) {
      userProfilePage(id: $id) {
        ... # 画面に必要なすべて
      }
    }
  • 特徴: RESTのBFFパターンと同じ。フロントとバックが「この画面のために」密結合する。

2. GraphQL版 Server-Driven UI(UIスキーマ)

  • アプローチ: UIコンポーネントのツリー構造をGraphQLで表現する。
  • スキーマ: データではなくUI部品を定義する。
    interface UIComponent {
      id: ID!
      type: ComponentType!
    }
    
    type TextField implements UIComponent {
      label: String!
      value: String
    }
    
    type Screen {
      sections: [UIComponent!]!
    }
  • 特徴: AirbnbのGhost Platformがやっていることそのもの。「データ構造」としてUIツリーを返し、クライアントはそれをレンダリングするだけ。

3. GraphQL版 フルSPA(リソース駆動・Fragment Colocation)

  • アプローチ: 汎用的なリソース型を定義し、UIコンポーネントごとに必要なデータをFragmentで宣言する(Relayなどの思想)。
  • スキーマ: 純粋なデータモデル。
    type User {
      id: ID!
      name: String!
      friends(first: Int): [User!]
    }
  • クライアント: UserAvatar コンポーネントは fragment UserAvatar_user on User { profileImage } を要求し、UserName コンポーネントは fragment UserName_user on User { name } を要求する。コンパイラがそれらを結合してクエリを発行する。
  • 特徴: これが「正しさ」の極地。クライアントとサーバーは疎結合だが、ツールチェーン(Relay Compiler等)によって整合性を保つ。

4. GraphQL版 RSC(Server Components)

  • アプローチ: サーバーコンポーネント内で直接GraphQL(あるいはDB)を叩き、RSC Payload(シリアライズされたReactツリー)として返す。
  • 特徴: クライアントにはGraphQLのクエリもスキーマも届かない。結果としてのUIツリーだけが届く。GraphQLは「サーバー内部の実装詳細」として隠蔽される。

GraphQLへのアンサーソングの補正

先ほどのアンサーソング(特に「APIファースト論者」や「モバイル開発者」)において、「GraphQLを使えば解決する」という反論は成立しません。なぜなら:

  • APIファースト論者への反論: 「GraphQLでも、UI構造(SDUI)や画面固有型(BFF)を返してしまえば、それは汎用リソースAPIではない」からです。LLMエージェントにとって、type TextField だらけのスキーマはノイズでしかありません。
  • モバイル開発者への反論: 「WebチームがWeb画面専用のGraphQLクエリ(BFF的利用)を乱立させた場合、モバイルチームはそのクエリを再利用できません」。結局、モバイル用に別のクエリを書くか、スキーマ自体をモバイル向けに調整する必要が出てきます。

結論: RESTだろうがGraphQLだろうが、**「UIの都合(コンテキスト)をサーバー側のスキーマ/レスポンスにどれだけ混入させるか」**という設計判断こそが本質であり、プロトコルの違いはその実装手段の差異に過ぎません。

(返答終了理由: STOP)

メタデータ

{
  "runSettings": {
    "temperature": 1,
    "model": "models/gemini-3-pro-preview",
    "topP": 0.95,
    "topK": 64,
    "maxOutputTokens": 65536,
    "safetySettings": [
      {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "OFF"
      },
      {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "OFF"
      }
    ],
    "enableCodeExecution": false,
    "enableSearchAsATool": false,
    "enableBrowseAsATool": false,
    "enableAutoFunctionResponse": false,
    "thinkingBudget": -1,
    "outputResolution": "1K",
    "thinkingLevel": "THINKING_HIGH"
  },
  "systemInstruction": {}
}

Footnotes

  1. DTO (Data Transfer Object): プロセス間やシステム間でデータを転送するために使用されるオブジェクト。ここではAPIレスポンスの型定義を指す。 2

@podhmo
Copy link
Author

podhmo commented Dec 23, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment