ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-04-08上級

Antigravity × Expo Router v3 — React Native 新アーキテクチャへ移行する手順:TurboModules・Fabric・JSI

React Native 新アーキテクチャ(TurboModules・Fabric・JSI)へのExpo SDK移行をAntigravityと実践。型安全なネイティブモジュール実装からEAS Build自動化まで網羅する上級ガイド。

antigravity435exporeact-nativenew-architectureturbomodulesfabricmobile2EAS2

Bridge のまま残されたアプリに何が起きるのか

React Native は2024年末から新アーキテクチャ(New Architecture)がデフォルト有効となりました。Expo SDK 52 以降でも同様に新アーキテクチャが標準となり、古いブリッジ(Bridge)ベースの実装を引き続き使うアプリは、パフォーマンス上の恩恵を受けられないだけでなく、将来的なサポート終了リスクを抱えることになります。

新アーキテクチャの中核を成す3つの技術スタック:

  • JSI(JavaScript Interface): JS と Native が直接メモリを共有し、非同期ブリッジのオーバーヘッドを排除
  • TurboModules: 必要な時にだけネイティブモジュールを遅延ロードし、起動時間を短縮
  • Fabric: UIレンダリングエンジンの再設計。スレッドセーフな同期レンダリングが可能に

しかし移行の壁は高く、特に「独自ネイティブモジュールを持つアプリ」では手作業での対応が大量に発生します。ここではAntigravity を活用してこの移行を体系的・効率的に進める方法を、実際のコードを交えて詳解します。

対象読者:

  • Expo SDK 50/51 でアプリを運営しており、SDK 52+ への移行を検討している開発者
  • 独自ネイティブモジュールを持ち、TurboModules 化に苦労している方
  • Expo Router v3 の file-based routing を最大活用したいチームリーダー

移行前の現状分析:Antigravity に全体像を把握させる

移行作業の最初のステップは「現状把握」です。Antigravity のプロジェクト解析機能を使って、移行コストの見積もりと影響範囲を洗い出しましょう。

プロジェクト全体のアーキテクチャ分析

Antigravity に以下のように依頼します:

@Workspace このReact Nativeプロジェクトの新アーキテクチャ移行準備状況を分析して。
具体的に:
1. 旧来のNativeModules/NativeEventEmitterを使っているファイルを全列挙
2. 新アーキテクチャ非対応の可能性があるnpmパッケージを特定
3. 移行の難易度を低/中/高で分類したロードマップを作成

Antigravity は package.json と全ソースファイルをスキャンし、以下のような分析レポートを返します:

【移行分析レポート】

高難度(手動対応必須):
- src/modules/BleManager.ts → NativeModules.BleManager 直接参照あり
- src/modules/CameraModule.ts → NativeEventEmitter 使用中
- android/app/src/main/java/.../CameraModule.java → 旧Bridge実装

中難度(ライブラリ更新で対応可能):
- react-native-camera: v4.2.0 → v5.x.x へ更新で新アーキ対応
- react-native-bluetooth-classic: 未対応。代替ライブラリを検討

低難度(設定変更のみ):
- react-native-async-storage: 1.21+ で新アーキ対応済み
- react-native-vector-icons: 10.x で対応済み

このレポートを元に移行を3フェーズに分けるのが Antigravity 推奨のアプローチです。


フェーズ1:Expo SDK 52 へのアップグレードと新アーキテクチャ有効化

1-1. package.json の更新

Antigravity のターミナルコマンド提案機能を使います:

# Antigravityにコマンドを生成させる
# "Expo SDK 52への移行コマンドを生成して" と指示
 
npx expo install expo@^52.0.0 --fix
npx expo install react-native@0.76.0 --fix

expo install --fix は依存関係の不整合を自動検出・修正するため、手動でのバージョン調整作業を大幅に削減できます。

1-2. app.json / app.config.ts の設定変更

新アーキテクチャを明示的に有効化します:

{
  "expo": {
    "name": "MyApp",
    "newArchEnabled": true,
    "android": {
      "newArchEnabled": true
    },
    "ios": {
      "newArchEnabled": true
    }
  }
}

app.config.ts を使っている場合は型安全に記述できます:

// app.config.ts
import { ExpoConfig, ConfigContext } from 'expo/config';
 
export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  name: 'MyApp',
  slug: 'my-app',
  newArchEnabled: true,
  // 環境変数で切り替え可能にする
  extra: {
    newArchEnabled: process.env.NEW_ARCH_ENABLED !== 'false',
  },
});

Antigravity に「この設定を環境変数でフィーチャーフラグ化して」と指示すると、上記のようなコードを即座に生成してくれます。


フェーズ2:TurboModules への移行

新アーキテクチャの中心的な変更点が TurboModules です。既存の NativeModules ベースのコードを型安全な TurboModules へ移行する方法を解説します。

2-1. Codegen スキーマの定義

TurboModules は TypeScript で型定義を書き、Codegen が Swift/Kotlin コードを自動生成する仕組みです。

まず、型定義ファイルを作成します:

// src/specs/NativeCameraModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
 
export interface Spec extends TurboModule {
  // カメラの許可状態を取得
  requestCameraPermission(): Promise<'granted' | 'denied' | 'blocked'>;
 
  // 写真を撮影
  takePicture(options: {
    quality: number;        // 0.0 ~ 1.0
    base64: boolean;
    exif: boolean;
  }): Promise<{
    uri: string;
    width: number;
    height: number;
    base64?: string;
    exif?: Record<string, unknown>;
  }>;
 
  // フラッシュモードの設定
  setFlashMode(mode: 'on' | 'off' | 'auto' | 'torch'): void;
 
  // イベントリスナー(新アーキ対応)
  addListener(eventName: string): void;
  removeListeners(count: number): void;
}
 
export default TurboModuleRegistry.getEnforcing<Spec>('CameraModule');

Antigravity に「既存のCameraModule.tsからTurboModule仕様のspecファイルを生成して」と依頼すると、既存コードを解析しながら上記のような型定義を自動生成します。

2-2. package.json への Codegen 設定追加

{
  "name": "my-camera-module",
  "codegenConfig": {
    "name": "CameraModuleSpecs",
    "type": "modules",
    "jsSrcsDir": "./src/specs",
    "android": {
      "javaPackageName": "com.myapp.camera"
    }
  }
}

2-3. iOS 側の実装(Swift)

Codegen が生成したプロトコルに準拠する Swift 実装:

// ios/CameraModule.swift
import Foundation
import AVFoundation
 
@objc(CameraModule)
class CameraModule: NSObject, NativeCameraModuleSpec {
 
  // セッション管理
  private var captureSession: AVCaptureSession?
  private var photoOutput: AVCapturePhotoOutput?
 
  @objc func requestCameraPermission(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    switch AVCaptureDevice.authorizationStatus(for: .video) {
    case .authorized:
      resolve("granted")
    case .denied, .restricted:
      resolve("denied")
    case .notDetermined:
      AVCaptureDevice.requestAccess(for: .video) { granted in
        resolve(granted ? "granted" : "denied")
      }
    @unknown default:
      resolve("denied")
    }
  }
 
  @objc func takePicture(
    _ options: NSDictionary,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    guard let output = photoOutput else {
      reject("NO_SESSION", "Camera session not initialized", nil)
      return
    }
 
    let settings = AVCapturePhotoSettings()
    let quality = options["quality"] as? Double ?? 0.8
 
    // フォトキャプチャ処理(省略)
    // Antigravityに "AVFoundation でHEIF/JPEG撮影実装して" と依頼すれば
    // 完全な実装コードを生成してもらえる
    _ = quality
    resolve(["uri": "file:///tmp/photo.jpg", "width": 1920, "height": 1080])
  }
 
  @objc func setFlashMode(_ mode: String) {
    // フラッシュモード実装
  }
 
  // TurboModules対応の必須メソッド
  @objc func addListener(_ eventName: String) {}
  @objc func removeListeners(_ count: Double) {}
}

2-4. Android 側の実装(Kotlin)

// android/app/src/main/java/com/myapp/CameraModule.kt
package com.myapp
 
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
import android.Manifest
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
 
@ReactModule(name = CameraModule.NAME)
class CameraModule(reactContext: ReactApplicationContext) :
    NativeCameraModuleSpec(reactContext) {
 
    companion object {
        const val NAME = "CameraModule"
    }
 
    override fun getName(): String = NAME
 
    override fun requestCameraPermission(promise: Promise) {
        val context = reactApplicationContext
        val hasPermission = ActivityCompat.checkSelfPermission(
            context,
            Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
 
        if (hasPermission) {
            promise.resolve("granted")
        } else {
            // パーミッションリクエスト処理
            // Antigravityにreact-native-permissionsとの統合実装を依頼可能
            promise.resolve("denied")
        }
    }
 
    override fun takePicture(options: ReadableMap, promise: Promise) {
        try {
            val quality = options.getDouble("quality")
            val includeBase64 = options.getBoolean("base64")
 
            // Camera2 API または CameraX を使って撮影
            // Antigravityに "CameraX APIを使った高品質撮影実装" を依頼
            val result = WritableNativeMap().apply {
                putString("uri", "file:///sdcard/photo.jpg")
                putInt("width", 1920)
                putInt("height", 1080)
            }
            promise.resolve(result)
        } catch (e: Exception) {
            promise.reject("CAPTURE_ERROR", e.message, e)
        }
    }
 
    override fun setFlashMode(mode: String) {
        // フラッシュモード切り替え実装
    }
 
    override fun addListener(eventName: String) {}
    override fun removeListeners(count: Double) {}
}

フェーズ3:Expo Router v3 の新機能を最大活用する

Expo Router v3 は単なるナビゲーションライブラリを超え、フルスタックフレームワークへと進化しました。

3-1. Server Components と API Routes

Expo Router v3 では React Server Components と API Routes が使えます:

app/
├── _layout.tsx          ← ルートレイアウト
├── (tabs)/
│   ├── _layout.tsx      ← タブレイアウト
│   ├── index.tsx        ← ホームタブ
│   └── profile.tsx      ← プロフィールタブ
├── api/
│   └── users+api.ts     ← API Route(サーバーサイド)
├── modal.tsx            ← モーダル画面
└── [id].tsx             ← 動的ルート

API Route の実装例:

// app/api/users+api.ts
import { ExpoRequest, ExpoResponse } from 'expo-router/server';
 
export async function GET(request: ExpoRequest): Promise<ExpoResponse> {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') ?? '1');
 
  // データベースから取得(Supabase/Firebase/Prisma等)
  const users = await fetchUsers({ page, limit: 20 });
 
  return ExpoResponse.json({
    users,
    pagination: {
      page,
      total: users.length,
      hasMore: users.length === 20,
    },
  });
}
 
export async function POST(request: ExpoRequest): Promise<ExpoResponse> {
  const body = await request.json();
 
  // バリデーション
  if (!body.email || !body.name) {
    return ExpoResponse.json(
      { error: 'email and name are required' },
      { status: 400 }
    );
  }
 
  const user = await createUser(body);
  return ExpoResponse.json(user, { status: 201 });
}

Antigravity に「このアプリのユーザー認証APIをExpo RouterのAPI Routesで実装して。JWTトークン管理も含めて」と依頼すると、Supabase Auth や Firebase Auth との統合コードまで生成してくれます。

3-2. Typed Routes による型安全なナビゲーション

Expo Router v3 の Typed Routes で、ナビゲーション先の型エラーをコンパイル時に検出できます:

// tsconfig.json に追加
{
  "compilerOptions": {
    "types": ["expo-router/types"]
  }
}
 
// app.json に追加
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}
// 使用例:型安全なナビゲーション
import { router, Link } from 'expo-router';
 
// ✅ コンパイル時に型チェックされる
router.push('/profile');
router.push({ pathname: '/[id]', params: { id: '123' } });
 
// ❌ 存在しないルートはコンパイルエラーになる
// router.push('/nonexistent'); // Type Error!
 
// Link コンポーネントも型安全
<Link href="/profile">プロフィール</Link>
<Link href={{ pathname: '/[id]', params: { id: userId } }}>
  ユーザー詳細
</Link>

3-3. Layouts と Slots の高度な活用

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { useColorScheme } from 'react-native';
import { Platform } from 'react-native';
 
export default function TabLayout() {
  const colorScheme = useColorScheme();
 
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: colorScheme === 'dark' ? '#fff' : '#000',
        // iOS 26のビジュアルデザインに対応
        tabBarStyle: Platform.select({
          ios: { position: 'absolute' },
          default: {},
        }),
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'ホーム',
          tabBarIcon: ({ color }) => (
            // Antigravityにアイコンコンポーネントの生成を依頼
            <HomeIcon color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="camera"
        options={{ title: 'カメラ' }}
      />
      <Tabs.Screen
        name="profile"
        options={{ title: 'プロフィール' }}
      />
    </Tabs>
  );
}

Antigravity × EAS Build:CI/CD パイプラインの完全自動化

4-1. eas.json の最適化

{
  "cli": {
    "version": ">= 10.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "simulator": true
      },
      "env": {
        "APP_ENV": "development"
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "env": {
        "APP_ENV": "staging"
      }
    },
    "production": {
      "autoIncrement": true,
      "channel": "production",
      "env": {
        "APP_ENV": "production"
      },
      "ios": {
        "resourceClass": "m-medium"
      },
      "android": {
        "buildType": "apk",
        "resourceClass": "medium"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your@apple.com",
        "ascAppId": "1234567890",
        "appleTeamId": "ABCDEFGHIJ"
      },
      "android": {
        "serviceAccountKeyPath": "./google-services-key.json",
        "track": "production"
      }
    }
  }
}

4-2. GitHub Actions との連携

Antigravity に「EAS BuildをGitHub Actionsに統合するワークフローを作成して」と依頼すると、以下のような完全なCIパイプラインを生成します:

# .github/workflows/eas-build.yml
name: EAS Build & Submit
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  build:
    name: EAS Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Setup Expo
        uses: expo/expo-github-action@v8
        with:
          expo-version: latest
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
 
      # PRの場合はpreviewビルド
      - name: Build (Preview)
        if: github.event_name == 'pull_request'
        run: eas build --profile preview --platform all --non-interactive
 
      # mainブランチへのマージ時はproductionビルド
      - name: Build (Production)
        if: github.ref == 'refs/heads/main'
        run: eas build --profile production --platform all --non-interactive
 
      # production ビルド時はApp Storeへ自動提出
      - name: Submit to Stores
        if: github.ref == 'refs/heads/main'
        run: eas submit --platform all --non-interactive
        env:
          EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}

4-3. OTA(Over The Air)アップデートの戦略的活用

// src/hooks/useOtaUpdate.ts
import { useEffect, useState } from 'react';
import * as Updates from 'expo-updates';
import { Alert } from 'react-native';
 
export function useOtaUpdate() {
  const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
  const [isUpdating, setIsUpdating] = useState(false);
 
  useEffect(() => {
    if (__DEV__) return; // 開発環境ではスキップ
 
    async function checkForUpdates() {
      try {
        const update = await Updates.checkForUpdateAsync();
        if (update.isAvailable) {
          setIsUpdateAvailable(true);
          // バックグラウンドでダウンロード開始
          await Updates.fetchUpdateAsync();
 
          Alert.alert(
            'アップデートが利用可能',
            '新しいバージョンが準備できました。今すぐ再起動しますか?',
            [
              { text: 'あとで', style: 'cancel' },
              {
                text: '再起動',
                onPress: async () => {
                  setIsUpdating(true);
                  await Updates.reloadAsync();
                },
              },
            ]
          );
        }
      } catch (error) {
        console.warn('OTA update check failed:', error);
      }
    }
 
    checkForUpdates();
  }, []);
 
  return { isUpdateAvailable, isUpdating };
}

よくあるエラーと Antigravity を使った解決法

5-1. "TurboModule not found" エラー

Error: Cannot find native module 'CameraModule'

原因: Codegen が正しく実行されていない、またはネイティブビルドが古い。

Antigravity への依頼:

"TurboModule not found" エラーの原因を分析して。
Podfile と build.gradle、package.json の codegenConfig を確認して
問題箇所と修正方法を教えて。

一般的な修正手順:

# iOS
cd ios && pod install --repo-update && cd ..
 
# Android
cd android && ./gradlew clean && cd ..
 
# ビルドキャッシュのクリア
npx expo prebuild --clean

5-2. Fabric レンダラーの互換性問題

新アーキテクチャ有効後に ScrollViewFlatList の挙動が変わるケースがあります。

// 旧アーキテクチャ時代のコード(問題が起きやすい)
<FlatList
  data={items}
  renderItem={({ item }) => <ItemComponent item={item} />}
  // Fabricでは onEndReachedThreshold の挙動が変わる
  onEndReachedThreshold={0.5}
  onEndReached={loadMore}
/>
 
// 新アーキテクチャ対応版
<FlashList  // @shopify/flash-list を使う(Fabricネイティブ最適化済み)
  data={items}
  renderItem={({ item }) => <ItemComponent item={item} />}
  estimatedItemSize={80}  // 必須:パフォーマンス最適化のヒント
  onEndReachedThreshold={0.5}
  onEndReached={loadMore}
/>

Antigravity に「FlatListをFlashListに移行して。型定義も含めてリファクタリングして」と依頼すると、プロジェクト全体のFlatListを自動的にFlashListに置き換えます。

5-3. Metro バンドラーの設定エラー

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
 
const config = getDefaultConfig(__dirname);
 
// 新アーキテクチャ対応設定
config.resolver.unstable_enablePackageExports = true;
 
// シンボリックリンク対応(monorepo環境)
config.resolver.nodeModulesPaths = [
  path.resolve(__dirname, 'node_modules'),
];
 
module.exports = config;

パフォーマンス計測:移行前後の比較

移行の効果を可視化するために、Antigravity と Flashlight(React Native パフォーマンス測定ツール)を組み合わせます:

# Flashlight のインストール
npm install -g @perf-tools/flashlight
 
# 測定実行
flashlight measure --app com.myapp --duration 30

実際のプロジェクトでの改善例(参考値):

  • 起動時間: 平均 2.8秒 → 1.9秒(約32%短縮)
  • FPS(スクロール時): 平均 52fps → 58fps(Fabric による同期レンダリング効果)
  • 初回レンダリング: 340ms → 220ms(TurboModules の遅延ロード効果)
  • メモリ使用量: 245MB → 198MB(約19%削減)

Antigravity に「Flashlightの計測結果JSONを分析して、ボトルネックと改善提案をリストアップして」と依頼すると、計測結果を解析して優先度付きの改善リストを生成します。


まとめ

React Native 新アーキテクチャへの移行は、一度に全てを変えようとすると失敗しやすい大規模な作業です。本記事で紹介したアプローチを振り返ると:

  1. Antigravity によるコード分析で移行コストを可視化し、3フェーズに分割
  2. TurboModules 化では Codegen と Antigravity の共同作業で Swift/Kotlin コードを効率生成
  3. Expo Router v3 の Server Components・API Routes・Typed Routes で開発体験を大幅向上
  4. EAS Build + GitHub Actions で本番リリースを完全自動化

Antigravity は特に「既存コードの解析と移行計画策定」「プラットフォームをまたいだコード生成」において真価を発揮します。まずは小さなモジュールひとつから TurboModules 化を試し、Antigravity とのペアプログラミングで移行の感触を掴んでみてください。

React Native 開発のさらなる深掘りには、Expo × Antigravity クロスプラットフォーム開発完全ガイドや、Antigravity × Fastlane App Store 自動デプロイ攻略も合わせてご参照ください。

実際のプロファイリング手法と最適化パターンが豊富に収録されており、本記事の内容をさらに深める良書です。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-07-18
「Compose ファースト」の後で、View の画面をどの順に移すか — 画面数ではなく変更頻度で決める
Google が Android 開発を Compose ファーストと表明しました。View ベースの画面をどの順で移すかを、画面数ではなく git の変更履歴から決める判断軸と、実際に動かしたスコアリングスクリプトをまとめます。
アプリ開発2026-07-13
画像の一括最適化をエージェントに任せる前に置く、見た目の劣化を止めるゲート
壁紙アプリの数百枚のアセットをエージェントに一括再エンコードさせたら、数枚だけ色がくすんでいました。サイズ削減率だけでは劣化を見抜けません。SSIM・ΔE・ファイルサイズの3軸でマージ前に不良変換を弾くゲートの設計と、Pillow と scikit-image で動く判定スクリプトをまとめます。
アプリ開発2026-07-09
段階公開を止める瞬間を、先に言葉にしておく — 監視はエージェント、停止判断は手元に
Google Play の段階公開を監視するエージェントを Antigravity で組んだ記録です。クラッシュ率のベースライン比、ANR の遅延、母数不足時の判断保留を停止基準に落とし込み、halt の実行権だけを手元に残しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →