◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-03-26上級

Rork Max × Server-Driven UI — バックエンドからUIをリアルタイム制御する次世代アーキテクチャ

Server-Driven UI(SDUI)をRork Maxで実装する方法を解説。バックエンドからUIコンポーネントをJSON定義で配信し、アプリ更新なしで画面レイアウトを動的に変更する設計パターンを紹介します。

rork-max41server-driven-uiarchitecture6react-native12dynamic-uibackend3JSONdesign-pattern

✦ プレミアム記事

Server-Driven UI とは何か — なぜ今モバイル開発で注目されているのか

モバイルアプリ開発において、UI の変更のたびに App Store / Google Play の審査を待つという従来のリリースサイクルは、ビジネスのスピードとの間に大きなギャップを生んでいます。Server-Driven UI(SDUI)は、この課題を根本から解決するアーキテクチャです。

SDUI の核心はシンプルです。UI のレイアウト・構成・表示条件をサーバーサイドの JSON レスポンスとして定義し、クライアント(アプリ)はその JSON を解釈してコンポーネントをレンダリングするという設計パターンです。Airbnb、Shopify、Lyft といった大手テック企業がプロダクション環境で採用しており、A/B テストの即時切り替えやパーソナライズド UI の配信に活用されています。

Rork Max は React Native / Expo ベースのネイティブアプリを生成するため、SDUI パターンとの相性が非常に良好です。ここで扱うのはRork Max プロジェクトに SDUI を導入するための設計原則、JSON スキーマ、レンダリングエンジン、バックエンド構成までを体系的に解説します。

SDUI の設計原則 — 3つの基本レイヤー

SDUI アーキテクチャは以下の3層で構成されます。

レイヤー1: スキーマ定義層(Contract Layer)

サーバーとクライアント間の「契約」となる JSON スキーマを定義します。このスキーマが SDUI の要であり、慎重な設計が求められます。

// types/sdui.ts — SDUI コンポーネントの型定義
type SDUIComponentType =
  | 'hero_banner'
  | 'product_grid'
  | 'text_block'
  | 'cta_button'
  | 'carousel'
  | 'feature_list'
  | 'testimonial'
  | 'spacer';
 
interface SDUIComponent {
  id: string;
  type: SDUIComponentType;
  props: Record<string, unknown>;
  children?: SDUIComponent[];
  visibility?: {
    conditions: VisibilityCondition[];
    operator: 'AND' | 'OR';
  };
  analytics?: {
    impressionEvent: string;
    tapEvent: string;
  };
}
 
interface VisibilityCondition {
  field: string;        // 例: "user.plan", "device.platform"
  operator: 'eq' | 'neq' | 'gt' | 'lt' | 'in';
  value: string | number | string[];
}
 
// サーバーレスポンスのルート型
interface SDUIScreenResponse {
  screenId: string;
  version: number;
  ttl: number;          // キャッシュ有効期限(秒)
  components: SDUIComponent[];
  metadata: {
    title: string;
    analytics_screen_name: string;
  };
}

レイヤー2: レンダリングエンジン層(Rendering Layer)

JSON を受け取り、対応する React Native コンポーネントに変換するエンジンです。

// components/sdui/SDUIRenderer.tsx
import React from 'react';
import { View } from 'react-native';
import { HeroBanner } from './HeroBanner';
import { ProductGrid } from './ProductGrid';
import { TextBlock } from './TextBlock';
import { CTAButton } from './CTAButton';
import { Carousel } from './Carousel';
import { FeatureList } from './FeatureList';
 
// コンポーネントレジストリ — 新しいコンポーネントはここに追加するだけ
const COMPONENT_REGISTRY: Record<string, React.ComponentType<any>> = {
  hero_banner: HeroBanner,
  product_grid: ProductGrid,
  text_block: TextBlock,
  cta_button: CTAButton,
  carousel: Carousel,
  feature_list: FeatureList,
};
 
interface SDUIRendererProps {
  components: SDUIComponent[];
  context: UserContext;  // ユーザー情報(プラン・デバイス等)
}
 
export function SDUIRenderer({ components, context }: SDUIRendererProps) {
  return (
    <View style={{ flex: 1 }}>
      {components
        .filter((comp) => evaluateVisibility(comp.visibility, context))
        .map((comp) => {
          const Component = COMPONENT_REGISTRY[comp.type];
          if (!Component) {
            // 未知のコンポーネントは安全にスキップ(前方互換性)
            console.warn(`Unknown SDUI component: ${comp.type}`);
            return null;
          }
          return (
            <Component
              key={comp.id}
              {...comp.props}
              analytics={comp.analytics}
            >
              {comp.children && (
                <SDUIRenderer components={comp.children} context={context} />
              )}
            </Component>
          );
        })}
    </View>
  );
}
 
// 期待される出力: JSON 定義に応じたコンポーネントが動的にレンダリングされる
// 例: hero_banner → <HeroBanner title="..." />, product_grid → <ProductGrid items={[...]} />

レイヤー3: データ配信層(Delivery Layer)

バックエンドから JSON を配信し、クライアントがキャッシュ付きで取得する仕組みです。

// hooks/useSDUIScreen.ts — 画面データの取得とキャッシュ管理
import { useState, useEffect, useCallback } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
 
const SDUI_CACHE_PREFIX = 'sdui_cache_';
 
export function useSDUIScreen(screenId: string) {
  const [screen, setScreen] = useState<SDUIScreenResponse | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
 
  const fetchScreen = useCallback(async () => {
    try {
      // 1. キャッシュを先にチェック(オフライン対応)
      const cached = await AsyncStorage.getItem(
        `${SDUI_CACHE_PREFIX}${screenId}`
      );
      if (cached) {
        const parsed = JSON.parse(cached);
        const isExpired =
          Date.now() - parsed.cachedAt > parsed.data.ttl * 1000;
        if (!isExpired) {
          setScreen(parsed.data);
          setLoading(false);
          return;
        }
      }
 
      // 2. サーバーから最新データを取得
      const response = await fetch(
        `https://api.example.com/sdui/screens/${screenId}`,
        {
          headers: {
            'X-App-Version': '2.1.0',
            'X-Platform': Platform.OS,
          },
        }
      );
      const data: SDUIScreenResponse = await response.json();
 
      // 3. キャッシュに保存
      await AsyncStorage.setItem(
        `${SDUI_CACHE_PREFIX}${screenId}`,
        JSON.stringify({ data, cachedAt: Date.now() })
      );
 
      setScreen(data);
    } catch (err) {
      setError(err as Error);
      // フォールバック: キャッシュが古くても表示する
      const cached = await AsyncStorage.getItem(
        `${SDUI_CACHE_PREFIX}${screenId}`
      );
      if (cached) {
        setScreen(JSON.parse(cached).data);
      }
    } finally {
      setLoading(false);
    }
  }, [screenId]);
 
  useEffect(() => {
    fetchScreen();
  }, [fetchScreen]);
 
  return { screen, loading, error, refetch: fetchScreen };
}
✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦Server-Driven UI の設計原則と JSON スキーマ定義を基礎から実装レベルまで理解できる
✦アプリストア審査なしで UI を即時更新するリリースフローを構築できる
✦Airbnb・Shopify が採用した SDUI パターンを Rork Max プロジェクトに応用する具体的手法を習得できる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-05-20
iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処
Rork で作った iOS アプリが起動直後に消える。クラッシュログに 0x8badf00d。Apple のウォッチドッグが React Native の重い初期化を切ったときの正しい直し方を、私の運用中アプリで実際に効いた手順で説明します。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-14
起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、読み込みより先に過ぎていたためです
Android 版の起動広告が、アプリを完全に終了してから開く回だけ一度も出ていませんでした。「前面に戻った」合図を読み込みの完了と切り離し、順番ごとにテストできる形へ組み直した記録です。