初めましてsatoshiです。
エンジニアとしては業務未経験のエンジニアの僕と非エンジニアであるCEOの二人三脚で開発したバックエンドオールFirebaseアプリの知見を残しておきたいと思い記事にします。
学生エンジニアが数ヶ月で一気にプロダクトを完成させる工程という観点でも参考になれば幸いです。
#アプリ概要
「Wilico」というヘルスケアCtoCサービスを開発しました。
Wilicoはダイエットや食事管理が必要なタイミングに合わせて、ユーザーと管理栄養士さんをマッチングし、すぐにプロによる食事管理を受けることができるサービスです。
〜生きるために食べる時代から、(健康に)生きるために痩せる時代へ〜
飽食の時代と言われるようになり、好きなものを好きなだけ食べることができる現代ですが、その先に待っているのは生活習慣病です。
すなわち、健康に生きるためには、好きなものばかり食べるわけにはいかなくなってきたわけです。
そんなところに課題感を感じサービスを開発しました。
#開発時期
2019年4月中旬〜2019年6月末
##開発人数
エンジニアの自分と、CEOの2人。
Firebaseを触るのは2人とも初めてで、Vueは2人とも書いたことがありました。
CEOはコーディングと簡単なJS書ける程度
##開発フロー
- モック作成(Adobe XD)、UI設計 / 3日
- 技術選定 / 1日
- 環境構築 / 1日
- Docker(今はデプロイでしか使ってない)
- Firebaseのプロジェクト作成
- Firebase勉強タイム、DB設計 / 1日
あとは書きながら学んで行った感じで、
Firebaseに関してはひたすらドキュメントと海外のエンジニアの方の記事やソースコードを読み漁っていました。
当時はご飯食べながら海外のFirebaseのYoutubeとか観てましたw
基本リモートで週2,3回ジョナサンでオール開発というブラックさでした。。笑笑
##役割
自分 | Firebase, Nuxt, Cloud Functions,Stripe |
相方 | Nuxt,モックアップ(UI設計),コーディング |
基本的には機能を自分が作って、それを相方に投げてコーディングしてもらうスタンスで開発を進めていた感じです。
後半になると相方もNuxtとFirebaseに慣れてきて開発速度が上がりました。
2ヵ月半の開発期間のうち、1ヵ月半で主な機能を完成させ
最後の1ヵ月はひたすらバグ回収とブラッシュアップをする期間に。
バグ回収辛かったなあ(笑)
今回初めて本当にユーザーがいる想定で開発したので、どうしたら使いやすいかをUI・UXに関してかなり詰めたし、
普段使ってるTwitterやLINEなど、どれだけこだわり抜いてPDCAを繰り返してあのUIが完成しているんだなと実感しました。
#使用技術
APIのフロー的にはこんな感じです!
| | |
|:-----------------|:------------------:|:------------------:|
| フロント | Nuxt.js |
| バックエンド | Firebase Cloud Functions / CloudStore |
| Auth | Firebase Authentication |
| 決済 | Stripe Billing / Connect |
| CI / CD | Circle CI |
| Hoting | Firebase Hosting |
| 全文検索 | Algolia |
| その他 | Docker, Zapier,Slack |
バックエンドはほとんどFirebaseに頼り切っています!
本当に便利なところと不便なところがはっきりしてて、
NoSQLが活きる開発(ちょっとした個人開発など)には最強かと思いました。
開発中にFirebaseの新しいバージョンリリースでCollectionGroupというものが使えるようになったりと、今後改善されるところは多々ありそう。(何箇所かでこれ使ってます)
ただ今回のようなテーブル数がかなり多いアプリケーション開発においてはRDBの方がやりやすいと感じました。
また、CloudStoreはクエリが貧弱なので検索周りはAlgoliaというサードパーティに丸投げしています!
Nuxtについてはここ半年書き続けてきたから慣れていたが、コンポーネントのライフサイクルやstoreの管理の仕方についてはとても勉強になりました。
一回もリロードをしないで完全なるSPAを目指すことでPWAとしてモバイルで使えるようにできたことはNuxtの恩恵として大きかったです。
開発中に導入して大きなメリットを感じたのは、以下の2点をSlackに通知するようにしたこと。
・GitHubの特定のブランチにpushしたら結果を通知
・GithubのreleaseブランチにpushしたらCircle CIで自動デプロイして結果を通知
管理栄養士の方には、管理栄養士免許証写真を提出していただいていますが、僕らが目視で確認して許可するまではプランの作成ができないようになっています。
見逃しがないように、Slack APIを用いてコーチ申請がきたらSlackの特定のチャンネルに通知を飛ばすように設定しています。
Webを選んだ理由
相方がエンジニアではないので、SwiftでUIを構築していくのに開発速度が落ちる
β版でリリースしてユーザーテストを繰り返してブラッシュアップして行きたかったから
##Firebaseを選んだ理由
以前RailsとNuxtで開発したときにやりにくさを感じた。フロントエンドを書いてる時間が圧倒的に長く、フロント主体ならサーバーレスで開発したほうがやりやすそうと思ってたから。
単純にRails飽きたのと、新しい技術を触ることでモチベーションを維持し一気に開発を進めるため。
##苦労したところ
- firebase.auth().currentUserが取得できない!!
これは今となっては当たり前なのですがfirebaseからデータを取得してる通信の時間があるため、間に合ってないことが原因。
mounted(){
console.log(firebase.auth().currentUser) // => undifined
}
mounted(){
window.setTimeout(() => {
console.log(firebase.auth().currentUser) // => 取得できる
}, 2000)
}
ログイン情報はstoreに入れとく。
- リロードした時にログイン状態が消えてしまう
これはvuexのstateやpropsがリロードしたら破棄される性質を持ってるいることが原因。
vuex-persitetedというプラングインを使えば、session storageとstoreをバインディングすることができます。
当初はこのやり方でやっていたが、Nuxt.jsのライフサイクルの特性を利用してpluginsの中で(pluginsはコンポーネントのマウントよりも更に前で読み込まれます)
firebase.auth().onAuthStateChanged(){}
を呼び込み、actionsを叩いてstoreに入れ込む関数を作るようにしました。
これでリロード時もログイン状態を保持できるようになり、storeのstateはコンポーネントがマウントされる前に読み込まれるので二つの問題を解決できました!
- Firebase CloudStore(NoSQL)の設計
開発初期の段階で何がベストプラクティスなのかをかなり吟味しましたが、ここは先輩方にお聞きした方が早いと思い、
PostCoffeeさん主催のイベントに参加し、Firebaseの開発では日本で一番すごいと思っている@1amageekさんにお会いして設計面を相談させていただきました!そこから自分たちなりにtry&errorを繰り返して設計していきました。アドバイス頂いた@1amageekさんと@Masataka-nさんにはとても感謝しています!
最終的にルート階層はこんな感じです
あとはこれらの下にSubCollectionとしていくつかコレクションが入っています
(roomsの下にmessagesなど)
- StripeでCtoCサブスクリプションを行う上での最適なフロー
「Stirpe Billingのサブスクリプション」の概念を利用しています。
- まずProduct(商品)を作成し、その中に複数のPlan(料金プラン)を作成
- ユーザーがクレジットカード登録をした段階でCustomerを作成
- 契約が押されたら先ほど登録したPlanとCustomerを繋ぎ合わせることでサブスクリプションという状態が完成します
※Customerの作成にはStripe ElementというUIコンポーネントを使ってカードのtokenを作成し、axiosでcloud functionsの エンドポイントを叩く-> Stripeで顧客データを作成
-
Stripe外部銀行口座振り込み
僕らのサービスでは、CtoCなので銀行口座振込みなどでStripe connectも併用しています。 -
タイムラインなど多くのテーブルを跨ぐページでのデータ取得→ここでプロミス地獄に陥りasync await芸人と化す
-
リレーショナルなtransactionをかけたい場面が多く、。NoSQLの難しさも実感した。
-
チャット周り
-
非同期でチャットの未読数を更新してナビゲーションバーに表示する
まずDB的にはmessageに誰に送ったかの情報(toプロパティ)を付与し、デフォルトで未読状態(notice: false)にしときます。
コンポーネントがマウントされた時(= 個別チャットに飛んだ時に一気に既読状態(notice: true)にupdateをかけています)
同時にチャットを開いている状態も可能性としてあるので、コンポーネントを離れる時(destroyed)にも一気に既読状態(notice: true)にupdateをかけています
チャットを送るときのデータはこんな感じです。
db.collection("rooms")
.doc(this.$route.params.slug)
.collection("messages")
.add({
notice: false,
to: this.room.coach.id,
from: this.$store.state.auth.currentUser.id,
content: this.message,
timestamp: Date.now()
})
##反省点
設計は本当に最初にガッチリ決めてから開発に入った方がいい。
今回はNoSQLが初めてだったのと、自分自身作りながら考えるのが性に合ってるというのもあるが、開発中に何回も設計を見直した。開発中のDB設計変更の反映はただNoSQLなので良くも悪くもマイグレーションをかける必要がないのでそこまで大変でもなかった。
#まとめ
・Firebaseは習得するのに敷居が低いのと、開発の初速が早い
・Firebaseは慣れたら爆速で開発ができるが、大規模なサービス開発にはまだ追いついてない感じ(NoSQLが故に)
・いかにリレーショナルデータベースが便利かを実感した
・Nuxt(Vue)は書いてて楽しい!(規約みたいなのが結構あるのでRailsと似ている気がします)
・大体困った時は公式のドキュメントをしっかり読み込めば書いてあります。この癖がついたことは良かったかも。(Githubのissueとかにヒントあったりもする)
・次フロント書くときは最初からTypeScriptで。
Firebaseは本来mbass(mobile backend as a Service)ですが、WebでもJavaScript系のバックエンドとしてめちゃくちゃ相性が良いので今後に注目していきたいです!
##追記
一応会社のHPもNuxtで作りました!
https://wilico.co.jp