はじめに
モバイルアプリを作りたいけど、SwiftとKotlin両方覚えるのはキツい。React Nativeを試そうと思ったけど、Xcodeの設定とかAndroid Studioの設定とかで挫折した。
そんな経験、ありませんか。
正直なところ、僕もその一人だったんですよ。Webエンジニアとして普段はReactを書いているから、React Nativeには興味があった。でもネイティブ周りの設定が面倒すぎて、何度も「やっぱりやめとこう」となっていました。
そんなときに出会ったのがExpo。GitHubで45,000スター以上、月間ダウンロード1,000万回超え。React Native開発者の80%が使っているという話も聞いて、試してみることにしました。
これ、もっと早く知りたかったですね。
Expoとは
ExpoはReact Nativeのフルスタックフレームワークです。Meta(旧Facebook)が公式に推奨している唯一のReact Nativeフレームワークでもあります。
「Android、iOS、Webで動作するユニバーサルネイティブアプリをReactで作れるオープンソースプラットフォーム」という説明が公式にありますが、要するに「React Nativeの面倒な部分を全部引き受けてくれる」ツールです。
最新バージョンは54.0.27で、MITライセンス。10年以上の開発実績があり、50万以上のプロジェクトで使われています。
特徴・メリット
1. ネイティブ設定が不要
これ、意外と大きいんですよ。
普通のReact Nativeだと、Xcodeでプロビジョニングプロファイルを設定して、Android Studioでgradle設定をいじって...みたいな作業が必要。Expoを使えば、そういうネイティブ周りの設定をほぼ全部スキップできます。
コスパ的に、この「設定レス」は最高です。
2. 100以上のプロダクション対応ライブラリ
カメラ、プッシュ通知、位置情報、ディープリンク...モバイルアプリで必要になりそうな機能は、Expo SDKとして用意されています。
個人的には、この「必要なものが揃っている」感じが良い。ライブラリ選定で悩む時間が減ります。
3. Expo Goで即座にテスト
開発中のアプリを実機でテストするの、普通は面倒じゃないですか。ビルドして、インストールして...。
Expo Goアプリをスマホに入れておけば、QRコードをスキャンするだけで開発中のアプリが動く。時短になる。
4. OTA(Over-the-Air)アップデート
アプリストアの審査を待たずに、JavaScriptのコード変更を即座にユーザーに配信できる。
これ、運用面で相当QOL上がります。バグ修正を数分で全ユーザーに反映できるのは強い。
5. EAS(Expo Application Services)
ビルド、デプロイ、アップデートをクラウドで実行できる統合サービス。ローカルにXcodeがなくてもiOSアプリをビルドできます。
Windowsユーザーでも問題なくiOSアプリを開発・ビルドできるのは、地味に革命的。
インストール方法
前提条件
Node.js(LTS推奨)がインストールされている必要があります。
プロジェクト作成
npx create-expo-app@latest my-app
cd my-app
これだけ。本当にこれだけでReact Nativeプロジェクトが立ち上がります。
開発サーバー起動
npx expo start
QRコードが表示されるので、スマホのExpo Goアプリでスキャンすれば実機で動作確認できます。
Expo Goアプリ
- iOS: App Storeで「Expo Go」を検索
- Android: Google Playで「Expo Go」を検索
基本的な使い方
プロジェクト構成
my-app/
├── app/ # ページコンポーネント(App Router)
│ ├── _layout.tsx # レイアウト
│ └── index.tsx # ホーム画面
├── components/ # 共通コンポーネント
├── assets/ # 画像・フォント
├── package.json
└── app.json # Expo設定
基本的なコンポーネント
import { Text, View, StyleSheet } from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Hello, Expo!</Text>
<Text style={styles.subtitle}>モバイルアプリ開発、始めました</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666',
},
});
ReactとほぼおなじAPIで書ける。divの代わりにView、pの代わりにTextを使うくらいの違いです。
Expo Routerでのナビゲーション
Expo Router(ファイルベースルーティング)を使えば、Next.jsみたいにファイル構造でルーティングを定義できます。
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'ホーム' }} />
<Stack.Screen name="profile" options={{ title: 'プロフィール' }} />
<Stack.Screen name="settings" options={{ title: '設定' }} />
</Stack>
);
}
// app/index.tsx
import { Link } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';
export default function Home() {
return (
<View style={styles.container}>
<Text style={styles.title}>ホーム画面</Text>
<Link href="/profile" asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>プロフィールへ</Text>
</Pressable>
</Link>
</View>
);
}
Webのルーティングを知っていれば、すんなり理解できる。
カメラ機能の実装
import { useState } from 'react';
import { Button, Image, View, StyleSheet } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
export default function CameraScreen() {
const [image, setImage] = useState<string | null>(null);
const takePhoto = async () => {
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
setImage(result.assets[0].uri);
}
};
return (
<View style={styles.container}>
<Button title="写真を撮る" onPress={takePhoto} />
{image && <Image source={{ uri: image }} style={styles.image} />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
image: {
width: 300,
height: 300,
marginTop: 20,
},
});
ネイティブのカメラ機能がこの短さで実装できる。expo-image-pickerを入れるだけ。
実践的なユースケース
プッシュ通知の実装
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export function usePushNotifications() {
const [expoPushToken, setExpoPushToken] = useState<string>('');
const notificationListener = useRef<any>();
useEffect(() => {
registerForPushNotificationsAsync().then((token) => {
if (token) setExpoPushToken(token);
});
notificationListener.current = Notifications.addNotificationReceivedListener(
(notification) => {
console.log('通知を受信:', notification);
}
);
return () => {
Notifications.removeNotificationSubscription(notificationListener.current);
};
}, []);
return { expoPushToken };
}
async function registerForPushNotificationsAsync() {
let token;
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') {
console.log('プッシュ通知の権限が拒否されました');
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
});
}
return token;
}
プッシュ通知って普通は設定が大変なんですけど、Expoなら比較的シンプルに実装できます。
位置情報の取得
import * as Location from 'expo-location';
import { useState, useEffect } from 'react';
import { Text, View, StyleSheet } from 'react-native';
export default function LocationScreen() {
const [location, setLocation] = useState<Location.LocationObject | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
(async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setErrorMsg('位置情報の権限が拒否されました');
return;
}
const location = await Location.getCurrentPositionAsync({});
setLocation(location);
})();
}, []);
return (
<View style={styles.container}>
{errorMsg ? (
<Text>{errorMsg}</Text>
) : location ? (
<Text>
緯度: {location.coords.latitude}
{'\n'}
経度: {location.coords.longitude}
</Text>
) : (
<Text>位置情報を取得中...</Text>
)}
</View>
);
}
ローカルストレージ(AsyncStorage)
import AsyncStorage from '@react-native-async-storage/async-storage';
// データ保存
const saveData = async (key: string, value: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('保存エラー:', error);
}
};
// データ取得
const loadData = async (key: string) => {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('読み込みエラー:', error);
return null;
}
};
// 使用例
await saveData('user_settings', { theme: 'dark', language: 'ja' });
const settings = await loadData('user_settings');
EASでのビルド・デプロイ
# EAS CLIのインストール
npm install -g eas-cli
# EASにログイン
eas login
# ビルド設定の初期化
eas build:configure
# 開発用ビルド
eas build --profile development --platform all
# 本番用ビルド
eas build --profile production --platform all
# ストアへの提出
eas submit --platform ios
eas submit --platform android
ローカルでXcodeを使わなくても、クラウドでiOSアプリをビルドできる。これ、Mac持ってない人には一択ですね。
Webエンジニアから見たExpoの良さ
30代になって思うのは、新しい技術を学ぶときの「初期コスト」をいかに下げるかが重要だということ。
ExpoはWebエンジニア目線でかなり親切に作られています。
- React知識が活かせる: コンポーネント設計、hooks、状態管理、全部そのまま使える
- ファイルベースルーティング: Next.js使ってる人なら違和感ゼロ
- TypeScriptサポート: 最初からTypeScriptで書ける
- npm/yarnエコシステム: 慣れたパッケージ管理
個人的には、「Webの延長線上でネイティブアプリが作れる」のが最大のメリットだと思います。
まとめ
Expoを使ってみて感じた変化:
- 環境構築: 5分で完了。Xcode/Android Studioの設定地獄から解放
- 学習コスト: React知ってれば即戦力。ネイティブ固有の知識は最小限
- 開発速度: Expo Goで即座にテスト。ホットリロードでサクサク開発
- デプロイ: EASでビルド・提出まで一気通貫
- OTAアップデート: ストア審査なしで即反映
正直なところ、個人開発や小規模チームでReact Nativeを使うなら、Expo一択ですね。
「モバイルアプリ作りたいけど、ネイティブ開発は敷居が高い」と思っている人ほど、Expoの恩恵を受けられると思います。まずはnpx create-expo-appを叩いてみてください。
5分後には実機でアプリが動いていますよ。