OpenAIExpo2026/01/28 14:45

The solo dev playbook: ship faster with Expo, EAS Build, and OTA Updates

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

ソロ開発者のプレイブック:Expo・EAS・OTAで高速リリース

Key Points

  • OTAで即時修正
  • EASが証明書を自動管理
  • Expoでネイティブ不要

Summary

1人開発でiOS/Androidを一週間で出すための実践メモ。Expo SDK(audio/camera/notifications等)とEAS(Build/Update/Submit)を組み合わせることで、ネイティブコードを書かずに短期間でクロスプラットフォームアプリを構築・署名・配布できる。OTA(EAS Update)でストア審査を待たずにバグ修正や改善を配信できるため、初期リリース後の反復が高速になる。

Key Points

  • ネイティブ不要/単一コードベース: Expo SDKでネイティブAPIをTypeScriptフック経由で扱える(例: expo-audio, expo-camera, expo-notifications)。
  • 録音/再生の設計: useAudioRecorder / useAudioPlayer のようなフックをラップした共通コンポーネント(RecordingButton, AudioPlaybackBar)を作り、全画面で再利用する。
  • カメラ実装: useCameraPermissions / useMicrophonePermissionsで権限をガードし、recordAsync({ maxDuration: 60 })などで短尺動画を簡潔に扱う。
  • 通知とリマインダー: expo-notificationsでチャンネル作成・権限取得・Expoプッシュトークンを取得し、リマインダーロジックに組み込む。
  • ビルドと証明書管理: EAS Build/Submitはプロビジョニングやキーストアを自動化。Xcode/Android Studioを開かずにバイナリ作成・提出が可能。
  • OTAアップデート: EAS Updateでコード/JSの修正を即配信。更新ポリシー(起動時反映、強制更新など)を設計して互換性を保つ。
  • 実運用上の注意: 権限フローはUXに配慮して実機で検証、音声はバックエンドへ安全にアップロードしてトランスクリプト/解析パイプラインを用意する。

Implementation tips

  • 抽象化: 録音や再生、カメラUIは小さな再利用コンポーネントに分離してテストしやすくする。
  • 権限とフィードバック: 権限拒否時のフォールバックを用意し、Haptics等で操作フィードバックを追加する。
  • CI/CD: EASのビルドプロファイルを整え、デプロイ手順(build→submit→update)を自動化する。
  • モニタリング: 初期週はクラッシュ/解析を細かく見て、OTAで迅速に修正を流す。

Tech stack参考: React Native + Expo SDK 54, TypeScript, LangChain/OpenAI (解析), AssemblyAI (文字起こし), RevenueCat (課金), EAS Build/Update/Submit

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

ソロ開発者のプレイブック:Expo、EAS Build、OTA Updatesでより速くリリースする

ソロ開発者のプレイブック:Expo、EAS Build、OTA Updatesでより速くリリースする

Users • React Native • Development • 2026-01-28 • 13分読み物

Liam Du(ゲスト著者)

クロスプラットフォームアプリを迅速にビルドして出荷する方法:音声、カメラ、通知、安全なストレージ、そしてEAS Build、Update、Submitを使ったワークフロー。この投稿はWellspokenの創業者であり、より良いコミュニケーションの支持者であるLiam Duによるゲスト投稿です。

序盤の背景とアイデア

会議で自分の製品アイデアをチームに説明しようとしていました。頭の中ではコンセプトは明確で、よく考えていて理にかなっていると分かっていましたが、話し始めると言葉がうまく出てきませんでした。言い直したり、また最初の説明に戻ったり、話の筋を失ったり。何度も同じ会議で起きました。試みるたびに動揺していき、表現がさらに悪くなり、さらに動揺するという悪循環。正直恥ずかしかったです。自分が言いたいことは分かっている。アイデアも明確。しかし、思考とそれを明確に表現する能力の間にギャップがあり、特にプレッシャーや緊張時に顕著でした。

しばらくは自分だけの問題だと思っていましたが、他の場所でも同じ現象を目にするようになりました。話している内容は明確に理解しているのに、会話の中でうまく言語化できない賢い人たち。そこで、これを助けるツールを探しました。スピーチコーチ(高額)、パブリックスピーキングのコース(問題が違う)、声の投影やステージプレゼンスに特化したアプリ(形式が違う)などを見つけましたが、日常会話でその場で思考を構造化する能力を高める、という実際に必要な点を満たすものはありませんでした。

そこで、自分でアプリを作ることにしました。発話力のためのジム。日々の短いエクササイズで、リアルタイムで思考を整理し明確に表現する認知スキルを鍛えるアプリ。これをWellspokenと名付けました。

なぜExpoを選んだのか

私はiOSとAndroidの両方でわずか1週間でリリースすることを目標にしました。デザインとエンジニアリングを一人でこなすソロ開発者として、インフラやプラットフォーム固有の問題に時間を費やす余裕はありませんでした。実際のプロダクト構築に集中する必要がありました。Expoはこれを可能にしました。

  • ネイティブ開発不要:Expo SDKはSwiftやKotlinを書かずにネイティブ機能へアクセスさせてくれました。オーディオ録音、ファイルシステムアクセス、通知などはクリーンなTypeScript APIで扱えます。
  • ビルド設定地獄からの解放:EAS Buildがプラットフォーム固有のコンパイルを処理してくれました。XcodeやAndroid Studioを一度も開く必要はありませんでした。
  • 証明書管理不要:プロビジョニングプロファイル、署名証明書、キーストア類はEASが自動で処理してくれました。コマンドラインのチュートリアルに従うだけで完了しました。
  • リリース後の高速イテレーション:OTAアップデートにより、App Storeの審査を待たずにバグ修正や改善を配布できます。更新をプッシュし、次回アプリ起動でユーザーに適用されます。ローンチ直後に何度もこれに助けられました。

ソロ開発でスケジュールが厳しい場合、ネイティブ開発へ切り替えたり、認証情報管理に時間を取られる必要がないことが、出荷できるかどうかの差を生みました。

Wellspokenの機能とアーキテクチャ

コア機能:

  • パーソナライズされた練習セッション(模擬面接、トピック説明、役割演技など)
  • AIによる発話解析(どこで集中を失ったか、苦労した箇所を特定)
  • 日次5分エクササイズ
  • 進捗トラッキングと連続実施(streaks)
  • フリートライアル付きのサブスクリプション課金

テックスタック:

  • React Native + Expo SDK 54
  • TypeScript
  • Langchain
  • OpenAI API(発話解析用)
  • Assembly AI(文字起こし)
  • RevenueCat(サブスクリプション管理)
  • EAS Build、Update、Submit

どのようにExpoを実装したか

以下は私が使った主要なExpo機能と、それがWellspokenの構築をいかに容易にしたかの説明です。

  1. expo-audio:録音体験の核

オーディオ録音はWellspokenの基盤です。すべての練習モードで高品質な音声キャプチャが必要で、iOSとAndroidで一貫して動作することが求められました。ネイティブコードを書かずにexpo-audioがこれを単純にしてくれました。hooksベースのクリーンなAPIを提供します。

Code Copy // components/RecordingButton.tsx
import { useAudioRecorder , useAudioRecorderState , AudioModule , RecordingPresets , setAudioModeAsync } from 'expo-audio' ;
import * as Haptics from 'expo-haptics' ;
export function RecordingButton ( ) {
const recorder = useAudioRecorder ( RecordingPresets . HIGH_QUALITY ) ;
const recorderState = useAudioRecorderState ( recorder ) ;
const [ hasPermission , setHasPermission ] = useState ( false ) ;
async function startRecording ( ) {
try {
// Request permissions
const { status } = await AudioModule . requestRecordingPermissionsAsync ( ) ;
if ( status !== 'granted' ) return ;
// Configure audio mode
await setAudioModeAsync ( { allowsRecording : true , playsInSilentMode : true , } ) ;
// Prepare and start recording
await recorder . prepareToRecordAsync ( ) ;
recorder . record ( ) ;
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Medium ) ;
} catch ( error ) {
console . error ( 'Failed to start recording:' , error ) ;
}
}
async function stopRecording ( ) {
try {
await recorder . stop ( ) ;
Haptics . notificationAsync ( Haptics . NotificationFeedbackType . Success ) ;
// recorder.uri contains the file path
const audioUri = recorder . uri ;
// Upload to backend for analysis...
} catch ( error ) {
console . error ( 'Failed to stop recording:' , error ) ;
}
}
return ( < TouchableOpacity onPress = { recorderState . isRecording ? stopRecording : startRecording } > < View style = { recorderState . isRecording ? styles . recording : styles . idle } > { recorderState . isRecording ? < StopIcon / > : < MicrophoneIcon / > } < / View > < / TouchableOpacity > ) ;
}

なぜこれが素晴らしいのか:

  • useAudioRecorderフックがすべての状態を管理
  • RecordingPresets.HIGH_QUALITYがプラットフォーム固有のエンコーディング設定を処理
  • 同じコードがiOSとAndroidの両方で動作
  • ネイティブモジュールの設定不要

このRecordingButtonコンポーネントはQ&A、フレームワーク練習、スピード分解、自由形式などすべての練習モードで再利用しています。一度書けばどこでも使えます。

オーディオ再生も同様にシンプルです:

Code Copy // components/AudioPlaybackBar.tsx
import { useAudioPlayer , useAudioPlayerStatus } from 'expo-audio' ;
export function AudioPlaybackBar ( { audioUri } : { audioUri : string } ) {
const player = useAudioPlayer ( audioUri ) ;
const status = useAudioPlayerStatus ( player ) ;
function togglePlayback ( ) {
if ( status . playing ) {
player . pause ( ) ;
} else {
player . play ( ) ;
}
}
return ( < View > < TouchableOpacity onPress = { togglePlayback } > { status . playing ? < PauseIcon / > : < PlayIcon / > } < / TouchableOpacity > < Slider value = { status . currentTime } maximumValue = { status . duration } onSlidingComplete = { ( value ) => player . seekTo ( value ) } / > < Text > { formatTime ( status . currentTime ) } / { formatTime ( status . duration ) } < / Text > < / View > ) ;
}

useAudioPlayerフックが再生状態を自動管理し、useAudioPlayerStatusは再生進捗をリアルタイムで提供します。

  1. expo-camera:Daily 60ビデオ練習

Wellspokenの機能の1つに「Daily 60」があります。これはユーザーがトピックを説明する60秒のビデオ練習です。フロント/バックの切替、ビデオ録画、カメラとマイクの権限取得が必要でした。expo-cameraがこれらを処理しました。

Code Copy // screens/practice/Daily60PracticeScreen.tsx
import { CameraView , useCameraPermissions , useMicrophonePermissions } from 'expo-camera' ;
import * as Haptics from 'expo-haptics' ;
export function Daily60PracticeScreen ( ) {
const [ cameraPermission , requestCameraPermission ] = useCameraPermissions ( ) ;
const [ micPermission , requestMicPermission ] = useMicrophonePermissions ( ) ;
const [ facing , setFacing ] = useState < 'front' | 'back' > ( 'front' ) ;
const [ isRecording , setIsRecording ] = useState ( false ) ;
const cameraRef = useRef < CameraView > ( null ) ;
async function startRecording ( ) {
if ( ! cameraRef . current ) return ;
try {
setIsRecording ( true ) ;
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Heavy ) ;
// Automatically stops at 60 seconds
const video = await cameraRef . current . recordAsync ( { maxDuration : 60 , } ) ;
// video.uri contains the recorded video
await handleVideoComplete ( video . uri ) ;
} catch ( error ) {
console . error ( 'Recording failed:' , error ) ;
}
}
async function stopRecording ( ) {
if ( ! cameraRef . current ) return ;
cameraRef . current . stopRecording ( ) ;
}
function toggleCameraFacing ( ) {
setFacing ( current => current === 'front' ? 'back' : 'front' ) ;
Haptics . selectionAsync ( ) ;
}
// Request permissions if needed
if ( ! cameraPermission ?. granted || ! micPermission ?. granted ) {
return ( < View > < Text > Camera and microphone access needed for video practice < / Text > < Button onPress = { ( ) => { requestCameraPermission ( ) ; requestMicPermission ( ) ; } } > Grant Permissions < / Button > < / View > ) ;
}
return ( < View style = { { flex : 1 } } > < CameraView ref = { cameraRef } style = { { flex : 1 } } facing = { facing } mode = "video" onCameraReady = { ( ) => console . log ( 'Camera ready' ) } > < View style = { styles . controls } > < TouchableOpacity onPress = { toggleCameraFacing } > < FlipCameraIcon / > < / TouchableOpacity > < TouchableOpacity onPress = { isRecording ? stopRecording : startRecording } > < View style = { isRecording ? styles . stopButton : styles . recordButton } / > < / TouchableOpacity > < / View > < / CameraView > { isRecording && ( < View style = { styles . timer } > < Text > { recordingDuration } s / 60s < / Text > < / View > ) } < / View > ) ;
}

何が簡単にしているのか:

  • useCameraPermissionsフックが権限状態の管理を担当
  • カメラの切り替えはfacingプロパティを変えるだけ
  • recordAsyncのmaxDurationで自動的に60秒で停止
  • 同じカメラUIコードがiOSとAndroidの両方で動作

代替案としては、SwiftとKotlinで別々にネイティブなカメラコードを書き、異なる権限システムに対応し、2つのコードベースを管理する必要がありました。expo-cameraのおかげで、本来1週間かかる仕事が1日に短縮されました。

  1. expo-notifications:日々の練習リマインダー

ユーザーを日々戻らせることはWellspokenにとって重要でした。プッシュ通知で練習リマインダーを出したかったのですが、APNsやFCMの設定、デバイストークンの手動管理は避けたかった。expo-notificationsがすべてを処理してくれました。

Code Copy // contexts/AuthContext.tsx
import * as Notifications from 'expo-notifications' ;
import { Platform } from 'react-native' ;
// Configure notification behavior
Notifications . setNotificationHandler ( { handleNotification : async ( ) => ( { shouldShowAlert : true , shouldPlaySound : true , shouldSetBadge : false , } ) , } ) ;
async function registerForPushNotifications ( ) {
// Android notification channel setup
if ( Platform . OS === 'android' ) {
await Notifications . setNotificationChannelAsync ( 'default' , { name : 'default' , importance : Notifications . AndroidImportance . MAX , vibrationPattern : [ 0 , 250 , 250 , 250 ] , } ) ;
}
// Request permissions
const { status : existingStatus } = await Notifications . getPermissionsAsync ( ) ;
let finalStatus = existingStatus ;
if ( existingStatus !== 'granted' ) {
const { status } = await Notifications . requestPermissionsAsync ( ) ;
finalStatus = status ;
}
if ( finalStatus !== 'granted' ) {
return null ;
}
// Get the Expo push token
const tokenData = await Notifications

ここまでで示したように、expo-notificationsは権限要求、プラットフォーム固有のチャンネル設定、通知ハンドリングの標準化を行ってくれます(上記コードは途中で切れていますが、続きはExpoのドキュメント通りにトークン取得やサーバーへの登録を行います)。


注:この記事は元記事の一部を翻訳したものです。コードの断片は元のまま保持してあります。実装の詳細や完全なサンプルは公式ドキュメント(Expo、EAS、各ライブラリのリファレンス)を参照してください。