📱

Web系エンジニアの私が、初めてExpo × React Nativeに触れた記録

に公開

本記事のサマリ

React と Next.js の経験しかなかった私が、Expo を使って React Native アプリの開発環境を構築し、実際に動かしながらコードを読み解いた記録です。Web 開発との違いや共通点、つまずきそうなポイントを実例ベースでまとめました。

今回のコードは下記のリポジトリにあります。
https://github.com/toto-inu/lab-202510-expo-test/tree/after-init

なぜ今 React Native なのか

正直に言うと、これまでアプリ開発は「Web開発とは全く違う世界」と勝手に思い込んでおり、完全に食わず嫌いしていました。Swift や Kotlin といった言語を新たに学ぶ必要があるのでは、開発環境の構築も大変そう、といった漠然とした先入観です。

ただ、今後個人開発などもやっていく上で、モバイルアプリは避けて通れない領域だと感じていました。そこで今回、満を持してチャレンジすることにしました。実際に触ってみると、思った以上にWeb開発の知識が活かせる部分と、全く違う考え方が必要な部分があることがわかり、その発見がなかなか面白かったのです。

今回は Expo を使って環境構築から実際のコード理解まで、一連の流れを記録として残しておこうと思います。

実際の環境構築手順

1. Expo CLI でプロジェクト作成

まずは公式ドキュメント通りにプロジェクトを作成しました。

https://docs.expo.dev/

npx create-expo-app@latest

プロジェクト名を聞かれるので適当に答えると、必要なファイルが生成されます。Web開発と同じく package.jsonnode_modules が作られるので、この時点では「いつものNode.js環境だな」という印象でした。

まずはブラウザで動作確認してみます。

npm run web

localhost:8081 でアプリが立ち上がり、ブラウザ上でReact Nativeアプリが動くのを確認できました。これは予想以上に簡単で、Web開発者にとって最初のハードルが低いのは嬉しいポイントですね。

Webブラウザでの起動画面

ブラウザで開いた初期画面。localhost:8081で起動し、通常のReactアプリと同じように確認できる

2. iOSシミュレーター環境の準備

次にiOSシミュレーターで動かすための準備をしました。Xcodeのアップデートとプラットフォームのインストールが必要とのことで、こちらの記事を参考にしました。

https://qiita.com/kawabata324/items/b5f060503c82b8cab825

Xcodeのアップデートは時間がかかりましたが、完了後にシミュレーターが立ち上がることを確認。この時点で「本格的にモバイル開発に足を突っ込んだな」という実感が湧いてきました🔥

Xcodeからシミュレーターを起動

Xcodeの「Open Developer Tool」から「Simulator」を選択してiOSシミュレーターを起動

3. 開発サーバーの起動と各プラットフォームでの確認

https://docs.expo.dev/get-started/start-developing

npm run start

このコマンドを実行すると、開発サーバーが立ち上がってターミナルにメニューが表示されます。

開発サーバーのターミナル画面

npm run start で起動したターミナル。QRコードとショートカットキーが表示される

ここで i キーを押すとiOSシミュレーターでアプリが起動しました。

iOSシミュレーターでの起動画面

iOSシミュレーターで起動した初期画面。開発メニューの使い方も表示される

また、プロジェクトをクリーンな状態から始めたかったので、サンプルコードをリセット。

npm run reset-project

これでサンプルコードが削除され、真っ白な状態からスタートできます。

Web開発との共通点!⛓️

開発ワークフローは意外と慣れ親しんだもの

基本的なコマンド体系はWeb開発とそれほど変わりません。

npm start     # 開発サーバー起動
npm run ios   # iOSシミュレーター
npm run android # Androidエミュレーター  
npm run web   # ブラウザ
npm run lint  # ESLint実行

開発サーバー起動後のショートカットキーも直感的です。

  • i - iOSで開く
  • a - Androidで開く
  • w - Webで開く
  • r - リロード
  • m - 開発メニューを開く

このあたりはNext.jsなどのWeb開発環境と似ていて、すんなり馴染めました。
(同じくnodeで動いてるのでそりゃそうですね)

実際にアプリを動かしながら開発する際は、開発メニュー(cmd + dでも開ける)が非常に便利です。

開発メニュー画面

開発メニューからは、リロード、パフォーマンスモニター、Element Inspector、JSデバッガーなどにアクセスできる

DOM要素が使えない衝撃

Web開発者が最初に戸惑うのは、おそらくここです。

// Web (React/Next.js)
<div className="container">
  <h1>タイトル</h1>
  <p>テキスト</p>
  <a href="/about">リンク</a>
</div>

// React Native
<View style={styles.container}>
  <Text style={styles.title}>タイトル</Text>
  <Text>テキスト</Text>
  <Link href="/about">リンク</Link>
</View>

<div><p> といった馴染みのある要素が使えず、代わりに <View><Text> を使います。最初は「なぜこんな面倒なことを」と思ったのですが、よく考えてみると、モバイルアプリには <div> に相当するネイティブ要素が存在しないわけで、これは必然的な違いなのだと理解しました。

スタイリングの考え方が根本的に違う

CSSクラスが使えないのも大きな違いです。

// Web
.container {
  display: flex;
  padding: 20px;
  background-color: #fff;
}

// React Native
const styles = StyleSheet.create({
  container: {
    display: 'flex',     // デフォルトでflex
    padding: 20,         // 単位不要
    backgroundColor: '#fff', // キャメルケース
  }
});

StyleSheet.create() を使ったオブジェクト形式でのスタイル定義に最初は戸惑いましたが、TypeScriptの型補完が効くため、慣れてしまえばむしろ書きやすいかもしれません。

実際の使い方はこんな感じです。

<ThemedView style={[styles.container, { backgroundColor }]}>
  <ThemedView style={styles.header}>
    <ThemedText type="title">Todo リスト</ThemedText>
  </ThemedView>
</ThemedView>

定義した styles.containerstyle propsに渡すわけですが、これは冷静に考えると、Webで className="container" とクラス名を指定するのと本質的には似ています。書き方が違うだけで、「あらかじめ定義したスタイルを参照する」という構造は同じなので、思ったより違和感は少なかったです。

興味深いのは、React Nativeではすべての要素がデフォルトでFlexboxになっていることです。ただし、デフォルトが flexDirection: 'column' なので、横並びにしたい場合は明示的に flexDirection: 'row' を指定する必要があります。

ルーティングはNext.jsそのもの

一方で、Expo Routerのファイルベースルーティングは、Next.jsのApp Routerと非常に似ていました。

app/
├── _layout.tsx      # ルートレイアウト
├── (tabs)/          # タブグループ
│   ├── _layout.tsx  # タブナビゲーション
│   ├── index.tsx    # Home画面
│   └── explore.tsx  # Explore画面
└── modal.tsx        # モーダル画面

(tabs) のようなグループ化、_layout.tsx でのレイアウト定義、index.tsx でのページ作成など、Next.jsユーザーにはお馴染みの構造です。

import { Link } from 'expo-router';

<Link href="/modal">モーダルを開く</Link>

Linkコンポーネントの使い方もほぼ同じで、ここはすんなり理解できました。

コンポーネント設計で学んだこと

Propsの型定義は基本的に同じ

React Nativeでも、Webでの型定義パターンがそのまま使えます。

import { type TextProps } from 'react-native';

export type ThemedTextProps = TextProps & {
  lightColor?: string;
  darkColor?: string;
  type?: 'default' | 'title' | 'defaultSemiBold';
};

export function ThemedText({
  style,
  type = 'default',
  ...rest
}: ThemedTextProps) {
  // ...
}

既存の TextProps を拡張して独自のpropsを追加するパターンは、Webでのコンポーネント設計と同じ考え方です。

スタイルの合成パターン

面白いのは、スタイルを配列で合成できることです。

<Text style={[
  { color },           // 基本スタイル
  styles.default,      // 型ごとのスタイル
  style,              // propsで渡されたスタイル(最優先)
]}>

後ろの要素が優先されるので、上書きしたいスタイルを後から指定できます。これはCSS-in-JSライブラリのスタイル合成に似ていて、慣れるとかなり使いやすいパターンでした。

プラットフォーム固有の処理

Web開発にはない概念として、プラットフォームごとに異なる処理を書く必要があります。

import { Platform } from 'react-native';

<Text>
  Press{' '}
  <Text>
    {Platform.select({
      ios: 'cmd + d',
      android: 'cmd + m', 
      web: 'F12',
    })}
  </Text>
</Text>

また、環境変数でのチェックも可能です。

onPressIn={(ev) => {
  if (process.env.EXPO_OS === 'ios') {
    // iOSのみ触覚フィードバック
    Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
  }
  props.onPressIn?.(ev);
}}

この「プラットフォーム分岐」の考え方は、レスポンシブデザインでの画面サイズ分岐に似ているものの、より根本的な違いを扱う点で新鮮でした。

アニメーションとネイティブ機能

react-native-reanimatedの威力

Web開発でCSS animationやframer-motionを使っていた身としては、react-native-reanimatedは全く新しい世界でした。

import Animated, {
  interpolate,
  useAnimatedStyle,
  useScrollOffset,
} from 'react-native-reanimated';

const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(scrollRef);

const headerAnimatedStyle = useAnimatedStyle(() => {
  return {
    transform: [
      {
        translateY: interpolate(
          scrollOffset.value,
          [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
          [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
        ),
      },
    ],
  };
});

スクロールに連動したパララックス効果を実装する際、useScrollOffset でスクロール位置を取得し、interpolate で値をマッピングするというパターンは、最初は理解に時間がかかりましたが、慣れると非常に強力そうですね。

ネイティブ機能へのアクセス🔥

これこそWebとの大きな違いだと思うのですが...
Expoの魅力の一つは、ネイティブ機能へ簡単にアクセスできることです!

// 触覚フィードバック
import * as Haptics from 'expo-haptics';
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);

// アプリ内ブラウザ
import { openBrowserAsync } from 'expo-web-browser';
await openBrowserAsync('https://expo.dev');

Web開発では当然できない「デバイスを振動させる」「アプリ内でブラウザを開く」といった機能が、数行のコードで実現できるのは驚きでした。

実際の学習で感じた「Web開発の知識が活かせる部分」

環境構築から実際にコードを動かすまでの過程で強く感じたのは、Node.js環境での開発という基盤は共通しているということです。

  • ESLintやPrettierによるコード品質管理
  • カスタムフックによるロジック分離
  • TypeScriptによる型安全性
  • Jestによるテスト実行

これらの開発手法やツールチェインは、React Nativeでもそのまま使えます。新しい技術を学ぶ際の心理的ハードルが下がるのは、Web開発経験者にとって大きなアドバンテージだと思います。

また、Reactのコンポーネント設計やHooksの考え方も基本的には同じです。useStateuseEffect の使い方、コンポーネントの責任分離、propsの受け渡しといった設計思想は変わりません。

まとめと今後の展望

React Nativeに初めて触れてみて感じたのは、「思ったより敷居は高くない」ということです。確かにDOM要素が使えない、CSSが使えないといった違いはありますが、根本的な開発思想や環境は共通している部分が多く、Web開発の知識を活かしながら学習を進められました。

特に興味深かったのは、プラットフォームごとの差異を意識した設計の考え方です。Web開発でも画面サイズによる分岐はありますが、OSレベルでの違いを考慮する必要があるのは新たな視点でした。

今回は基本的な環境構築と簡単なコード読解までしか行いませんでしたが、次のステップとしては実際に何らかの機能を持ったアプリを作りながら、より深い部分を理解していきたいと思います。

Expo が提供するエコシステムの豊富さを考えると、Web開発者がモバイルアプリ開発に参入する選択肢として、かなり有力ではないでしょうか。少なくとも「とりあえず触ってみる」段階では、想像していたよりも大分スムーズに進められました!皆さんも是非触ってみてください!

株式会社StellarCreate | Tech blog📚

Discussion