この記事の背景と目的
- 最近、業務で「社内のPOと一緒に二ヶ月間でMVPとなるアプリを一つ作ってリリースする」という貴重な機会があった。
- 実際の開発は自分一人だったため、使ってみたい技術を自由に試してみることができ、非エンジニア職からの出戻りの自分にとっては学べることがとても多かった
- この記事では技術選定や設計の際に考えたことや、実際やってみての感想や気づき、反省点の備忘録として投稿する
作ったもの
-
「OurTime」というカップル(夫婦)向けエンゲージメントアプリを作成
- パートナーと二人でアプリを利用し、アンケートや日々の日記を投稿することで、アプリの方から二人の話し合いを促してくれて、二人の繋がりをより強くするというコンセプト
- 詳しくはLPを参照
- リリースは現状iOSのみ
- iPhone持っている人はぜひDLお願いします!→ストアリンク
使った技術について
技術スタック
- React Native(Expo)
- TypeScript
- Jset
- ESLint
- prettier
- husky
- Firebase
- Authentication
- Cloud Firestore
- Storage
- React Native Navigation
選定理由や感想、反省点など
- React Native(Expo)
- 自分は主にSwiftで育ってきたが、Swift以外でもiOSアプリを書いてみたいと思ったため
- 社内ではReact Nativeを書いてる人が多かったため、Flutterではなくこちらを選択
- この時点はReact Nativeの経験は0でReactも社内の研修で一ヶ月ちょっと触っただけだったが、関数型コンポーネントとカスタムhooksを理解できればあとはそんなに苦労しなかった。
- 二ヶ月と期間が短かかったことと、他にも技術的なチャレンジが多かったため、楽できるところは楽をしようと思いExpoを選択
- 結果、Expoを選択したのは正解だったと思う。ビルドに必要なあれこれは一切気にしなくてよいし、OTAは便利だし、公式ドキュメントが充実しており、基本的にこれを信じて実装すれば間違いはない。悩む必要がないのも時間が限られてる中では楽だった。info.plistをいじりたい時など、やや悩んだりしたが、調べれば情報はなくはなかった。ビルドに20分待たされることがあったりすることを除けばかなり快適。
- TypeScript
- React Nativeで調べた情報がTypeScriptが多かったという理由でなんとなくTypeScriptを採用
- TypeScriptを使うのも今回が初めてだったため、最初はかなり悩んだが、慣れてくると静的型付けということでSwiftと似た感覚で書けるのでとても快適だった。
- ただし、まだ表面的な理解してしておらず、雰囲気で使っている
- Jset
- 自動テストに関してはJestくらいしか選択肢ないんじゃないか?
- 今回一番苦労したのはJestかもしれない。
- この時点ではJest未経験というのもあり何度もつまづいた。
- 開発初期では気合を入れてテストを書いていたが、全体の作業時間のうち半分以上テスト書いてることに気付き、途中からテストの粒度を粗くし、最終的にテストを書くことをやめてしまった。
- これまで業務でテストを書く経験がなかったこともあり、どの程度のプロダクトのどのフェーズでどれくらいのテストを書けば良いのかという肌感覚みたいなものがなく、必要以上にテストに固執したり、逆にテストを放棄してしまったりしたことが今回の大きな反省点。
- ESLint
- 最低限のセットアップにとどめた。確か何かの記事を参考に設定しただけで、特にオリジナルの設定などはなし。チーム開発するならもっとちゃんと理解して設定する必要が出てきそう
- prettier
- 同上
- husky
- git commitをフックして、lintとformatを実行、git pushでtestを実行するように設定した。その代わりcircle ciなどは導入しなかった。
- プッシュの度にローカルでテスト通してるんだからci環境は必要ないだろ、でいいのかは不明
- 犬が可愛い
- Firebase
- 開発が一人でバックエンドは楽をしたかったためfirebaseを選択。なんでfirebaseかというと「モバイルアプリエンジアといったらfirebaseでしょ」みたいなイメージから。
- この時点はfirebaseを使った開発の経験はなし(swiftプロジェクトにCrachlyticsを導入だけしたことはあった)
- firebaseの採用は良い判断だったと思う。authenticationは認証基盤をあっという間に用意してくれるし、firestoreはなんでもjsonで突っ込んでおけるし、簡単にリアルタイム同期できてアプリ側のデータ管理が楽だった。難点としてはちょっとでも複雑なクエリが書けないこと。 複数のカラムに対するwhereができなかったり、whereしたカラム以外でorder byできなかったりは苦労した。
- storageはユーザーのプロフォール画像を保存するために利用したが、これも特に迷うことなく使えて楽だった。
- React Native Navigation
- 一番スタンダートっぽいものを利用した。たしかExpoでもお勧めされてた。
- スタックナビゲーションとタブナビゲーションの組み合わせがちょっと複雑だったり(特定の画面ではタブナビゲーションを非表示にしたいとかが難しい)、typescriptで使う時にちょっとよくわからなかったりしたけど、大きな不満はなかった。
- タブのデザインも結構カスタマイズできるし、やってないけど多分上部のナビゲーションバーも結構いじれると思う
設計について
ディレクトリ構成(一部抜粋)
- __tests__
- components
- containers
- screens
- hooks
- navigator
- assets
- images
- src
- components
- containers
- screens
- hooks
- __mocks__
- repositories
- __mocks__
- navigator
- contexts
- theme
- .env
- app.json
- App.tsx
- firebase.ts
ディレクトリ構成の感想、反省点など
-
src/
と__tests__/
- 基本的にプロダクトコードは
src
以下に収めるようにした -
__tests__
以下にはsrc
と同じディレクトリ構造とし、テストコードを整理した
- 基本的にプロダクトコードは
-
assets
- images以外のリソースを入れるかもと思い
assets/images
以下に画像ファイルを置いたが、特に画像以外のリソースが必要になるタイミングがなかったため、とくにimagesを作る必要はなかった。フォントとか音声ファイルを使うタイミングでディレクトリ分ければ十分だった。
- images以外のリソースを入れるかもと思い
-
root/
以下- 上記のディレクトリ構成では省略しているが、package.josnやjest.config.tsなど設定ファイル系はroot直下に配置している。
- やや煩雑としてしまった印象もあるが、プロダクトコードとは切り分けられているからまあいいかなという感想
-
.env
とfirebase
- firbaseの環境変数は.envに記載して、firebase.tsで読み出して、ExpoのreleaseChannelで本番と開発を切り替えるようにした
-
App.tsx
- App.tsxは極力なにも書かないようにした
- 認証の読み込みと、アプリ全体通して表示したいロード中のインジゲーターの表示コンポーネントだけを配置した。
- 開発初期段階ではfirebaseの認証ロジックや、ナビゲーションを書いていたのでかなり巨大だったが、それぞれカスタムhooksとナビゲーションだけをするコンポーネントNavigator.tsxに切り出すことでスリム化を実現した。
- ロード中のインジゲーターコンポーネントも最終的にはApp.tsxに書かなくてもいける方法を思いついたので、やろうと思えばApp.tsxは認証用のカスタムhooksの呼び出しだけOKになりそう
src以下の設計で感想、反省点など
- ビューに関するコンポーネントはcomponents, containers, screensで切り分けた
- 大きな方針としては、React Native Expressで紹介されていたコンテナコンポーネントとプレゼンテーションコンポーネントの考えにのっとて、ビジネスロジックを持つコンポーネント(Screens)と、propsを受け取ってイベントを返すだけのコンポーネント(componentsとcontiners)に分けることにした
- しかし一部誤解したまま進めていたため、continersの役割が間違っている。正しくはcontainersもビジネスロジックを持つことができる。ここら辺の正しい解説は本家のページを参照してもらいたい。
- 今思うと素直にAtomic Designを採用していればよかったと感じる。
- components
- ButtonやText、TextInputなど、アプリ全体通して汎用的に使うコンポーネントはここに入れた。Atomic DesignでいうところのAtoms的なノリだが、別にcomponentsの中で他のcomponentsを参照することは禁じなかった。
- たとえばボタンのラベル部分にはTextコンポーネントをimportして使っている
- ButtonやText、TextInputなど、アプリ全体通して汎用的に使うコンポーネントはここに入れた。Atomic DesignでいうところのAtoms的なノリだが、別にcomponentsの中で他のcomponentsを参照することは禁じなかった。
- containers
- 本来の役割は「ビジネスロジックを持った、画面全体ではないがある程度の塊を持った複数の画面で使いまわされるコンポーネント」を入れておくディレクトリ。
- 例えばユーザーのプロフィールのコンポーネントなど
- 今回は「ビジネスロジックを持たずに、画面全体ではないがある程度の塊を持った複数の画面で使いまわされるコンポーネント」として扱っていた。
- とはいえ、後述するが結局ビジネスロジックは全てカスタムhooksに切り出していたため、カスタムhooksをscreensで読み出してpropsで渡すか、直接containersで読み出すかの違いぐらいしかなかったとは思う。
- Atomic DesignでいうところのMoleculesやOrganismsに該当すると考えられるが、厳密なルールを決めていなかったため、componentsとcontainersの区切りが曖昧で感覚的なものになってしまった。
- componentsよりは汎用性が低いがscreensではないものがここに入っている。
- containersが他のcontainersをimportすることはあるが、componentsがcontainersをimportすることはない
- 本来の役割は「ビジネスロジックを持った、画面全体ではないがある程度の塊を持った複数の画面で使いまわされるコンポーネント」を入れておくディレクトリ。
- screens
- swiftのViewControllerの感覚で書いた。
- いち画面いちScreen.tsx
- 必要なデータやビジネスロジックはカスタムhooksからもってきて、表示したいcomponentsとcontainersにpropsで渡す役目。また返ってきたイベントの処理もscreesで行う。
- コンポーネントの表示非表示や、表示するためのデータの整形などもここで行う。
- ビジネスロジックとビューの間を取り持つことになるのでimportするものが多くなりがちだった。
- これを防ぐには、本来の意味でのconainersの使い方をすればロジックを分散させたりしてscreensをスリムにできたのかもしれない。あとは冗長なpropsのバケツリレーも防げたと思う。
- 大きな方針としては、React Native Expressで紹介されていたコンテナコンポーネントとプレゼンテーションコンポーネントの考えにのっとて、ビジネスロジックを持つコンポーネント(Screens)と、propsを受け取ってイベントを返すだけのコンポーネント(componentsとcontiners)に分けることにした
- hooks
- カスタムhooksはここにまとめた
- 実際に作ったカスタムhookは
useAuth
(firebase authを用いて認証を行う)やuseCurrentUser
(認証したユーザーのデータを取得したり、更新する),useDiary
(ユーザーの投稿した日記一覧を取得したり、追加フェッチしたり、追加削除更新する)などなど - 主な役割はrepositoryを操作してデータを取得し適切に加工しscreensに渡すことと、screensにデータの追加更新削除のメソッドを渡すことが多かった。
- これによりscreensはデータのあれこれについて気にしないで済んだ。
- 逆にカスタムhooksはビューに関しての責務はない
- repository自体は原則データの永続化についてのみの責務しか持たせなかったので、どんなデータを要求するかといったロジックや、バリデーションロジックはカスタムhooksに集約されている。
- ビジネスロジックはhooksに集約するという方針でなんでもかんでもカスタムhooksにしていったため、「それhooksじゃなくても良くない?」みたいなものも多い気はするが、責務の分離という観点では成功だったと思う。
-
__mocks__
の中に同じ名前でuseHoge.tsを作成するとテストの時にいい感じにモックを使えて便利だった
- repositories
- データの永続化と、アプリないで使うTypeの定義を行なっている
- 例えばUserRepositoryではfirestoreからユーザを検索して取得するのと、
type User
を定義して、useCurrentUserにUserを返したりしている。
- 例えばUserRepositoryではfirestoreからユーザを検索して取得するのと、
- 保存先は今回はfirestoreか、ローカルストレージだが、repository以外では保存先を意識せずに済んでいる。
- 扱うリソース(UserとかDiaryとか)ごとにrepositoryの実装がそれぞれ特殊になってしまった。
- この辺りはfirestoreの使い方が試行錯誤で慣れてなかったせいもある
- もっと抽象化したかっこいい実装にしたかった。
- データの永続化と、アプリないで使うTypeの定義を行なっている
- navigator
-
Navigator.tsx
を入れるためだけのディレクトリ。他と階層を合わせるために作成。 - ナビゲーターを切り出したのは正解だった。すぐに大きくなるため。
- 今回は一つの
Navigator.tsx
に全てのナビゲーションを書いたが、アプリの規模に応じてナビゲーターも分割すると見通しが良くなりそうと感じた。
-
- contexts
- ReactのContextを各所でimportして使うために用意。
- 多分使い方の問題だけどContextとReact Native Navigationとの相性がよくなかったため、あまりContextを活用しなかったのであまり感想がない。
- theme
- Colors.tsとFontSize.tsを作り、アプリ内で使う色とフォントサイズを一元管理した。
その他の感想、反省点など
- ユーザーデータの管理はauthentication+firestore+storageの合わせ技で行った
- authenticationでアカウントを作成し、authenticationで発行されたuidをdoc名にしてfierstoreにusersを作成した
- メールアドレスも含めた全てのユーザ情報はfirestoreに保存した。authenticationでもユーザ名の保存などはできたが、authenticationに保存できるデータだけでは足りなかったため、authenticationは認証目的だけに絞った。
- プロフィール画像はstorageにアップロードし、そのDLリンクをfirestoreにusersに保存した
- アプリ内でプロフィール画像を表示する時には毎回DLするとお金がかかりそうだったため、UserRepository内でキャッシュし、ローカルパスを使い回す方法で節約した
全体通した感想とまとめ
- 二ヶ月という限られた期間内に、新しい技術をいくつもキャッチアップしながらMVPとなるアプリのリリースまで達成できたのは大いに自信につながったし、達成感も味わえた。
- 責務の分離も概ね成功したし、初めてのReact Nativeアプリにしてはいい感じに作れたと思う。
- 複雑なことをしないアプリであればExpoとfirebaseの組み合わせは最速でリリースできる最強の組み合わせなんじゃないかと感じた(他をしらないだけだけど)
- 後半、リリースに間に合わせるために妥協が多くなってしまったことと、結局テストは書けなかったことが反省。
- 今回は実装に必死だったためあまりできなかったが、POのやりたいことの具現化やデザイナーとのデザインの調整など、もっと上流からPOをサポートできればよりよいものが作れたかもしれない。(POにはさらに上流の「どんな課題があって、ユーザーには何が必要なのか」みたいな部分に集中できる環境を提供したかった)
だらだらと感想を書いてきたが、アプリは結構いいものができたと思うので、iPhone持っている人はぜひDLお願いします!(2回目)→ストアリンク
Twitterもやってるので、よければフォローお願いします。
→https://twitter.com/ObataGenta