Expo

モバイル開発React NativeクロスプラットフォームJavaScriptTypeScript開発ツール

モバイルプラットフォーム

Expo

概要

Expoは、React Nativeの開発体験を革命的に向上させるプラットフォームです。ビルド、テスト、デプロイメントを大幅に簡素化し、クロスプラットフォームアプリ開発を加速します。2025年現在、React Native開発の事実上の標準ツールチェーンとして確立されており、EAS(Expo Application Services)により、CI/CDパイプラインの構築も劇的に簡単になりました。

詳細

Expoは、React Nativeエコシステムの中核を成すプラットフォームとして、開発者の生産性向上に重点を置いています。従来のReact Native開発で必要だった複雑な環境設定や、iOS/Android固有の開発環境の準備を最小限に抑え、JavaScriptとTypeScriptの知識だけで本格的なネイティブアプリ開発を可能にします。

2025年の最新アップデートでは、Web開発者にとって特に親しみやすいワークフローを提供し、Expo Go アプリを使った即座のプレビュー機能により、コードの変更をリアルタイムで物理デバイス上で確認できます。また、EAS Build サービスにより、クラウド上でのアプリビルドと配布が可能になり、開発チームの規模に関係なく、プロフェッショナルなアプリ開発ワークフローを実現します。

メリット・デメリット

メリット

  • 開発効率の飛躍的向上: Expo CLIにより、プロジェクトのセットアップから本番デプロイまでが数分で完了
  • クロスプラットフォーム対応: iOS、Android、Webアプリを単一のコードベースで開発
  • 豊富なAPIライブラリ: カメラ、GPS、プッシュ通知など、50以上のネイティブ機能への統一されたアクセス
  • ホットリロード: コード変更の即座の反映により、開発サイクルを大幅に短縮
  • OTA(Over The Air)更新: アプリストア審査を経ずに、JavaScript部分の更新を配信
  • EAS統合: クラウドベースのビルド、テスト、配布サービスとの完全統合

デメリット

  • React Native依存: React Nativeの制限や問題の影響を受ける可能性
  • カスタムネイティブコード: 特殊なネイティブ機能が必要な場合、追加の設定が必要
  • アプリサイズ: Expo SDKにより、アプリのサイズが若干増加する場合がある
  • プラットフォーム依存性: Expoエコシステムへの依存度が高い

参考ページ

書き方の例

プロジェクトセットアップと初期化

# Expo CLIのインストール
npm install -g @expo/cli

# 新規プロジェクト作成
npx create-expo-app MyMobileApp

# プロジェクトディレクトリに移動
cd MyMobileApp

# 開発サーバー起動
npx expo start

認証実装(Firebase Auth統合)

// App.tsx
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'firebase/auth';
import { useState } from 'react';
import { View, TextInput, Button, Alert } from 'react-native';

const firebaseConfig = {
  // Firebase設定
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

export default function AuthScreen() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const signIn = async () => {
    try {
      await signInWithEmailAndPassword(auth, email, password);
      Alert.alert('成功', 'ログインしました');
    } catch (error) {
      Alert.alert('エラー', error.message);
    }
  };

  const signUp = async () => {
    try {
      await createUserWithEmailAndPassword(auth, email, password);
      Alert.alert('成功', 'アカウントを作成しました');
    } catch (error) {
      Alert.alert('エラー', error.message);
    }
  };

  return (
    <View style={{ padding: 20 }}>
      <TextInput
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
        style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
      />
      <Button title="サインイン" onPress={signIn} />
      <Button title="サインアップ" onPress={signUp} />
    </View>
  );
}

バックエンド統合(API通信)

// services/api.ts
const API_BASE_URL = 'https://your-api.com/api';

interface User {
  id: string;
  name: string;
  email: string;
}

class ApiService {
  private async request(endpoint: string, options: RequestInit = {}) {
    const url = `${API_BASE_URL}${endpoint}`;
    const config = {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    };

    const response = await fetch(url, config);
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return response.json();
  }

  async getUsers(): Promise<User[]> {
    return this.request('/users');
  }

  async createUser(userData: Partial<User>): Promise<User> {
    return this.request('/users', {
      method: 'POST',
      body: JSON.stringify(userData),
    });
  }

  async updateUser(id: string, userData: Partial<User>): Promise<User> {
    return this.request(`/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(userData),
    });
  }
}

export const apiService = new ApiService();

// 使用例
import { useEffect, useState } from 'react';
import { FlatList, Text, View } from 'react-native';

export default function UsersList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadUsers();
  }, []);

  const loadUsers = async () => {
    try {
      const data = await apiService.getUsers();
      setUsers(data);
    } catch (error) {
      console.error('Failed to load users:', error);
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return <Text>読み込み中...</Text>;
  }

  return (
    <FlatList
      data={users}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 10 }}>
          <Text>{item.name}</Text>
          <Text>{item.email}</Text>
        </View>
      )}
    />
  );
}

プッシュ通知実装

// services/notifications.ts
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';

// 通知設定
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export class NotificationService {
  static async registerForPushNotificationsAsync() {
    let token;

    if (Platform.OS === 'android') {
      await Notifications.setNotificationChannelAsync('default', {
        name: 'default',
        importance: Notifications.AndroidImportance.MAX,
        vibrationPattern: [0, 250, 250, 250],
        lightColor: '#FF231F7C',
      });
    }

    if (Device.isDevice) {
      const { status: existingStatus } = await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
      
      if (existingStatus !== 'granted') {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      
      if (finalStatus !== 'granted') {
        alert('プッシュ通知の許可が必要です');
        return;
      }
      
      token = (await Notifications.getExpoPushTokenAsync()).data;
    } else {
      alert('実際のデバイスでプッシュ通知をテストしてください');
    }

    return token;
  }

  static async sendLocalNotification(title: string, body: string) {
    await Notifications.scheduleNotificationAsync({
      content: {
        title,
        body,
      },
      trigger: null,
    });
  }
}

// App.tsx での使用例
import { useEffect } from 'react';

export default function App() {
  useEffect(() => {
    NotificationService.registerForPushNotificationsAsync().then(token => {
      console.log('Push token:', token);
      // サーバーにトークンを送信
    });

    // 通知受信時の処理
    const subscription = Notifications.addNotificationReceivedListener(notification => {
      console.log('Notification received:', notification);
    });

    // 通知タップ時の処理
    const responseSubscription = Notifications.addNotificationResponseReceivedListener(response => {
      console.log('Notification tapped:', response);
    });

    return () => {
      subscription.remove();
      responseSubscription.remove();
    };
  }, []);

  return (
    // アプリのコンテンツ
  );
}

アナリティクス統合(Firebase Analytics)

// services/analytics.ts
import { initializeApp } from 'firebase/app';
import { getAnalytics, logEvent, setUserId, setUserProperties } from 'firebase/analytics';

const firebaseConfig = {
  // Firebase設定
};

const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);

export class AnalyticsService {
  static logCustomEvent(eventName: string, parameters?: Record<string, any>) {
    logEvent(analytics, eventName, parameters);
  }

  static logScreenView(screenName: string) {
    logEvent(analytics, 'screen_view', {
      screen_name: screenName,
      screen_class: screenName,
    });
  }

  static logUserAction(action: string, item?: string) {
    logEvent(analytics, 'user_action', {
      action,
      item,
      timestamp: new Date().toISOString(),
    });
  }

  static setUser(userId: string, properties?: Record<string, any>) {
    setUserId(analytics, userId);
    if (properties) {
      setUserProperties(analytics, properties);
    }
  }

  static logPurchase(transactionId: string, value: number, currency: string) {
    logEvent(analytics, 'purchase', {
      transaction_id: transactionId,
      value,
      currency,
    });
  }
}

// hooks/useAnalytics.ts - React Hook
import { useEffect } from 'react';
import { AnalyticsService } from '../services/analytics';

export const useScreenTracking = (screenName: string) => {
  useEffect(() => {
    AnalyticsService.logScreenView(screenName);
  }, [screenName]);
};

// コンポーネントでの使用例
import { useScreenTracking } from '../hooks/useAnalytics';

export default function HomeScreen() {
  useScreenTracking('Home');

  const handleButtonPress = () => {
    AnalyticsService.logUserAction('button_press', 'home_cta');
    // ボタンアクション
  };

  return (
    // UIコンテンツ
  );
}

本番環境デプロイ(EAS Build)

# EAS CLI インストール
npm install -g eas-cli

# EAS にログイン
eas login

# プロジェクト設定
eas build:configure

# Development Build(開発用)
eas build --platform android --profile development
eas build --platform ios --profile development

# Production Build(本番用)
eas build --platform android --profile production
eas build --platform ios --profile production

# App Store / Google Play Store への自動提出
eas submit --platform android
eas submit --platform ios
// eas.json - EAS設定ファイル
{
  "cli": {
    "version": ">= 7.8.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "resourceClass": "m-medium"
      }
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": true
      }
    },
    "production": {
      "ios": {
        "resourceClass": "m-medium"
      }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "[email protected]",
        "ascAppId": "1234567890",
        "appleTeamId": "ABCDEFG123"
      },
      "android": {
        "serviceAccountKeyPath": "./path-to-google-service-account.json",
        "track": "production"
      }
    }
  }
}
// app.config.ts - アプリ設定
export default {
  expo: {
    name: "My Mobile App",
    slug: "my-mobile-app",
    version: "1.0.0",
    orientation: "portrait",
    icon: "./assets/icon.png",
    userInterfaceStyle: "light",
    splash: {
      image: "./assets/splash.png",
      resizeMode: "contain",
      backgroundColor: "#ffffff"
    },
    assetBundlePatterns: [
      "**/*"
    ],
    ios: {
      supportsTablet: true,
      bundleIdentifier: "com.yourcompany.mymobileapp"
    },
    android: {
      adaptiveIcon: {
        foregroundImage: "./assets/adaptive-icon.png",
        backgroundColor: "#FFFFFF"
      },
      package: "com.yourcompany.mymobileapp"
    },
    web: {
      favicon: "./assets/favicon.png"
    },
    extra: {
      eas: {
        projectId: "your-eas-project-id"
      }
    },
    updates: {
      url: "https://u.expo.dev/your-eas-project-id"
    },
    runtimeVersion: {
      policy: "sdkVersion"
    }
  }
};