React のウェブ版からネイティブアプリへ、一週間で移行した
React Web 開発者が Expo、Claude Code、Expo Skills を使って1週間で実際に動く iOS アプリを作り上げた全記録。どこまで応用が利いて、どこからは通用しないのか。
日本語
コピー

React 開発者として、毎日 Next.js、Tailwind、コンポーネントライブラリ、デザインシステムを書いている。この世界はもう手に馴染んでいる。ネイティブ開発はまったく別物で、何年ものあいだ、その隔たりはとてつもなく大きく見えていた。
iOS アプリを作ろうと考えるたびに、頭に浮かぶのは Xcode、Swift、設定ファイルの山。そして「Hello World」にすらたどり着かないうちに2週間が消えている光景だった。
それが、1週間で本当に自分の iPhone 上で動くネイティブ iOS アプリを作り終えた。環境構築、ネイティブのデバッグ、実機テスト、ビルドまで、端から端まで全部通した。
チュートリアルのサンプルでも、カウンターでも、またひとつ増えた ToDo リストでもない。作ったのは Sun Buddy——かわいいアニメーションの太陽マスコットがいて、現在地の紫外線をリアルタイムで追跡し、触覚フィードバック、毎日のローカル通知、永続化された進捗、そして日が沈むと眠りにつく夜間状態を備えたネイティブ iOS アプリだ。

使ったのは Expo、Claude Code、そして Expo Skills。
環境の内訳: Expo SDK(最新版)、Claude Code(最新版)、Expo Skills は /plugin marketplace add expo/skills のあと /plugin install expo でインストール、最新版の macOS と Xcode、iOS シミュレータと実機 iPhone で開発し、最終的には EAS でビルド。
以下が端から端までの記録だ。React からそのまま持ち込めたもの、持ち込めなかったもの、Expo がどこでネイティブ開発を手の届くものにしてくれたか、そしてプラットフォームの現実がどこで顔を出したか。
短く言えば、アプリ開発者に先にならなくても、このアプリは作れた。学ぶ必要があったのは、すでに持っている React スキルの周りにあるネイティブの縁の部分だけだった。
アイデア:Sun Buddy
ネイティブ開発を本当に試すには、アプリは十分に本物でなければならなかった。本質的にアプリの皮をかぶったウェブページでは意味がない。触りたかったのは本物のデバイス機能だ。位置情報、触覚、通知、ネイティブアニメーション、ローカルストレージ、そして自分の iPhone で実際に動くビルド成果物。
そこで日照トラッキングを選んだ。Sun Buddy は、推奨される屋外日照時間を達成するのを手伝う習慣アプリで、実際の位置情報の紫外線指数に基づいて追跡する。
考え方はシンプルだ。人は毎日少しは日光を浴びるべきなのに、ほとんどの人は浴びていない。進捗に応じて気分が変わる、Sunny という小さなマスコットが欲しかった。
Sunny にはいくつかの状態がある:
-
充電 0% のときは眠そう
-
始めたばかりのときは好奇心いっぱい
-
半分達成でごきげん
-
目標達成で輝きわたる
-
夜、日が沈むと眠りにつく
ちょっと変で、ちょっとかわいい。自分の iPhone に本当に入れておきたくなるようなアプリだ。

Sun Buddy は意図的に小さく作ってある。そしてそこが要点だ。範囲を絞りつつ、本物のネイティブインターフェースを一通り通らされるアプリが欲しかった。位置情報の権限、デバイス API、触覚フィードバック、ローカル通知、永続化された状態、アニメーション、アプリ設定、そして実機ビルド。
これらのパターンは太陽のマスコット専用のものではない。フィットネスアプリ、習慣トラッカー、外勤ツール、旅行アプリ、社内ダッシュボード、配送フローなど、スマホ上で自然に感じられる必要のあるアプリならどこでも、同じパターンに出会う。
はじめかた:まず Expo、次に Claude Code
この経験で一番重要だったのは AI を使ったことではなく、すでに身につけている React のメンタルモデルでネイティブアプリを構築できたことだ。そしてまさにそこが Expo の要点だ。 Expo はアプリの構造、ルーティング、デバイス API、ビルドの道筋、「React はわかる」から「自分の iPhone で動く」までの橋を渡してくれた。
Claude Code はペア作業のループをくれた。そして Expo Skills がそのループをはるかに有用なものにしてくれた。
この違いは大きい。Claude Code だけでもコードは書けるが、ネイティブ開発には一般的なアドバイスではカバーできない細部が山ほどある。権限、config plugin、EAS Build の設定ファイル、App Store の制約、ネイティブモジュールの対応状況。まさに古くなっていたり曖昧だったりするガイダンスが一番時間を無駄にする場所だ。
具体的な例をひとつ: 早い段階で Reanimated を追加した途端、Sunny のアニメーションが壊れた。一般的な AI の提案に従って、バージョンの不一致と間違った import で30分ほど格闘した。expo-dev-client Skill は、Reanimated の Babel プラグインが babel.config.js に登録されているか、Metro キャッシュをクリアしたかを確認すべきだと即座に把握した。設定2行、--clear フラグひとつで、アニメーションは直った。まさに一般的なアドバイスがよく取りこぼす、ネイティブ固有の文脈だ。
Expo Skills は Expo に関する文脈をワークフローに持ち込んでくれる。
最もはっきりした差が出るのは、ネイティブ開発ならではの質問をしたときだ。一般的な AI アシスタントはコンポーネントの書き方を教えてくれるが、そこは難所ではない。難しいのはこういう問いだ:
-
ここは普通の Expo Go のフローでいくべきか、それとも dev client か?
-
この権限はどこで設定する必要がある?
-
これはランタイムライブラリか、config plugin か、それとも両方か?
-
iPhone 版を実際にビルドするには、正しい道筋はどれか?
-
シミュレータでのテストから TestFlight に移ると、何が変わるのか?
ここで Expo Skills が効いてくる。React Native に関する漠然とした推測ではなく、このペアプログラミングのループに Expo 固有のコンテキストを与えてくれる。Claude Code にこれらの Skills を追加した:
/plugin marketplace add expo/skills
/plugin install expo
Expo Skills は Claude Code、Cursor、Codex などの agent で使える構造化された指示ファイルだ。自分が使ったのは expo plugin で、現在も活発にメンテナンスされている 3 つの Expo Skills が同梱されている:
-
building-native-ui—— Expo Router のパターン、ネイティブ UI 構造、Apple HIG ガイドライン -
expo-deployment—— EAS Build、TestFlight、App Store、クレデンシャル -
expo-dev-client—— development build とネイティブモジュールのサポート
これで Claude Code は汎用のコーディングアシスタントというより、Expo エコシステムを理解した協働者に近づく。
ちなみに: 最初は Expo Go を使い、数分で React のシェルを iPhone 上で動かせた。だが Sunny に Skia を、アニメーションに Reanimated を追加した時点で development build に切り替えた。どちらも Expo Go に含まれないネイティブモジュールに依存しているからだ。Skills のおかげでこの切り替えは明確になった。アプリを壊して初めて気づく、という事態にはならなかった。
意外だったこと:React のスキルがほぼそのまま移行できた
これがメンタル面で最大の転換点だった。コンポーネントのコードは React そのものだ——useState、useEffect、カスタム hook、JSX、コンポーネントの合成、条件付きレンダリング、データ取得、ローディングとエラー状態の管理。これらはすべてそのまま持ち込めた。
基本コンポーネントは変わった:View が div に、Text が p に、Pressable が button に。だがメンタルモデルに戸惑いはなかった。useUVIndex.ts や useSunTracking.ts の書き方は、Web アプリの hook を書くときとまったく同じだ。データを取得し、状態を管理し、値をコンポーネントに返す。レイアウトモデルも見慣れたもの——flexbox だ。
だからといって、Web のすべてが完璧に移行できるわけではない。ただ、React の核心的な直感は、思っていたよりずっと多くがそのまま持ち込めた。
実際の感触はこうだ:
最後の行には脚注を付けたい。EAS はフローを簡素化するが、App Store と TestFlight には依然として Apple 固有の審査、コード署名、メタデータ、アカウント要件が絡む。これらに Web で真正面から対応するものはない。
この表が、なぜこれがハードルが低いと感じられたかを説明している。ゼロから始めたのではなく、新しいランタイムの境界を探っていたのだ。
自動では移行できなかった部分
ネイティブが本当に違って見えるのは、その外側の層だ。コンポーネントモデルでも、React の部分でもなく、ネイティブの縁の部分。
たとえば:
-
権限ダイアログ
-
デバイス API
-
ビルド時の設定
-
ネイティブモジュール
-
シミュレータの状態
-
開発ビルド
-
コード署名
-
App Store の制約
-
実機でのテスト
Expo と Expo Skills がなければ、自分はここで回り道をしていた。
たとえば、ネイティブの位置情報は navigator.geolocation ではない。expo-location を使う場合、まずフォアグラウンド権限をリクエストし、結果を確認し、拒否された場合を適切に処理してから、デバイスの位置を取得する。
ロジックの形は見慣れたものだが、プラットフォームの期待は違う。コードが難しいのではない。正しい順序を知らなければならないのだ。
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
// Handle denial properly
return;
}
const location = await Location.getCurrentPositionAsync({});
設定も同じだ。Web では app.json のようなものをほとんど意識しない。Expo ではこのファイルが重要になる。ライブラリによっては JS のインポートだけでは済まない——ネイティブプロジェクトの設定も変更する。expo-location には iOS の用途説明文字列が必要で、それがないとシステムは権限ダイアログに何を表示すべきか分からない。expo-notifications はアイコンと通知チャンネルを登録する。expo-router はネイティブのナビゲーションスタックを接続する。
自分のアプリにはこういう plugin 設定が必要だった:
"plugins": [
"expo-router",
"expo-location",
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png"
}
]
]
まさにここで Expo 固有のコンテキストが役に立つ。それがなければ、まったく筋の通った React コードを書いたあと、自分のコンポーネントとは何の関係もないネイティブの挙動をデバッグして時間を溶かしていただろう。
追加したネイティブ機能
アプリの構造ができたあと、面白いのは本当にネイティブらしいものを足す部分だ。
expo-location でスマホの GPS 座標を取得し、それを Open-Meteo API に渡して現在の UV 指数を取得する——API key もバックエンドも不要で、デバイスの位置と公開天気 API だけで済む。これだけでこのアプリは普通の web プロジェクトとは別物になった。ユーザーに都市名を入力させるのではなく、デバイスがどこにあるかを最初から知っている。
次に触覚フィードバックを追加した。最小の機能だが、効果は最大だった。expo-haptics を 2 つの場面で使っている:
-
日光浴を開始したときの軽いタップフィードバック
-
100% に達したときの成功フィードバック
コードは短い:
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
そして:
await Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success
);
だが効果は即座だった。
タップ。ポン。ブルッ。
React のページが、アプリのふりをした何かではなく、最初からスマホに属していたもののように感じられた瞬間だった。
Web 開発者はたいてい触覚フィードバックを考えない。Web には、わざわざ設計する価値のある対応物が存在しないからだ。ネイティブでは、2 行のコードでインタラクション全体の手触りが変わる。
毎日のローカル通知には expo-notifications を使った。文言はわざと少し変にしてある——デバイス上でスケジュールするのであって、リモートプッシュではない。(リモートプッシュには APNs の証明書と有料の Apple Developer Program が必要で、このアプリはどちらも要らない。)
Sunny が外で待ってるよ。たぶんね。
マスコットの性格によく合っている。

Sunny は @shopify/react-native-skia で作り、アニメーションは react-native-reanimated で動かしている——跳ねる、まばたきする、感情の状態が切り替わる。Framer Motion を使ったことがあれば、このメンタルモデルは馴染みがあるはずだし、思っていたよりずっと滑らかに動く。「デモには十分」な滑らかさではなく、本当に滑らかだ。
毎日の進捗は @react-native-async-storage/async-storage に保存している。非同期版の localStorage といった使い心地だ。フォントは expo-font で Nunito を読み込み、タブのアイコンには @expo/vector-icons を使った。どれもゼロからモバイル開発を学んでいる感覚にはならず、むしろ React で、Web では普段届かないデバイス機能の部分に手を伸ばしている感覚だった。
実際に詰まったところ
思ったよりすんなり動いたものもあれば、壊れたものもあった。
Web での Skia クラッシュ
開発中は、アプリをネイティブと Web の両方で動かしたいと思っていた。Sunny のネイティブ版は Skia を使っているが、Web では Skia が CanvasKit と WebAssembly に依存する。CanvasKit の準備ができていないうちに Skia.Path.Make() を呼んでしまい、Web ビルドが即座に落ちた。エラーを見ればバグの内容はすぐ分かった。分からなかったのは、React Native で慣習的にどう直すかだ。
答えはプラットフォームごとにファイルを分けることだった:
components/
SunBuddy.native.tsx # full Skia implementation
SunBuddy.web.tsx # SVG fallback
Metro が iOS では .native.tsx を、Web では .web.tsx を自動で解決してくれる。
このパターンは自分にとって新しいものだった。
この規約は筋肉記憶になるまで練習する価値がある。実行時に分岐を書くのではなく、Metro にバンドル時に正しいファイルを選ばせる。そうすればプラットフォーム固有のコードが間違ったターゲットに混入することがない。
Web なら、実行時のチェックか遅延読み込みで済ませただろう。React Native ではプラットフォームごとのファイル分割が一級の規約だ。デバッグの勘はそのまま持ち込めるが、ネイティブのパターンの知識はそうとは限らない。そこを Expo Skills が埋めてくれた。
Metro バンドラのキャッシュが古いまま
react-native-reanimated を追加してシミュレータを開いたら、アプリが落ちた。エラーメッセージは役に立たず、15 分ほど自分の設定ミスだと思い込んでいた。本当の対処は Metro のキャッシュ削除だった:
npx expo start --clear
古いバンドルが配信され続けていて、Reanimated の Babel プラグインが正しく登録されていなかった。
Expo や React Native のプロジェクトで、Babel プラグインを持つライブラリを追加したあとにシミュレータの挙動がおかしくなったら、これが定番の「まずこれを試せ」だ。筋肉記憶に入れておく価値がある。
Web 開発では馴染みのない問題だった。Vite はたいてい自分で再起動してリロードして、あとは邪魔をしない。ネイティブ開発はエディタの外にもっと状態を持っている。
気に入らないが、一度踏んで理解してしまえば「ああ、もう分かった」で終わる類のものだ。
夜のバグ
これが一番のお気に入りだ。技術的な問題ですらないから。夜 9 時、外はもう暗い。Sun Buddy をテストしていて「start sun session」を押した。Sunny が跳ねた。進捗が伸びた。アプリは、僕が浴びられない日光を嬉しそうに記録していた。
面白いのは、アプリが設計どおりに完全に動いていたことだ——日照時間を記録し、進捗を積み上げる日焼けトラッカーとして。見落としていたプロダクトのロジックは、実際に使ってみて初めて当たり前に見えてくる。日焼けトラッカーなら、まず太陽がそこにあるかどうかを見るべきだろう。これは AI の失敗ではなく、自分のプロダクト思考の問題だ。
ツールはあなたの意図を実行する。意図が正しいことを確かめるのはあなたの仕事だ。
そこで GPS 座標から日の出・日の入りを計算するようにした。夜になると Sunny は眠りにつく。半分閉じた目、小さな「zzz」、だらりと下がった腕、そして無効化されたボタンにはこう書いてある:
Sunny は眠っています。日の出に会おう。

このバグひとつでアプリはずっと良くなったし、AI でものを作っても、自分で作ったものを実際に使う手間は省けないと改めて思い知らされた。
コード署名
最も「ネイティブはやっぱりネイティブ」だと感じた瞬間はコード署名だった。EAS ビルドで、プッシュ通知の capability が無料の Apple Developer アカウントの署名で引っかかった。リモートプッシュは有料の Developer Program の機能だからだ。これは Expo の問題ではなく、Apple がずっとそういうものだ。
Expo の設定でプッシュ通知の capability を外した。Xcode を探し回るのではなく。Expo のネイティブなやり方は、この変更を app.json に書いて、次のビルド時に Continuous Native Generation(CNG)にネイティブプロジェクトを再生成させる——config plugin でも expo-build-properties でもいい——そうすれば変更は Xcode の UI の状態ではなくソースコードに残る。ローカル通知はプッシュ capability を必要としないので引き続き使えるし、ユーザーから見た挙動も何も変わらない。
ただ、何が起きているのかはちゃんと把握する必要があった。
これはいいリマインダーだ。Expo はネイティブの道にある多くの落とし穴をならしてくれるが、プラットフォームの現実までは消せない。Apple には依然として証明書、capability、アカウントの種類、ルールがある。
Apple の制約はいくつかの段階に分かれていて、手を動かす前にどの段差にいるのかを把握しておく価値がある。無料の Apple ID でも自分のデバイスでビルドは動かせる(署名は7日間有効)。TestFlight と App Store 配布には有料の Apple Developer Program が要る。一部の capability——プッシュ通知、アプリ内課金、Sign in with Apple——は開発段階でも有料アカウントが必要だ。
TestFlight や App Store 配布をやるなら、Apple Developer Program に入るしかない。ここは避けて通れない。
まずは EAS を勧める
ローカルの iOS 環境も組んでみた。裏で何が起きているのか知りたかったからだ。でも、別の React Web 開発者にどう始めればいいか聞かれたら、早めに EAS を見ろと言う。
基本的な流れはこうだ:
eas build:configure
eas build --platform ios
eas submit --platform ios
どの profile を使うかは何をやっているかで決まる。development は dev client 用、preview は少人数のテスターへの内部配布用、production は App Store / TestFlight 向けだ。Profile は eas.json に書いて、そこで管理する。
もう一点。eas submit はバイナリを App Store Connect にアップロードし、処理が終わると TestFlight で見えるようになる。本番にアプリをリリースするわけではない。App Store Connect のメタデータ、スクリーンショット、プライバシー質問票を用意し、ビルドを選んで、App Review に提出する必要は依然としてある。
こうして得られるのはクラウドビルド、クレデンシャル管理、そして TestFlight と App Store 提出へのよりクリーンな道だ。最初の正式なビルドでローカルマシンに発生する面倒の多くがなくなる。
Expo Skills もここでは役に立った。デプロイ周りのアドバイスはすぐ古くなるからだ。ビルド profile、クレデンシャル、提出フロー、内部配布、TestFlight——まさに最新の Expo 固有のコンテキストが最も必要な領域だ。
ローカル環境も試した。Xcode、CocoaPods、デバイスの信頼、署名。1時間ほどかかった。楽とは言わないが、頭の中で膨らませていたような2週間の悪夢ではなかった。
試すリスクは見た目より小さい。
最初から iOS 開発者になって、ツールを全部揃えて、App Store に出すと決める必要はない。抵抗が最も少ない第一歩は、npx create-expo-app で Expo Go を使い iOS Simulator で開き、ネイティブの capability を1つつなぐことだ(位置情報かハプティクスがいい選択だ)。一晩で回せるループだ。Expo Go に組み込まれていないライブラリ——Skia、Reanimated、カスタムネイティブモジュール——を使う段になって、development build に移ればいい。
そこまで行けば、EAS が実際のビルドと配布への道をくれる。行かなければ、週末を1つ使って、自分の React スキルが別の領域にどう対応するかを確かめただけだ。
ネイティブ開発環境は本当に面倒だ。でも、だからといって永遠にネイティブを避け続ける理由にはならない。
あの瞬間
これを理論の話ではなくしたのは、自分の iPhone で Sun Buddy を動かした瞬間だ。Sunny が飛び出した。Skia のプログレスリングが描画された。UV バッジには実際の位置から取った実際の値が表示されていた。開始ボタンを押すと振動を感じ、もう一度押し、さらに何度か押した。このループの手触りが良すぎたからだ。
タップ。アニメーション。振動。進捗。 ブラウザのタブでも、レスポンシブな Web ビューでも、アプリのふりをしたプロトタイプでもなく、自分の iPhone に実在するものだ。React で書かれている。

ホーム画面にある。自分がどこにいるか分かっている。タッチへの反応は、Web が普通は出せないものだ。
その瞬間から、ネイティブはまったく別のキャリアパスという感じではなくなり、自分の既存スキルを活かせるもう一つの場所という感じになってきた。
別の React Web 開発者に言いたいこと
スキルは移せる。これが一番大事な点だ。コンポーネントモデル、hooks、デバッグの勘——全部ついてくる。ゼロから始めるわけではない。
ただネイティブには角があって、必ずぶつかる。権限フロー。古くなった Metro bundle。config plugin。署名 capability。おそらく、これだからネイティブアプリはやらないんだ と思う瞬間が来る。自分にとっては、そういう瞬間はだいたい --clear のフラグ1つか設定項目1つのところで解決する。
AI が仕事を奪ったわけではない。このアプリが何をすべきかは、今も自分で決めなければならない。夜の 9 時になっても日照トラッカーが元気に日光を追いかけている——そんな馬鹿げたことに気づくのも自分だ。プロダクトを考えるのも自分。判断するのも自分。
React を書いていて、ネイティブをずっと避けてきたなら、いきなりモバイルの専門家になろうとしなくていい。Web では得られないものから始めればいい。位置情報、振動、カメラ、通知、ジェスチャー。
それを中心に小さなものを作る。実機で動かす。
それが独立した世界のままなのか、それとも自分の既存スキルのもう一つの活躍の場になるのかは、すぐにわかる。
Sun Buddy は Expo、Claude Code、Expo Skills で作られている。React Web 開発者でネイティブへの移行を考えているなら、ここから始めるといい:Expo for React web developers。