Realtime Messaging with Firebase #phpcon2017

Realtime Messaging with Firebase #phpcon2017

PHP Conference Tokyo 2017で発表しました。

046baac588d91fd78a85b189847a151d?s=128

Sota Sugiura

October 08, 2017
Tweet

Transcript

  1. Realtime Messaging with Firebase PHP Conference Tokyo 2017 @sota1235

  2. 悲報

  3. Cloud Firestore launched! https://firebase.googleblog.com/2017/10/introducing-cloud-firestore.html

  4. 要約 Realtime Databaseの 次世代版が出たよ

  5. 今⽇日 10/8 ブログポスト 10/3

  6. But Realtime Database is not dead https://firebase.googleblog.com/2017/10/introducing-cloud-firestore.html

  7. But Realtime Database is not dead https://firebase.googleblog.com/2017/10/introducing-cloud-firestore.html

  8. 今⽇日の⽅方向性 • Cloud FirestoreとRealtime Databaseの違いを 最後に話します • Cloud Firestoreを使うとしても役に⽴立つ知⾒見見 に寄せました

  9. Realtime Messaging with Firebase PHP Conference Tokyo 2017 @sota1235

  10. About me • Sota Sugiura • @sota1235 • Mercari, Inc.

  11. 突然ですが…

  12. こんな画⾯面 ⾒見見たことある⼈人

  13. メルカリチャンネル • 今年年7⽉月にローンチした Liveコマース機能 • メルカリの⼀一機能 • 動画で売れる/買える https://www.mercari.com/jp/mercari-channel/

  14. 気になる⼈人はggってください 今⽇日は宣伝に来たわけではないので…

  15. 4⽉月下旬のこと P 「Liveコマース出したいんだよね」 ⋵

  16. 4⽉月下旬のこと P 「Liveコマース出したいんだよね」 ⋵ 「なるほど」

  17. 4⽉月下旬のこと P 「Liveコマース出したいんだよね」 ⋵ 「なるほど」 P 「7⽉月にリリースしたいんだよね」

  18. 4⽉月下旬のこと P 「Liveコマース出したいんだよね」 ⋵ 「なるほど」 P 「7⽉月にリリースしたいんだよね」 ⋵ 「なるほど???」

  19. サービスとして求められていたこと w ڝ߹ΑΓૣ͘ग़͢ w ཁ݅ఆٛɺ։ൃɺ2"ɺϦϦʔε ·ͰΛϲ݄Ͱ࣮ݱ͢Δ ① 7⽉月上旬のリリース w ੈք؍Λ࣮ݱ͢ΔΪϦΪϦΛ

    w ·ͣಈ͘΋ͷΛੈʹग़͢ ② 最⼩小機能で動くものを
  20. サービスとして求められていたこと w ڝ߹ΑΓૣ͘ग़͢ w ཁ݅ఆٛɺ։ൃɺ2"ɺϦϦʔε ·ͰΛϲ݄Ͱ࣮ݱ͢Δ ① 7⽉月上旬のリリース w ੈք؍Λ࣮ݱ͢ΔΪϦΪϦΛ

    w ·ͣಈ͘΋ͷΛੈʹग़͢ ② 最⼩小機能で動くものを ʢສμ΢ϯϩʔυɺҰ೔ ສग़඼͞ΕΔτϥϑΟοΫ ͷΞϓϦͰ΋໰୊ͳ͘ʣ
  21. 技術的に求められていたこと ಈը഑৴ࢹௌػೳ αʔϏεͷ৺ଁͱ΋ ݴ͑Δػೳ

  22. 技術的に求められていたこと ಈը഑৴ࢹௌػೳ ϦΞϧλΠϜ௨৴ ϥΠϒײΛ͓٬͞·ʹ ͓ಧ͚͢Δ

  23. 技術的に求められていたこと ಈը഑৴ࢹௌػೳ ϦΞϧλΠϜ௨৴ ଱ߴෛՙ αʔϏεͷεέʔϧͱ Մ༻ੑΛݟਾ͑Δ

  24. ロードマップ ཁ݅ఆٛ ݟੵ΋ΓείʔϓܾΊ 5⽉月 5⽉月中旬 7⽉月 ։ൃ ಈըपΓϦΞϧλΠϜ௨৴  2"

    ϦϦʔε
  25. ロードマップ ཁ݅ఆٛ ݟੵ΋ΓείʔϓܾΊ 5⽉月 5⽉月中旬 7⽉月 ։ൃ ಈըपΓ1VTI௨৴  2"

    ϦϦʔε スケジュールがかなりタイト
  26. 締切 vs ⼯工数 • チームにはAPIエンジニア2⼈人、iOS/Android1⼈人 ずつ • 動画基盤やサーバPush基盤のノウハウは0 • 1から全部完成させようと思ったらとても2ヶ⽉月で

    は厳しい • しかしこのサービスは早く出すことに何よりの意 味があった
  27. どう実現するか

  28. なるべく作らない • 使えるもの(クラウドサービス)を使い倒す • Microserviceとして作らない • 既に持っている資産を再利利⽤用する • スコープを削れるだけ削る •

    サービスコンセプトを追求する
  29. 利利⽤用したクラウドサービス ಈը഑৴ࢹௌػೳ ϦΞϧλΠϜ௨৴ ଱ߴෛՙ ċ 㡿ކ

  30. 利利⽤用したクラウドサービス ಈը഑৴ࢹௌػೳ ϦΞϧλΠϜ௨৴ ଱ߴෛՙ ċ 㡿ކ 今⽇日のお話

  31. About Firebase Realtime Database

  32. Firebaseとは • Googleの提供するBaaS • モバイルに⾮非常に特化している https://firebase.google.com/?hl=ja

  33. Firebase Realtime Database • Firebaseの機能の1つ • JSON形式のデータの永続化 • ClientからリアルタイムにデータをSubscribeできる •

    ⼤大量量のClientにも対応 • 10万接続 / 秒間1000回の書き込み
  34. Subscribe Data { "messages": { "1": { "text": "hello, firebsae",

    "user": "sota1235" } } }
  35. Subscribe Data Subscribe /messages { "messages": { "1": { "text":

    "hello, firebsae", "user": "sota1235" } } }
  36. Subscribe Data { "messages": { "1": { "text": "hello, firebase",

    "user": "sota1235" }, "2": { "text": "hello, phpcon", "user": "phper" } } } Subscribe /messages New data
  37. Subscribe Data { "messages": { "1": { "text": "hello, firebase",

    "user": "sota1235" }, "2": { "text": "hello, phpcon", "user": "phper" } } } Subscribe /messages New data New data
  38. Even if many clients { "messages": { "1": { "text":

    "hello, firebase", "user": "sota1235" }, "2": { "text": "hello, phpcon", "user": "phper" } } }
  39. Not Only Database feature • 認証機能 • Facebook, Twitter, Custom

    Auth, etc… • Rule for each tree • Permission • Validation
  40. Rule • JSON形式で定義できる • JSON treeに対して細やかな制御ができる • Read/Write Permission •

    Validation • Adminユーザは全Ruleを無視できるので注意
  41. { "rooms": { "1": { "messages": { "1": { "text":

    "hello, php", "user": "sota1235" } } } } } 例例えばこんなJSON
  42. { "rooms": { "1": { "messages": { "1": { "text":

    "hello, php", "user": "sota1235" } } } } } 例例えばこんなJSON νϟοτϧʔϜΛදݱ
  43. { "rooms": { "1": { "messages": { "1": { "text":

    "hello, php", "user": "sota1235" } } } } } 例例えばこんなJSON ෦԰͝ͱͷVOJRVFͳ*%
  44. { "rooms": { "1": { "messages": { "1": { "text":

    "hello, php", "user": "sota1235" } } } } } 例例えばこんなJSON νϟοτϧʔϜ಺ͷ ίϝϯτҰཡ
  45. Ruleを設定してみる

  46. { "rules": { ".write": true, ".read": true, ".validate": "validation rule"

    } } Ruleの基本⽂文法
  47. { "rules": { ".write": true, ".read": true, ".validate": "validation rule"

    } } Ruleの基本⽂文法 ① おまじない
  48. { "rules": { ".write": true, ".read": true, ".validate": "validation rule"

    } } Ruleの基本⽂文法 ① おまじない ② Read/Write 権限
  49. { "rules": { ".write": true, ".read": true, ".validate": "validation rule"

    } } Ruleの基本⽂文法 ① おまじない ② Read/Write 権限 ③ Validationルール
  50. 例例. 意図しないKeyの追加 { "rooms": { "1": { "messages": { "1":

    { "text": "hello, php", "user": "sota1235" } } } } } • 意図しないJSON key を追加させたくない • rootにはroomsだけ⽣生 えてて欲しい
  51. 例例. 意図しないKeyの追加 { "rooms": { "1": { "messages": { "1":

    { "text": "hello, php", "user": "sota1235" } } } }, "extra": { "msg": "pwned" } } • 意図しないJSON key を追加させたくない • rootにはroomsだけ⽣生 えてて欲しい • こういうのを追加させ ない
  52. { "rules": { ".write": false, "rooms": { ".write": true, ".read":

    true } } } 例例. 意図しないKeyの追加
  53. { "rules": { ".write": false, "rooms": { ".write": true, ".read":

    true } } } 例例. 意図しないKeyの追加 Tree最上部への書き込みを 許容しない
  54. { "rules": { ".write": false, "rooms": { ".write": true, ".read":

    true } } } 例例. 意図しないKeyの追加 Tree最上部への書き込みを 許容しない “rooms”配下への あらゆる書き込みを許可する
  55. { "rules": { ".write": false, "rooms": { ".write": true, ".read":

    true } } } 例例. 意図しないKeyの追加 Tree最上部への書き込みを 許容しない “rooms”配下への あらゆる書き込みを許可する ਌ͷ3VMFΑΓ΋5SFF಺ͷ3VMF༏ઌ͢Δ
  56. Ruleの活⽤用例例 • 全Treeへの読み書き制御 • Validation処理理 • 認証/認可の制御 • 認証を管理理するTreeを⽤用意し、そこに存在し ないuser_idの書き込みを弾く、なんてこと

    もできる
  57. Ruleの適⽤用 • GUI or REST APIで⾏行行う • REST APIでの管理理がおすすめ

  58. Realtime Databaseを どこで使うか

  59. サーバPush⽅方式のデータ全て

  60. サーバPush⽅方式のデータ全て ↓ ライブ感を演出するもの全て

  61. ライブ感を演出するもの ↓ 即反映されて欲しい情報

  62. ライブ感を演出する要素 • コメント • いいね • 視聴者数の変動 • 商品周りのメッセージ

  63. ライブ感を演出する要素 視聴者数 コメント いいね 商品リスト 更更新

  64. ユーザに⾒見見せないもの • ライブ終了了通知 • 商品リスト更更新通知 • その他メタ情報

  65. Architecture

  66. アーキテクチャ

  67. アーキテクチャ 順番に解説していきます

  68. Step to use Realtime Database

  69. Step 1. スキーマ設計 2. Rule設計 3. Read/Write実装 4. スケーリング

  70. 1. スキーマ設計

  71. とにもかくにもスキーマ設計

  72. スキーマ定義のコツ • Subscribe側を意識する • ⾮非正規化したほうが場⾯面もある • 後⽅方互換性を意識する • バージョンアップした時に困らない構成を

  73. { "lives": { "1": { "messages": { "1": { "user":

    "sota1235", "image": "hoge.png", "text": "hello" } }, "notifications": { "buy_item": { "text": “sota1235さんが商品を購⼊入したぞい" } } } }, "alive_lives": { "1": true, "2": false } } メルカリチャンネルの例例(⼀一部略略)
  74. あれ?配列列がないけど… • Realtime DatabaseのJSONで配列列は扱えない • 特定のnodeにaddすると⾃自動でunique IDが振 られる messagesにaddして ⾃自動で振られたID

  75. Q. スキーマレスだけど、   どうやってスキーマ縛るの? Aを Q&A

  76. Q. スキーマレスだけど、   どうやってスキーマ縛るの? A. Ruleを使います Q&A

  77. 2. Rule設計

  78. Ruleによるスキーマ定義 • Realtime Databaseにスキーマという概念はない • データは⼀一枚岩のJSON • ドキュメント等にスキーマを記録し、書き込み/ 読み込み時の実装を⾏行行う •

    スキーマの整合性はRuleで担保する
  79. { "lives": { "1": { "messages": { "1": { "user":

    "sota1235", "image": "hoge.png", "text": "hello" } }, "notifications": { "buy_item": { "text": “sota1235さんが商品を購⼊入したぞい" } } } }, "alive_lives": { "1": true, "2": false } } メルカリチャンネルスキーマ(再掲)
  80. まずは追加できるkeyを縛る • あらかじめ決めたスキーマのkeyのみ書き込み を許可する • それ以外は許可しない • コメント等、データを丸ごと追加するものは それに含むkeyに対してvalidationをかける

  81. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule
  82. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule +40/SPPUʹॻ͖ࠐΈΛڐՄ͠ͳ͍
  83. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule NFTTBHFT഑Լͷ৽σʔλʹ VTFS JNBHF UFYULFZΛڧ੍͢Δ
  84. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule ʁʁʁ
  85. データ読み込みの動的制御 • ライブ放送中はデータ読み取りを許可する • 放送後はデータを読み込ませない

  86. データ読み込みの動的制御 • ライブ放送中はデータ読み取りを許可する • 放送後はデータを読み込ませない • 特定のライブIDが放送中かどうかのフラグを 管理理し、動的に変更更する

  87. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule ͜ΕΛ࢖͏
  88. { "rules": { ".write": false, "lives": { "$live_id": { "messages":

    { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule "alive_lives": { ".write": false, ".read": false } ΫϥΠΞϯτ͔Β͸ ಡΈॻ͖ΛҰ੾ͤ͞ͳ͍
  89. { "rules": { ".write": false, "lives": { "$live_id": { ".read":

    "root.child('alive_lives').child($live_id).val() === true", "messages": { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule
  90. ".read": "root.child('alive_lives').child($live_id).val() === true", Rule

  91. ".read": "root.child('alive_lives').child($live_id).val() === true", Rule JSONのrootから alive_channels.$live_idの値を 参照する

  92. ".read": "root.child('alive_lives').child($live_id).val() === true", Rule 該当のIDの値が alive_lives配下でtrueの時のみ 読み取り可

  93. { "lives": { "1": { ... }, "2": { ...

    } }, "alive_lives": { "1": true, "2": false } } つまり alive_lives[“1”]がtrueなので 読み取り可能
  94. { "lives": { "1": { ... }, "2": { ...

    } }, "alive_lives": { "1": true, "2": false } } つまり alive_lives[“2”]がfalseなので 読み取り不不可
  95. { "lives": { "1": { ... }, "2": { ...

    } }, "alive_lives": { "1": true, "2": false } } つまり ライブ放送開始/終了了時に ここのフラグを編集する
  96. ちなみに • メルカリチャンネルではクライアントからの 書き込みを⼀一切許容していない • ⼯工数節約のため • APIの負担を減らすなら必ずやるべき • その際はより細やかなRule設定が重要

  97. 3. Read/Write実装

  98. アーキテクチャ(再掲)

  99. アーキテクチャ(再掲) Read Write

  100. How to write data? Read Write

  101. Write戦略略 • データの書き込みをメルカリ側のAPIか らのみ⾏行行う • データの書き込みは複雑なロジックが多 い • 認証/NG判定/攻撃検知 •

    こういった処理理をAPI側で担う • 認証はFirebase側でも可能 • それ以上はCloud Functionで Write
  102. { "rules": { ".write": false, "lives": { "$live_id": { ".read":

    "root.child('alive_lives').child($live_id).val() === true", "messages": { "$message_id": { ".validate": "newData.hasChildren(['user', 'image', 'text'])" } }, "notifications": { "buy_item": { ".validate": "newData.hasChildren(['text'])" } } } }, "alive_lives": { ".write": false, ".read": false } } } Rule(再掲) Adminユーザ以外に ⼀一切の書き込みを許容しない
  103. 書き込み処理理の流れ

  104. 書き込み処理理の流れ ① APIでリクエストを受ける ② Firebaseへのリクエストを   Enqueue ③ Worker processでJobを

      Dequeue, Firebaseへ書き込み
  105. 書き込み処理理の流れ ② Firebaseへのリクエストを   Enqueue ③ Worker processでJobを   Dequeue,

    Firebaseへ書き込み
  106. ① APIでリクエストを受ける • クライアントからデータを 受け取る • 認証、DB保存、NG判定等 • Firebaseにデータを送る場 合はリクエストJobを

    Enqueueする
  107. 全てのDataをFirebaseに送る? • 送らない • 多すぎる書き込みはパフォーマンス低下を招く • ユーザ体験を損なわないものは間引く • いいね数は0.1秒に⼀一度しか送信しない •

    間引くものと間引かないものを分ける
  108. 書き込み回数の計算 • 秒間あたりFirebaseに1000回まで許容する • いいね数を0.1秒に1回、視聴者数1秒に1回 • するとコメント他メッセージは秒間989回ま で送れる

  109. ② FirebaseへのリクエストをEnqueue • Firebaseへのリクエストは APIでやらない • Don’t trust each other

    • FirebaseをSPOFにしない
  110. • メルカリAPIで利利⽤用しているqueueシステム • MySQLで動いている • 詳しくは[検索] [Q4M] Q4M Enqueue Dequeue

  111. ③ Firebaseへリクエスト • WorkerからFirebaseへリ クエスト • Firebase REST APIを叩く •

    kreait/firebase-phpを利利⽤用 • 公式で紹介されてる2つ のうち、こちらのほうが 質がいい
  112. クライアントからFirebaseまで

  113. クライアントからFirebaseまで ①データを送信

  114. クライアントからFirebaseまで ͜͜Ͱ'JSFCBTFʹૹΔ͔ Ͳ͏͔൑அ͢Δ ①データを送信

  115. クライアントからFirebaseまで ①データを送信 ②Queueを通じて リクエストJobを渡す

  116. クライアントからFirebaseまで ①データを送信 ②Queueを通じて リクエストJobを渡す ③REST APIを叩く

  117. How to read data? Read Write

  118. Read戦略略 • クライアントは受け取った データを表示するだけ • 細かい制御はしない • 終了了済みのLiveデータは読 み取れなくする

  119. Firebase接続の流れ GET channel Connect information Connect to Firebase Data publish

  120. クライアント実装 • 公式SDKを利利⽤用する • iOS/Android/JavaScript • 普通に使う分には特に難しいポイントはない

  121. Subscribe data • SDKのInterfaceはQuery形式で書けない • dataをsubscribeすると接続時点の過去のデータも取得される • 「視聴した瞬間からのコメントのみ取得」といったInterfaceは SDKにはない firebase.database()

    .ref(‘live/1235/messages') .on('child_added', (snapshot) => { console.log(snapshot.val()); });
  122. Timestamp • データにtimestampを付与し ておく • このtimestampと現在時刻を ⽐比較して表示するかどうか判 定する { "text":

    “old", "timestamp": 1507226243 } { "text": “still old", "timestamp": 1507226300 } { "text": “new", "timestamp": 1507228000 } 視聴開始
  123. Switch Firebase Instance • 複数のFirebase Realtime Databaseをつなぎ かえる場合はSDKのインスタンスを使いまわ せない •

    ⼀一意なkeyと⼀一緒にインスタンス⽣生成する必要 がある • 複数インスタンスの運⽤用については次から
  124. 4. スケーリング

  125. スケールアップしない問題 • Realtime Databaseはスケールアップできない • ⾃自動でスケールアウトすることもない • 事前に負荷を計算し、場合によってはシャー ディング構成を取る必要がある

  126. Single instance   subscribe live ID   

    
  127. インスタンスを複数台⽴立てる • ある程度のtrafficに耐えるため • 負荷の低いインスタンスに順番に配信IDを 振っていく • 垂直分割しても⼤大丈夫なスキーマにしておく

  128. 計算する • Realtime Databaseの⼀一台のスペック • 同時接続10万 / 秒間書き込み1000回 • 実際の負荷がこの30%程度に留留まるよう台数

    を検討する
  129. Sharding subscribe live ID      

  130. イレギュラーを考える • メルカリチャンネルは最初の⼀一ヶ⽉月間、芸能 ⼈人やインフルエンサーの放送があった • 局所的にFirebaseへのアクセスが跳ねること が予想できた 通常のお客さまがその影響を受けないよう 常⽤用インスタンスと⾼高Traffic専⽤用インスタンスを分けた

  131. ⾼高traffic⽤用インスタンス

  132. ⾼高traffic⽤用インスタンス 通常インスタンス ⾼高traffic⽤用 インスタンス

  133. ⾼高traffic⽤用インスタンス 通常インスタンス ⾼高traffic⽤用 インスタンス      live

    ID
  134. ⾼高traffic⽤用インスタンス      live ID

  135. ⾼高traffic⽤用インスタンス      live ID

  136. ⾼高traffic⽤用インスタンス      live ID

  137. ⾼高traffic⽤用インスタンス      live ID

  138. ⾼高traffic⽤用インスタンス      live ID

  139. 料料⾦金金体系 • Firebase Realtime Databaseは従量量課⾦金金 • 何台インスタンスを追加してもTrafficが無けれ ばタダ

  140. インスタンス管理理 • Firebase ProjectとGCP Projectは必ず1対1 • ただしGCP ProjectをGUI以外で作る⽅方法がな い… •

    Project作成、Rule設定、Auth情報ダウンロー ド等全てマニュアルでやらなければいけない
  141. Realtime Databaseを 導⼊入してみて

  142. リリース後の様⼦子 • キャンペーン期間の芸能⼈人による配信の際にも問題な く稼働した • Latencyが少ない • ⼈人の感覚ではほぼ同時 • 相当なコメント量量も問題なく捌いている

    • ボタン1つで10万接続耐えるインスタンスが作れるの は便便利利
  143. 課題点 • スケールアウトがめんどくさい • GUI以外でのFirebaseプロジェクトの追加⽅方法 が無い • 現状スケールアウト = マニュアル操作

    • 10万を越える接続に対応できない • 本番QAが⾯面倒くさい
  144. 余談: Cloud Firestore

  145. Cloud Firestoreで変わった点 • ⾃自動スケールアウトするようになった • SDKのI/Fがよくなった • いわゆるQueryが使える • Latencyが伸びる可能性がある

    • 肌感覚的には誤差とのこと(要検証)
  146. Realtime Database vs Cloud Firestore • 新規プロダクトは基本Cloud Firestoreでよい • Cloud

    FirestoreはRealtime Databaseの課題 を解決する形で実装されている
  147. Realtime Database vs Cloud Firestore • ただし以下の場合はRealtime Databaseを検討 すべし •

    Read/Writeオペレーションが⼤大量量に発⽣生する • 10milli sec単位でもLatencyを速くしたい
  148. Firebase Dev Summit • オランダで開催されるFirebaseのカンファレ ンス • Cloud Firestoreに関する発表が聞けそう

  149. 最後に

  150. 何がよかったか • 守りたいものを守りながら実装をできたこと • ライブ感というユーザ体験 • シンプルなアーキテクチャ • これらを捨てずに短期開発リリースを実現

  151. Thank you