Windows向けExpo, ReactNative環境構築（スマホアプリ開発）

# モチベーション
windowsPCでexpoの環境構築を行っていた際に、色々な沼にはまってしまった。
そのため、環境構築の手順や詰まった際の対処方法を共有したい。

# 概要
[expo](https://docs.expo.dev/)は[ReactNative](https://reactnative.dev/docs/getting-started)と組み合わせることで、android・iosを問わずにネイティブアプリを開発できるありがたいOSSです。
Reactの記法を基にTypeScript(JavaScript)でアプリを開発できるので、Webアプリ開発でReactを触っていた方にとってはかなり敷居が低いと思います。（色々異なる点はありますが）

今回はそんなexpoの環境構築に関して、通常の環境構築とdevContainerを用いた環境構築を紹介していきます。

# 通常の環境構築
操作自体は[expoのドキュメント](https://docs.expo.dev/)に沿った内容となります。

- 使用した環境
    - PC
        - Windows 11
        - node 22.14.0
        - npm 10.9.0
        - コードエディタ vscode
    - iPhone 14
    

## 1. expoプロジェクトの作成
:::note warn
**通常の環境構築時は、プロジェクトをOneDriveやDropboxなど自動同期系ソフトが管理するディレクトリには<INS>作成しない</INS>ことが推奨されます**
nodemodulesが同期されること自体あまり嬉しくないことや、同期中にファイルをロック？されることでexpoコマンドが正しく動作しない場合があります。
:::

:::note info
2022年頃まではexpo-cliのグローバルインストールが必要でしたが、expoのアップデートに伴いexpoモジュールに内包されたcliで操作が完結するようになりました
[The New Expo CLI](https://blog.expo.dev/the-new-expo-cli-f4250d8e3421)
:::

プロジェクトを作成したいディレクトリで`npx create-expo-app@latest`または`npx create-expo-app@latest プロジェクト名`を実行します。
```
PS C:\パス> npx create-expo-app@latest
Need to install the following packages:
create-expo-app@3.2.0
Ok to proceed? (y)

Creating an Expo project using the default template.

To choose from all available templates pass in the --template arg:
  $ npx create-expo-app --template

To choose from all available examples pass in the --example arg:
  $ npx create-expo-app --example

√ What is your app named? ... sample-app
√ Downloaded and extracted project files.
> npm install
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated @babel/plugin-proposal-class-properties@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
npm warn deprecated @babel/plugin-proposal-nullish-coalescing-operator@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated @babel/plugin-proposal-optional-chaining@7.21.0: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
npm warn deprecated abab@2.0.6: Use your platform's native atob() and btoa() methods instead
npm warn deprecated domexception@4.0.0: Use your platform's native DOMException instead
npm warn deprecated rimraf@2.6.3: Rimraf versions prior to v4 are no longer supported
npm warn deprecated sudo-prompt@9.1.1: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
npm warn deprecated @xmldom/xmldom@0.7.13: this version is no longer supported, please update to at least 0.8.*
npm warn deprecated sudo-prompt@8.2.5: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

added 1125 packages, and audited 1126 packages in 3m

94 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

✅ Your project is ready!

To run your project, navigate to the directory and run one of the following npm commands.
```
実行が完了すると、必要なnodemodulesがインストールされた状態でexpoのプロジェクトディレクトリが作成されます。

## 2. スマホでExpo Goのインストール
[AppStore](https://apps.apple.com/jp/app/expo-go/id982107779)からExpo Goをインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3880312/82676db2-46fb-46be-a427-e3e1a6523427.png)

## 3. スマホとPCのネットワーク設定
expoで書いたコードをスマホで動作確認するためには、**スマホとPCが同じWi-Fi (プライベートなネットワーク)に属している** ことが必要になります。
expoは動作確認用のコマンド実行時に、ホストPCのIPv4アドレスを使用してアプリを渡すためのサーバを立てるため、同じネットワーク空間にいない場合にはExpo Goからサーバに接続することができずにタイムアウトしてしまいます。

:::note info
expoでは[Metro bundler](https://docs.expo.dev/guides/why-metro/)を使用して、TS(JS)のバンドル＋（おそらく）バンドルファイルを渡すためのサーバを起動します。
スマホ側はサーバのURL（ホスト、ポート）からアクセスし、Expo Go上でバンドルファイルをダウンロード、実行することでアプリを使用することができます。
:::


## 4. 実機でのアプリ動作確認
プロジェクトの階層で`npm start`を実行することでターミナル上にQRコードが表示されます。
表示されたQRコードをスマホで読み込むことで、Expo Goが自動で開きアプリの動作を確認することができます。
```
root ➜ /workspace/app $ npm start

> app@1.0.0 start
> expo start

Starting project at /workspace/app
Starting Metro Bundler
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█ ▄▄▄▄▄ █▄▀ ▀ ▄ █▀█ ▄▄▄▄▄ █
█ █   █ █   █▀ ▄▀▀█ █   █ █
█ █▄▄▄█ █▄█▀ ▄▄█▄▀█ █▄▄▄█ █
█▄▄▄▄▄▄▄█▄█ █ █ ▀ █▄▄▄▄▄▄▄█
█   ▄█▀▄▀▄█ ▄ ▀▀▄▄▄██▄ ▄▀▄█
███ █▀ ▄█   ▄▄▀▄█▀█ ▀██▀███
█▄█▀ █ ▄▀▀▄▄▀ ▄█ ▄█ ▄ █ █▀█
█▀▄█▀  ▄█  ▄ █▄██ ▀▀█▀▀█ ▀█
████▄█▄▄▄▀███▄▄ ▄ ▄▄▄ ▀▄█▀█
█ ▄▄▄▄▄ ███ ▀██▀█ █▄█ █▄▄ █
█ █   █ █▀  ███▀▀▄▄   █▀ ▀█
█ █▄▄▄█ █  ▄▀▀▀ ▀▄▀▄█▄▄ ▄██
█▄▄▄▄▄▄▄█▄█▄▄▄██▄██████▄▄▄█

› Metro waiting on exp://192.168.0.59:8081
› Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

› Web is waiting on http://localhost:8081

› Using Expo Go
› Press s │ switch to development build

› Press a │ open Android
› Press w │ open web

› Press j │ open debugger
› Press r │ reload app
› Press m │ toggle menu
› shift+m │ more tools
› Press o │ open project code in your editor

› Press ? │ show all commands

Logs for your project will appear below. Press Ctrl+C to exit.
```


先ほどターミナル上に表示されたQRコードは、
「› Metro waiting on exp://192.168.0.59:8081」の行にある「exp://192.168.0.59:8081」というURIを示しており、これがバンドルファイルを受け取るエンドポイントになります。
そのため、QRコードを読み込まなくてもSafariなどのブラウザで「exp://192.168.0.59:8081」を直打ちして実行することも可能です。


# devContainerを用いた環境構築
- 使用した環境
    - PC
        - Windows 11
        - DockerDesktop
        - コードエディタ vscode
            - 拡張機能 Dev Containers
    - iPhone 14


## 1. devContainerの定義作成
プロジェクト用にディレクトリを準備し、中に.devcontainer/dencontainer.jsonを作成します。
ホストPCのIPアドレスに関しては、[3. スマホとPCのネットワーク設定](#3.-スマホとPCのネットワーク設定)を実施した後のアドレスを使用してください。

```json:プロジェクト用ディレクトリ/.devcontainer/devcontainer.json
{
  "name": "Node.js & TypeScript",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
  "runArgs": [
    "--name=EXPOAPP_SAMPLE",
    "--rm"
  ],
  "appPort": ["ホストPCのIPアドレス:8081:8081"],
  "containerEnv": {
    "REACT_NATIVE_PACKAGER_HOSTNAME": "ホストPCのIPアドレス"
  },

  "remoteUser": "root",
  "workspaceFolder": "/workspace",
  "mounts": [
    "source=${localWorkspaceFolderBasename},target=${containerWorkspaceFolder},type=volume"
  ],
  "customizations": {
    "vscode": {
      "extensions": [],
      "settings": {
        "editor.tabSize": 2,
        "files.trimTrailingWhitespace": true,
        "files.insertFinalNewline": true
      }
    }
  }
}
```
devcontainer.jsonの記述内容に関して重要な要素を以下にまとめました。
ポイントは、スマホからQRコードを読み込んでexpoに接続する流れを 
**「スマホ⇒ホストPCのIPアドレス:8081⇒ポートフォワードでコンテナのIPアドレス:8081⇒expoが起動したサーバ」** 
にすることです。

### devcontainer.jsonでのポイント
#### 1. addPort
ホストPCとコンテナのポート紐付け
**expoは8081ポートで起動するため、ホストPCからコンテナの8081にポートフォワードする必要があります。** また、npm start時に表示されるQRコード（URL）においても8081が使用されており、スマホで読み込んだ際にはホストPCの8081に接続しにいきます。そのため、ホストとコンテナともに8081に指定することを推奨します。

#### 2. containerEnv
コンテナ内の環境変数。
通常devcontainerの場合はコンテナのIPアドレスを使用してexpoがQRコードを作成するため、スマホから読み込んでもアドレスを解決できず接続できません。しかし、環境変数REACT_NATIVE_PACKAGER_HOSTNAMEにホストPCのIPアドレスを設定することで、expoがホストPCのIPアドレスを使用してQRコードを作成します。  <br/>

:::note info 
**addPortとcontainerEnvの設定内容の補足** 
コンテナへのポートフォワード以外はQRコードから接続するための設定なので、設定しなくてもスマホのブラウザから直打ちでホストPCのアドレス:ポートに接続すれば動作はします
:::

#### 3. mounts
ファイルストレージのマウント設定。
bindの場合、ホストPCのファイルストレージの指定領域をコンテナ内のファイルストレージにマウントします。
volumeの場合、Dockerが管理する指定領域をコンテナ内のファイルストレージにマウントします。volume名には半角英数＋一部記号`[a-zA-Z0-9][a-zA-Z0-9_.-]`のみ使用できます。
今回コンテナのワークスペース全体をvolumeマウントしているのは、OneDriveの同期領域から外すため＋ホットリロードを有効にするためです。

:::note info 
**OneDriveの同期領域から外せる理由**
windowsOSでDockerDesktopを使用する場合は、自動的にWSLによる仮想Linux環境上でDockerがインストールされ、WSLのファイルストレージ[(確認方法)](https://qiita.com/quzq/items/1096c638c0d86795be13)のDocker管理領域でvolumeが保持されます。
そのため、OneDriveの管理領域でdevcontainerを定義した場合でも、volumeマウントを使用した場合には実際のコードやnodemodulesがwsl上に存在しているため同期から外すことができます。
:::

:::note info 
**ホットリロード**
WSL＋Dockerでwindowsのファイルをコンテナにbindマウントした場合、ファイル変更が検知できないらしい。

参考：[Dockerを使っていてhot reload系が効かなかったとき](https://zenn.dev/motoishimotoi/articles/8adedc934334cc)、[WSL+DockerでHot reload が動作しない](https://banatech.net/blog/view/54)
> [Docker Desktop on WSL2: The Problem with Mixing File Systems | by Manfred Lange | Level Up Coding](https://levelup.gitconnected.com/docker-desktop-on-wsl2-the-problem-with-mixing-file-systems-a8b5dcd79b22)
上記の記事によるとファイル変更を検知するinotifyという機能がWindowsのファイルシステムとLinuxのファイルシステムでまたがっていると機能しないためHot reload機能も動作しないようです．

個人的にはnode_modules以外はbindマウントしたかったのですが、
参考元にあった解決方法である`CHOKIDAR_USEPOLLING=1`または`CHOKIDAR_USEPOLLING=true`の指定、Nextjsで開発した際に効いた`WATCHPACK_POLLING=true`の指定を試してもホットリロードが動かず、、、、
そのため、今回は全てをvolumeマウントして解決しました。
:::

## 2. devContainerの起動
vscodeの左下にある「><」のマークを押し、`Reopen in container`を選択することでdevcontainerが起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3880312/9f9ab93c-4b1e-4752-98dc-f8eb8f61c151.png)

vscodeの画面が切り替わり、左下の「><」のマークのあたりにDevContainerと表示されていれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3880312/ad3cb05a-6044-43f5-95d9-094277f1c13a.png)

## 3. プロジェクトの作成⇒実機での動作確認
[通常の環境構築](#通常の環境構築)の手順に沿って実施します。(3. スマホとPCのネットワーク設定 は実施済みのため飛ばして問題ありません)

# まとめ
devcontainerは便利。