Open25

create-t3-turbo

タナイタナイ

背景

  • 普段はNext.jsを中心にWebアプリケーションの開発をしている
  • 今回、はじめてネイティブアプリ開発に挑戦する
  • Reactつながりで多少は勝手が分かりそうなReact Nativeをとりあえず選定した
    • Reactに対するNext.js的な感じで、Expoってのを使うといろいろbattery-includeっぽい
  • サーバーサイドの型情報がリポジトリとして分離するのはつらい
    • モノレポでHono RPCやtRPCなどの仕組みを使いたい
      • turborepoとかも使ったことないし、セットアップを自前で調整するのはダルい
      • なんかcreate-t3-turboが想像してるようなことだいたい全部やってくれてそう
        • turborepoでtRPCとExpoを使用してくれてる(便利!)

セットアップ

https://github.com/t3-oss/create-t3-turbo

GitHubテンプレートのみでpnpm create t3-app@latestみたいなのができるnpmパッケージは存在しないっぽい

公式リポジトリから「Use this template」して使う。

ちなみに今まで曖昧な理解で使っていたが、以下のような関係らしい。

  • pnpx = pnpm dlx
  • pnpm dlx create-* = pnpm create *

ちゃんと公式ドキュメントを一度くらい読まないとダメね。
https://pnpm.io/ja/cli/create

タナイタナイ

npmパッケージは存在しないっぽい

めっちゃ嘘を書いてしまっていた。

npx create-turbo@latest -e https://github.com/t3-oss/create-t3-turboすればいけると公式READMEにわかりやすく書いてある。

タナイタナイ

DB変更

DB含め、APIはedge前提で作られているっぽい。
となるとDB接続はWebSocketベースになるが、ローカルDBに繋げないのは面倒臭い。

ということでNeonを使いつつローカルDBに繋げられるように変更。

pnpm rm @vercel/postgres
pnpm add @neondatabase/serverless ws
pnpm add -D @types/ws
drizzle.config.ts
import type { Config } from "drizzle-kit";

if (!process.env.POSTGRES_URL) {
  throw new Error("Missing POSTGRES_URL");
}
- 
- const nonPoolingUrl = process.env.POSTGRES_URL.replace(":6543", ":5432");

export default {
  schema: "./src/schema.ts",
  dialect: "postgresql",
-   dbCredentials: { url: nonPoolingUrl },
+   dbCredentials: { url: process.env.POSTGRES_URL },
  casing: "snake_case",
} satisfies Config;
packages/db/src/client.ts
import { neonConfig, Pool } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-serverless";
import { WebSocket } from "ws";

import * as schema from "./schema";

if (process.env.NODE_ENV === "production") {
  neonConfig.webSocketConstructor = WebSocket;
  neonConfig.poolQueryViaFetch = true;
} else {
  neonConfig.wsProxy = (host) => `${host}:5433/v1`;
  neonConfig.useSecureWebSocket = false;
  neonConfig.pipelineTLS = false;
  neonConfig.pipelineConnect = false;
}

const pool = new Pool({ connectionString: process.env.POSTGRES_URL });

export const db = drizzle({
  client: pool,
  schema,
  casing: "snake_case",
});
docker-compose.yml
services:
  postgres:
    image: 'postgres:latest'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - '5432:5432'
  pg_proxy:
    image: ghcr.io/neondatabase/wsproxy:latest
    environment:
      APPEND_PORT: 'postgres:5432'
      ALLOW_ADDR_REGEX: '.*'
      LOG_TRAFFIC: 'true'
    ports:
      - '5433:80'
    depends_on:
      - postgres
タナイタナイ

シミュレーターがちゃんと起動しない

プロジェクトのルートでpnpm devしてもコケる。

CommandError: No development build (your.bundle.identifier) for this project is installed.

apps/expo配下でpnpm iosすると何やらCocoaPodsとかのインストールが始まって起動できるようになった。

上記の操作が一体どこに何をして、どんな意味を持つのかは現時点では不明だが、とりあえず動いてそうなので今は作ることを優先する。

あと、your.bundle.identifierはどこで設定できるんだろう。まあデプロイ前に調べればいいや。

Expo Goで動かす方法は不明

sを押すとExpo Goモードにできるっぽいのだが、t3-turboのロゴが表示され続けるだけでアプリは起動しなかった。

こちらも理由は不明だが、後回し。

タナイタナイ
  • Expo Go
  • development build
  • managed workflow
  • bare workflow

などいろいろあるようだが、Expo Goだと限られたモジュールしか使えずproduction向きではないので、t3-turboでは初めからdevelopment buildが必要らしい。

development buildをすると(理解が正確かはわからないが)、自分専用のExpo Go環境みたいなのを都度生成するイメージっぽい。

さらにbare workflowではネイティブコードの入っている/iosフォルダの依存関係なりを自力で解決する必要も出てくるらしい。bare workflowでもEAS buildは使える?らしいのでExpoの抽象化の恩恵は一応あるらしい。

とりあえず、apps/expo配下でpnpm iosなりをすると、Expo Goでなくともsimulatorでホットリロードしながら開発できる環境を得られるという理解でいったん終える。

タナイタナイ

Post作成しようとしたらログインが必要だと怒られた

いったんバイパスに挑戦する。

タナイタナイ

Expoのディレクトリ構造を理解する

公式ドキュメントを読む前にまずはあたりをつける。

src/app/_layout.tsxを読むと<Stack />が現在の表示ページで、それをラップするようなレイアウト定義らしい。いま表示するページはどう決まるんだろうか。

ページの実体ぽいのはsrc/app/index.tsxっぽい。src/app/post/[id].tsxみたいなのもあるし、このあたりはNext.jsのようにファイルベースのルーティングが一定あるっぽい。

見た感じ<CeatePost />コンポーネントが上のエラー出してるフォームっぽい。

{error?.data?.code === "UNAUTHORIZED" && (
  <Text className="mt-2 text-destructive">
    You need to be logged in to create a post
  </Text>
)}

はいはい。
tRPCの対象エンドポイントはpackages/apiのほうに分離されている。いいね。

packages/api/src/router/post.ts
create: protectedProcedure
  .input(CreatePostSchema)
  .mutation(({ ctx, input }) => {
    return ctx.db.insert(Post).values(input);
  }),

問答無用でpublicProcedureに書き換えてちゃんとDBレコード作成されるか見る。

タナイタナイ

ちーん。

DB接続でコケた。

POST /api/trpc/post.create?batch=1 500 in 491ms
>>> tRPC Request from expo-react by undefined
[TRPC] post.create took 479ms to execute
>>> tRPC Error on 'post.create' error: client selected an invalid SASL authentication mechanism
    at eval (webpack-internal:///(rsc)/../../node_modules/@neondatabase/serverless/index.mjs:1359:74)
    ... 30 lines matching cause stack trace ...
タナイタナイ

packages/db/src/client.tsのコピペミスで指定すべきオプションが抜けてるだけだった。

無事にpost.createが通った。

タナイタナイ

Expo SDK パッケージの利用

expoにはネイティブ動作のラッパーがいろいろあるらしく、とりあえずexpo-background-fetchを使ってみようと思った。

/apps/expo上でExpo SDK パッケージをインストールする。

npx expo install expo-background-fetch
npx expo install expo-task-manager

このときpnpm add expo-*とはせずにnpx expo install expo-*とすることが推奨される。

expo内での互換性や依存性の解決をうまい感じにやってくれるらしい。

タナイタナイ

インストール周りの挙動がよくわからん。pnpm cleanだとかnpx expo installなどを何度かやり直すといきなり起動できるようになったりする。

とりあえず困ったらpnpm cleanでキャッシュ削除してやり直すことにする。

turborepo、xcode、react-native、expoなど登場人物が多く、なにがどこで影響を及ぼし合っているのか把握するのが大変だ。

タナイタナイ

コミットを巻き戻してもう一度順番にやってみたらnpx expo install --fixをしたタイミングで動くようになったっぽい。

あと、pnpm ios = npx expo run:iosの工程でExpo Development Buildが行われるらしい。

AIに簡単に聞いただけだが、Expo Development BuildはExpo Goの代替として登場したもので、本質的には自分専用のExpo Goを作るようなものらしい。開発ビルドを作成すると、そのアプリには指定したネイティブモジュール(Expo未対応のものも含め)を組み込めるため、Expo Goで動かなかった機能もテスト可能になる。具体的には、ローカルでnpx expo run:ios等を実行して開発モードのビルドを作る。

一度生成すれば、そのDevクライアントアプリ上でExpo Go同様に開発中のJSコードをホットリロードしながら動作確認できる。JSの修正ごとにアプリを再インストールする必要はなく、あくまで新しいネイティブ依存を追加したときにのみDevクライアント自体を再ビルドすれば良い点は開発効率を損なわない。

タナイタナイ

expo-background-fetch

利用するのに、以下の指定が必要。

app.config.ts
  ios: {
    bundleIdentifier: "your.bundle.identifier",
    supportsTablet: true,
+    infoPlist: {
+      UIBackgroundModes: ["fetch"],
+    },
  },

ちなみにしばらく以下のエラーがずっと出て困っていたが、先述のnpx expo install --fixにより動くようになった。

Error: Task 'background-fetch' is not defined. You must define a task using TaskManager.defineTask before registering.

イベントの手動発火は以下の手順に従えばよい。

https://docs.expo.dev/versions/latest/sdk/background-fetch/#triggering-background-fetches

ただし、今回はExpo Development Buildのため、指定するsimulatorをExpo Goではなくてexpoにする。

ちゃんとタスク登録されて「Unregister BackgroundFecth task」に表示が切り替わっている。

イベントを手動発火するとDevToolsにちゃんとコンソール表示が出た。

タナイタナイ

auth-proxyのデプロイ

turborepoなのでVercelでプロジェクト作成しようとすると対象appをプルダウンで選択できる。

auth-proxyをrootに指定してデプロイする。

ちなみに、DiscordのDeveloperアカウントは以前に別のアプリで都合よく作成済みだった。

環境変数にはそのプロジェクトのURLを指定する。このとき、auth-proxyのAUTH_REDIRECT_PROXY_URLには末尾の/rは付けない(Next.jsプロジェクトの環境変数には/rを付ける)。

ローカルでも無事にログインできた(表記が「Sign Out」になっている)。

タナイタナイ

実機にインストールしたい

Apple Developer Programへ入金は終わったものの、Apple側の確認に最大48時間かかるらしく支払いプロセスが完了していない。

そのため、実質的に無料アカウントの状態で実機インストールを試すことができないか試行錯誤した。

https://zenn.dev/k_zumi_dev/articles/6ac9ff4b39d0ef

この記事に助けられて、ローカルビルドを試すことができた。

npx expo export:embed \
--entry-file='node_modules/expo-router/entry.js' \
--bundle-output='./ios/main.jsbundle' \
--dev=false \
--platform='ios'
タナイタナイ

新しくExpo SDK パッケージを追加したとき

apps/expo上でpnpm iosするのを忘れるとモジュールが読み込めないので、パッケージ追加時は忘れずビルドする。

タナイタナイ

expo-background-fetchが動作しない

格闘したが動かなかったのでreact-native-background-fetchを利用することにした。
こちらは頻度は少ないものの動いている。

導入手順は下記のブログの手順のままでOKだった。
https://rakuraku-engineer.com/posts/react-native-background-fetch/

npx expo install react-native-background-fetch
app.config.ts
// 追記部分のみ抜粋
export default ({ config }: ConfigContext): ExpoConfig => ({
  ios: {
    infoPlist: {
      UIBackgroundModes: ["fetch", "processing"],
      BGTaskSchedulerPermittedIdentifiers: ["com.transistorsoft.fetch"],
    },
  },
  android: {
    permissions: ["RECEIVE_BOOT_COMPLETED", "WAKE_LOCK", "FOREGROUND_SERVICE"],
  },
  plugins: ["expo-router", "react-native-background-fetch"],
});
index.tsx
export default function Index() {
  // set background fetch
  useEffect(() => {
    const initBackgroundFetch = async () => {
      try {
        const status = await BackgroundFetch.configure(
          {
            minimumFetchInterval: 15, // minutes
            enableHeadless: true,
            stopOnTerminate: false,
            startOnBoot: true,
            requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY,
          },
          // main task
          (taskId) => {
            asyncFn
              .then(() => {
                console.log("[BackgroundFetch] taskId: ", taskId);
                BackgroundFetch.finish(taskId);
              })
              .catch((error) => {
                console.error(
                  "[BackgroundFetch] Failed: ",
                  error,
                );
                BackgroundFetch.finish(taskId);
              });
          },
          // timeout fallback
          (taskId) => {
            console.warn("[BackgroundFetch] TIMEOUT task: ", taskId);
            BackgroundFetch.finish(taskId);
          },
        );
        console.log("[BackgroundFetch] configure status: ", status);
      } catch (error) {
        console.error("[BackgroundFetch] Failed to configure: ", error);
      }
    };
    void initBackgroundFetch();
  }, []);

  // ...
}

expo-background-fetch

試したこと

  • ライブラリをインストール
  • app.config.tsのinfoPlist等の追記
    • UIBackgroundModes
    • BGTaskSchedulerPermittedIdentifiers(expo-task-managerで登録するものと一致させる)
  • タスク登録の成功確認
  • Instrumentsアプリを用いたsimulatorでの起動確認

できなかったこと

  • 実機での動作
タナイタナイ

TestFlightでの配布

有償アカウントの登録ができたのでTest Flightを試すことにした。

eas build --profile production --platform ios --local

apps/expo配下にbuild-0000000000000.ipa(0の部分は何らかの数字)という形式でビルド成果物が出力されるので、これをTransporterアプリにドラッグアンドドロップすればよい。

App Store Connectのページで対象アプリのTestFlightタブを開くとこんな感じで警告が出ているはず。

「管理」を押して進める。

とりあえず当てはまりそうな項目をクリックすると準備完了状態になる。

たぶん準備完了になると準備できたってメールも飛んでくるはず。

あー、この手順の前に「内部テスト」を追加して自分自身をテスターに追加する工程も必要かも。
難しくないので省く。

iPhoneでTestFlightアプリをインストールしてログインすれば、登録したアプリがインストールできるようになっているはず。

タナイタナイ

react-native-chart-kitが日付軸に対応してない

もしかしたら対応しているのかもしれんがAIも無理と言うし、そんな悩みをredditに投稿している人もいたのでたぶん本当なんだろう。

https://www.reddit.com/r/reactnative/comments/1efa7ex/react_native_chart_libraries_which_support/

react-native-echartsのビルドエラーが解消できない

https://github.com/wuba/react-native-echarts

react-native-echartsが雰囲気良さそうだったので使おうと思ったが、hermesのエラーが出て無理だった。
Metroに対応していないのかも?

victory-nativeを使う

Cursorのcomposerに投げたら素のReactと混同してひどいサジェストが出まくったので公式ドキュメントを最初にぶん投げて作らせるといいかもしれない。

https://commerce.nearform.com/open-source/victory-native/docs/

自分は面倒になってドキュメントを見て自力で実装した。
AIが別のものと勘違いするモードに入ると人間の方がまだ圧倒的に早い。

タナイタナイ

Expo Batteryで取得できるバッテリー残量が不正確

なんでだ〜、と思ってたらこれはExpo特有の問題ではない模様。

Battery.getBatteryLevelAsync()は内部的にiOSのUIDevice.currentDevice.batteryLevelを呼び出しているらしいが、このネイティブのAPI自体がiOS17からは5%刻みでしか取得できないらしい。

https://developer.apple.com/forums/thread/732903#:~:text=On iOS17%2C UIDevice,bug or a new feature

上記のページで、これはデバイスの個別特定につながりうる情報なのでプライバシーの観点で丸められているというようなことが書いてある。

タナイタナイ

まだ試せていないこと

  • 自前のネイティブコードの組み込み
  • Push通知
    • Expoの仕組みを利用するパターン
    • FCMを使用するパターン(こっちのほうが実装は手間だが安い?)
タナイタナイ

AndroidのEmulatorリセット

Android Studioを開いて[More Actions] -> [Virtual Device Manager]

タナイタナイ

Simulatorが開いた状態で画面上部のメニューバーから[Device] -> [Erase All Content and Settings...]するとリセットされるっぽい。

画面サイズを元に戻したいときは、[Window] -> [Point Accurate]

タナイタナイ

久しぶりにビルドしてエラーになる場合

iOSシミュレーターがないケースに注意

[RUN_FASTLANE]  Ineligible destinations for the "app" scheme:
[RUN_FASTLANE]          { platform:iOS, id:dvtdevice-DVTiPhonePlaceholder-iphoneos:placeholder, name:Any iOS Device, error:iOS 18.5 is not installed. To use with Xcode, first download and install the platform }
[RUN_FASTLANE] Exit status: 70

XCodeを立ち上げて設定のComponentsからiOSシミュレーターをインストールすること。
ログで怒られているバージョンがないはず。