Angular と Firebase で月間PV1億超えの PWA を作った話

# はじめに

これは [Angular Advent Calendar 2019](https://qiita.com/advent-calendar/2019/angular) 10日目の記事です。

こんにちは (｡･ω･｡)
Angular と Firebase で CGM サービス（一般ユーザー投稿型サービス）を開発している者です。
早いもので、Angular Advent Calendar も 3 回目の参加となります。

例年ではなんとなく不吉な 4 日目をいただいていたのですが、今年も募集当日にエントリーしに行くと既に 9 割ほどの枠で参加表明があり、狙っていた？ 4 日目も埋まっていました。
（どこでも良いので他の人が嫌がる確率が高そうなところに入ったろの精神
今まで #1 しかなかったカレンダーが今年は勢いそのまま [#2](https://qiita.com/advent-calendar/2019/angular-2) まで誕生し、ここ 1 年での Angular コミュニティの成長を実感しています (ﾉﾟ∀ﾟ)ﾉ

小話はこの辺で...

---

この記事では、約 2 年間 Angular/Firebase と共に開発・運営してきた PWA/SPA サービスのリアルをお伝えしたい思います。
また、Angular 以外で SPA に取り組んでいる方でも読み進められるように大きく分けて

- [#サービス について](https://qiita.com/MasanobuAkiba/private/e1c656b6a9fc7989a537#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [#PWA について](https://qiita.com/MasanobuAkiba/private/e1c656b6a9fc7989a537#pwa-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [#Firebase について](https://qiita.com/MasanobuAkiba/private/e1c656b6a9fc7989a537#firebase-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)
- [#Angular について](https://qiita.com/MasanobuAkiba/private/e1c656b6a9fc7989a537#angular-%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)

の 4 部構成となっています。
なるべく他の章の知識が混在しないようにまとめてあるので、Angular を利用していない方も自分の気になる章だけ読んでいただければ幸いです。

# #サービスについて

## サービスの規模感

今回例に出しているサービスは月間PV 1.3 億弱で、約 2 年前にリリースしたものになります。

立ち上げ当初は新卒2年目のデザイナー及び自分の計 2 人でデザインや仕様を考えリリースしていきました。
その後 1 年経過して4,000 万PV。そして 2 年目には 1.3 億PVにまで成長し、当初は 2 人だったチームメンバーも今では

エンジニア: 1 + 0.3 * 3（他プロジェクト掛け持ちのアプリエンジニア
デザイナー: 2
ディレクター: 4
カスタマーサポート: 1

と、人数でいえば 11 人の大所帯となっています。

※ [去年の記録](https://qiita.com/MasanobuAkiba/items/7adcfd5050150ac9ba36#%E3%81%93%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E8%A6%8F%E6%A8%A1%E6%84%9F)

## リリースしてきたもの

Web: PWA
iOS: PWA → TWA → ガワネイティブ
Android: PWA → ガワネイティブ

リソースが限られているため、最初は PWA で全プラットフォームをカバーしていましたが徐々にネイティブに寄せている最中です。

# #PWA について

## PWA を取り巻く環境の確認

> プログレッシブWebアプリケーションは、Webを通じて配信されるアプリケーションソフトウェアの一種で、HTML、CSS、JavaScriptなどの一般的なWebテクノロジーを使用して構築されます。これらは、標準に準拠したブラウザを使用するプラットフォームで動作することを目的としています。

参照： https://en.wikipedia.org/wiki/Progressive_web_application

PWA の定義は 2 年程前までは上記のように曖昧で、人によって境界がまちまちだったような気がしています。しかし現在では [lighthouse](https://developers.google.com/web/tools/lighthouse/audits/address-bar?hl=ja) にて明確な項目が記載されていますので、これらの判定をクリアできれば PWA ということになるのではないでしょうか。

簡単に言うと Web サイトがネイティブアプリのようにインストールできて、そのまま同じ UX で使えること。
PC やスマホのホーム画面に置けて、起動が早くて、PUSH 通知を受け取れる。
そんな感じだと思います。

Web Share や Payment 等 API 周りも年々充実してきており、よほど凝ったものでなければネイティブとの差もつきにくくなってきている PWA ですが、大きな問題がいまだに解消されていません...
この記事を読んでいる方には言うまでもないかもしれませんが、iOS/Safari の開発進捗が悪すぎることですね...
現時点での最新版 13 でもほとんどの機能が実装されていません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/f1a4cd31-5f30-8c7c-5c7f-f228b4aa0b38.png" width="200">

参照： https://tomayac.github.io/pwa-feature-detector/

特に問題なのは PWA の目玉機能である PUSH 通知が使えないことでしょう。
PUSH 通知はサービスの継続率向上に欠かせないファクターであり、いまだに日本で 50% を占める iOS のシェアを無視することはできません。

個人的には後の節で説明する問題と関係の無いプロジェクトであれば

Web: PWA
Android: PWA（TWA
iOS: ガワネイティブ（ほぼ WebView のアプリ

が現代のプロジェクトの最小構成になると考えています。

## PWA ではお金が稼ぎにくい

自分かこの 2 年で一番痛感した問題点はこれです。
前節で挙げた「iOS の PUSH 通知非対応」はもちろん痛いのですが、サービスというものはユーザーを幸福にし、その結果として利益を得るために存在しているため、最大の目的である収益を上げられなければお話になりません。

この問題はサービスの収益モデルに依存するのですが、広告商材で収益をあげている・あげる予定のプロジェクトは PWA と相性が悪い可能性があります。

その主な理由は、「PWA に対応した広告商材が少ない/アプリに比べて Web の広告単価が低い」からです。

これは PWA が本格的に普及していないことも原因の一つだと思うので、今後 PWA がさらに普及していけば解消する可能性はあります。
しかし、現時点ではネイティブアプリ側に圧倒的な商材数、数倍の広告単価がある...という現実があります。
Web 側の単価が低くアプリ側が高い要因はさまざまですが、いくつか挙げてみると

- Web 側の広告は MPA（マルチページアプリ）を前提にしている
- Web 側では広告の表示を最適化できない（MPA前提の規約上バックグラウンドで読み込んでおけない
    - それに加えて、SPA のページ遷移が早すぎて広告が表示される前に通過してしまう
- アプリ側にはインタースティシャル広告やリワード広告といった単価の高い広告商材が豊富に存在する


※ あくまでも傾向であり例外もあります。

そういった理由から広告モデルで収益をあげるサービスは Android であっても、最終的には PWA ではなくネイティブアプリが必要となってくるでしょう。
エンジニアとしては自分の開発した箇所と全く関係のない部分で PWA が通用しないことを知って非常にショックでしたが、これがリアルな現状なので受け入れる他ありませんでした。
時間が解決してくれることを待ちましょう...


## ネイティブアプリ並みの継続率やPV

■ Android アプリ平均継続率

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/640aa677-3bfd-51d3-d1f6-96efe538770f.png" width="600">

参照： https://andrewchen.co/new-data-shows-why-losing-80-of-your-mobile-users-is-normal-and-that-the-best-apps-do-much-better/

成功しているアプリの継続率が見つからなかったので、全 Android アプリの平均継続率を添付しましたが、1 日目が 20 代後半、7 日目が 15% 弱に見えます。

■ 担当アプリの PWA 継続率

<img width="600" alt="スクリーンショット 2019-12-07 19.43.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/33c15f01-8ca7-b790-2d86-424c37134299.png">


一方で、PWA で配信している担当サービスの継続率は、1 日目で 55%、7 日目でも 35% 程度を維持しています。
昔、別のプロジェクトを担当していた際に社内外に顔が広くてやり手のディレクターと同じチームになったことがありました。
そのディレクターが言うには、成功するアプリの最低条件は、「1 日目継続率 35% 以上、7 日目時点で 20%」以上らしいです。
改めて見てみると、たしかにそのラインは超えていますね。

しかし、ネイティブでつくればもっと数値が出たのではないか？
と思う方もいるでしょう。
たしかにその可能性はありますが、収益問題のために Android もガワネイティブ化して Play Store からリリースしてみた結果、継続率に差はみられませんでした。
この結果から言えることは、少なくとも PWA だからといってユーザー体験を大きく損ねることは無いということです。

ただしこれは担当サービスの話ですので、媒体によって結果が異なる可能性は大いにあると思います。ご留意ください。

## PWA の実装は iOS/Safari との戦い

PWA との戦い、それすなわち Safari との戦いです。
Safari でネイティブアプリのような UI/UX を再現しようとすると、多くのバグや謎仕様と戦うことになります。


### CSS のバグ・仕様

Safari での CSS バグは多々ありますが、その中でも影響度が高くヘイトを集めているのは `overflow scroll` 系でしょう。

まず、Safari では `overflow: scroll` を指定した要素内のスクロールがカクつくようになります。
表現がとても難しいですが UX 的に無視できないレベルでカクつきます。
一応そのバグ自体は`-webkit-overflow-scrolling: touch` を指定することで解消できるのですが、そうすると別のバグが発生します。
続いてのバグは、過剰スクロール分の描画が背景色で塗りつぶされるバグです。
（自分でも 何を言っているのかわかりません...

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/93e9e7c0-87ff-92f6-2e43-a145bcad312e.png" width="400">

参照: https://qiita.com/n4o847/items/c491c040f40809325394

これは流石に言葉での説明が難しいので画像をお借りしました。
そしてこの記事で紹介されている HTML/CSS を組めば、このバグは直ります。
しかし、その代わりに別のバグが発生します^q^

新たに発生するのは、スクロール領域の最上部、または最下部でスクロール方向を反転させる際に、スクロールが数秒フリーズするバグです。
このバグはおそらくですが、iOS の bounce scroll が原因で発生しているものと思われます。そのため、 touchmove が発生した際に、`position: 0` より上へ移動しようとしていたり、ページの高さよりも下へ行こうとしているイベントをキャンセルし、そもそも bounce scroll が発生しないような状態にすることで見た目上は解決しました。

が、普段 bounce scroll に慣れている iOS ユーザーはスクロールの挙動に違和感を覚えるかもしれません...

参照した記事にも書いてありましたが、こういった一連のバグ解消には Google などの一流企業も手を焼いていそうで、`overflow scroll` が必要そうなサービスのスクロールは一部 JavaScrip によって再現されています。

結局我々人類は Safari には勝てないのかもしれません。
今できるのは、どこで妥協するかという判断だけです。

### Javascript のバグ

Safari では、特定条件下で Window.alert すらまともに動きません。

参照: 

- [iPhoneでページ内リンクからブラウザバックした後にalertが動かない現象](https://qiita.com/nishimura/items/15846252cfaf527d65a3)
- [IOS Safari window.alert, window.confirm not working after onpopstate triggered](https://stackoverflow.com/questions/58022927/ios-safari-window-alert-window-confirm-not-working-after-onpopstate-triggered)


このバグはブラウザバックした際に alert 系関数が動作しなくなる…というものです。
confirm や prompt 等、ブラウザがダイアログを表示するタイプの関数全てが対象となります。
回避策としては、alert 系関数を使用しない以外になさそうだったので、alert や confirm の代わりに独自ダイアログを使用しています。

これの厄介なところは発生に気がつきにくいというところです。
なんせ開発環境は大抵が PC/Chrome であり、Safari で実機チェックする際もブラウザバックで行ったり来たりまではしません。

このバグを見つけることができた経緯はユーザーから多数のお問い合わせがあったからです。
特定の機能が稀に動かなくなるというお問い合わせが、とある時期から iOS ユーザーからのみ出始めたのです。
自分は Android ユーザーなので何かバグがありそうな気配がするとすぐに自分の端末でチェックするのですが、このケースでは報告が iOS に偏っていたため iOS 実機を借りてモンキーテストしてみることにしました。
すると確かにその機能が動かなくなるタイミングはあり、その後もしばらく続けていく中でブラウザバックをした際にだけ発生するということに気が付きました。
その後はどの行で問題が発生しているのかを調べるため、Safari の使いづらい開発者ツールで検証していき、Window.confirm が全く反応していないことに気が付きました。


...という、非常に面倒な事象に巻き込まれますので、特に SPA では Window.alert 系の関数を利用しない方が良いでしょう。



### Universal リンクの仕様

Universal リンクは、アプリをインストールしている場合に Web 上でクリックしたリンクに対応するアプリスキームに遷移できる仕組みです。
例えば話題になっているネタ動画等を YouTube/Web でチラっと見ようと思っているのに、わざわざアプリを強制起動してくるアレのことですね。個人的には Web で検索している場合はちょっと見て帰りたい場合が多く、この仕組を便利だと思ったことはありません...

個人の感想はさておきこれの何が問題かと言うと、アプリをインストールしている場合に表示されるバナーが HTML 構造を破壊してくるのです。
前節で説明した iOS のスクロールバグを回避するために調整した HTML が台無しです。

<img width="379" alt="スクリーンショット 2019-12-08 20.26.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/8cffee59-9783-32a2-6519-e6688b50e551.png">

こういうのですね。
このバナーは HTML から操作できないにもかかわらず、さも HTML 上に存在するかのような挙動を見せます。
高さを 100% で固定しているところに数 10px の異物が後から紛れ込むのです。
しかも Web からは閲覧ユーザーがアプリをインストールしているか正攻法では確認のしようがないため、
バナーが出ている場合に padding 等を調整するといった回避策も難しい...

調べたところ、このバナーを非表示とするオプションは存在しないそうでした。
担当サービスはガワネイティブであり アプリに遷移させることでの UX 向上は見込めないため、結局 Universal リンクはやめることにしました。
ここは各サービスの判断があると思います。

### TWA を導入してみて

> Trusted Web Activity（TWA）は、Android アプリ内でブラウザの UI がない Chrome ブラウザを全画面表示します。型通りに Chrome Custom Tab（CCT）や WebView を使えば Android アプリにウェブ コンテンツを含めることができますが、アプリ内で全画面モードで Chrome のパフォーマンスや機能を活用したい場合は、TWA を使うと他にはないメリットを得ることができます。

参照: https://developers-jp.googleblog.com/2019/03/trusted-web-activity.html


TWA は簡単に言うと PWA から変換した Chrome をコアとしたネイティブアプリです。
Chrome を間借りしているのでアプリ自体の容量が小さく、TWA 用には開発が一切必要ない点が特徴です。
PWA さえ用意しておけば、[変換ツール](https://appmaker.xyz/pwa-to-apk/) 利用して TWA に変換し、すぐに Play Store へ公開できます。
（細かく言うと加えてサーバーに `.well-known/assetlinks.json` をアップロードする必要があります

これの良いところはなんと言っても ほとんど実装コストをかけずに Play Store へのリリースができることです。
iOS のネイティブ化は iOS サイドの PWA で対応できていなかった PUSH 周りの調整やリジェクト対応を行った関係上 1 人月程度かかっていた覚えがありますが、TWA は 1 日もあればリリースまでの準備ができると思います。
本当にリソースがない中でサービスの開発をしているため、この仕組は非常にありがたいものでした。

ところで、Android の PWA はガワネイティブと機能差はありませんが、手間がかからないと言ってもわざわざ TWA 化してストアに登録する必要はあるのでしょうか？
個人的にはまだ対応する必要があると感じています。
その理由として、一般的には PWA という概念はまだまだ浸透しておらず Chrome で表示される PWA のインストール prompt を広告のようなものだと思い無意識的に ✗ を押してしまう人が一定数存在します。
大多数のユーザーにとってアプリはストアからインストールするものであり、Web からインストールするという発想には至りません。
アプリをストアで検索する層も一定数存在する以上、PWA で完結できると言っても TWA 化してストアから提供するルートも必要です。

しかし、担当サービスでは最終的に TWA をやめてガワネイティブアプリに移行しました。
これは TWA のままだと拡張性が非常に低く広告の SDK を導入できなかったためです。
TWA のままでは前節で述べたお金が稼げない状態なので、広告モデルのサービスは最終的にはネイティブアプリへ移行する必要があるでしょう。
一方で、そういったしがらみと関係のないプロジェクトにとっては素晴らしい技術だと思います。

## PWA の SEO

PWA（SPA） の SEO については[去年の記事に詳しく書いた](https://qiita.com/MasanobuAkiba/items/7adcfd5050150ac9ba36#%E3%81%93%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E8%A6%8F%E6%A8%A1%E6%84%9F)ので、今回は概要だけ記載します。

- Google Bot は JavaScript を実行して動的なコンテンツを解釈し正常にインデックスされる
    - Fetch as Google でもちゃんとレンダリングしている
    - でも、JavaScript 部分は一旦キューに回されて後回しにされる
    - つまり迅速に反映させたいコンテンツには SSR が必要
    - JavaScript の実行が遅いと途中で打ち切られていそう
- Twitter や facebook 等の SNS 系 Bot は JavaScript を実行しないため、最低でもメタタグ部分は SSR/ダイナミックレンダリング する必要がある

要点はだいたいこんな感じでしょうか...

アップデート分の情報としては、1 年前の Google Bot は Chrome 41 程度のレンダリングエンジンだったのですが、半年ほど前のアップデートで常に最新の Chrome に準拠した性能となったことです。
従来ですと旧ブラウザ向けのポリフィルが必要となっていたのですが、そういった面倒事にとらわれずに素直に対象ユーザーの環境だけを意識すれば良くなりました。


## ガワネイティブアプリの App ストア審査

既に何度か出てきていますが、ガワネイティブアプリとはほぼ WebView で構成されたネイティブアプリの俗称です。

これまで弊社に蓄積されてきた経験上、ただ単に WebView を置いただけのガワネイティブアプリはストアが求めている UX に満たないためリジェクトされるとされてきました。
最低限グローバルメニューとそれに対応する遷移部分はネイティブで実装しないと審査を通過できなかったと聞きました。

しかし今回は従来と異なり WebView に表示する部分が PWA のため「とりあえずダメ元で申請してみるか」という流れになったので申請してみたところ、なんとそのまま通過できてしまいました。
人によって判断基準がブレブレで有名な App ストアの審査ですが、現在までのところこれを理由にしたリジェクトはありません。

つまり、App ストアは実装方法ではなく結果としての UX をチェックしているのであり、SPA できちんと Ripple アニメーション等を施したグローバルメニューを実装していれば HTML/CSS/JS で組まれていようと問題ないということです。

SPA ＋ ガワネイティブでストア進出を狙っているプロジェクトは、今からグローバルメニューのアニメーションをこだわって作っておくと良いかもしれません。


## PWA の Push 通知周り

PWA で最大の売りと言っても良い WebPush ですが、愚直に利用すると最悪の UX を生み出してしまいます。

<img width="335" alt="スクリーンショット 2019-12-10 1.18.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/446042ab-9010-ac29-6aaf-5334f805e8e7.png">


ニュースサイト等に足を踏み入れた直後に表示されるコレですね。
WebPush 周りの実装を行っている自分ですら煩わしいと感じるので、一般の方々のフラストレーションは相当なものでしょう。

あまりに批判の声が多かったのか Firefox ではついに許諾ポップアップが表示されなくなってしまいました｡･ﾟ･(ﾉД`)･ﾟ･｡


参照: [非常に不人気な「Web Push通知」、Firefox 72以降ではポップアップを表示せずURLバーのアイコンで通知するのみに](https://it.srad.jp/story/19/11/11/1649205/)

ではどうすれば良いのかと言うと、よく挙げられるのは一度利点を説明するモーダルを表示してから許諾を取るパターンです。

![無題の図形描画 (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/2591e9b5-8dac-67d0-6e04-de14c1c7f6fa.png)

会員登録後のマイページなど、適切な対象とタイミングでメリットを明示した上で改めてブラウザの許諾ポップアップで承諾してもらう。
このフローを挟むことで、デフォルトの許諾 UI では承諾率 1% と言われているところを、20% 以上に引き上げることができます。

しかし、このフローは更に改善ができます。
仮説となってしまうのですが、モーダルが急に表示されるとびっくりする事があると思うんです。
その場合表示されている内容をよく理解せずに OK を押してしまい、その先の許諾ポップアップが表示されたところで冷静さを取り戻し「PUSH うざいからキャンセルー」というケースが一定数あるのではないかと考えています。
新しくインストールしたアプリのチュートリアルなどをよく読むタイプの人にはわからないかもしれませんが、そういった説明を読まない層は自分も含めてある程度存在します。

ここで問題となるのが、ブラウザ側の許諾ポップアップでリジェクトを選択するとそれを復帰させるのが非常に困難だということです。

<img width="317" alt="スクリーンショット 2019-12-09 16.11.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/61046cf3-2b9d-2bf1-7633-d4da6c200da3.png">

一度ブロックされた WebPush は URL バー左の 🔒アイコンをタップした先で解除することができますが、
おそらく大多数のユーザーはこの事実を知らないでしょう。
一度無自覚でリジェクト状態に陥ってしまうとその後復帰できるケースはほとんどないと思います。
更に残念なことに PWA をホームにインストールしている場合は URL バーが存在しないため、解除のために Web 側に戻らないといけません。
将来的にはネイティブアプリと同様に AndroidOS の設定画面から変更できるようになるかもしれませんが、現時点でそうはなっていないようです。（もしかすると、機種によって異なるとかはあるのかもしれません

つまり、許諾させるのと同じくらい `許諾する意思のない人をブラウザのポップアップまで進めない` ことが大事だと考えています。

それを可能にするのがバナー型許諾 UI です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/170f9926-02e4-4d59-4759-0f37faf6bbb4.png" width="300">


Twitter 等のサービスでも採用されている形式なので見かけたことがある人も多いと思いまが、このパターンはサービス側が求めている動作をユーザーが任意のタイミングで実行できる点で優れています。
実際に担当サービスでも導入してみましたが、リジェクトされる割合がモーダル型の 1/3 程度に激減しました。
最終的に許可した数は同程度だったのですが、ほぼ復帰不能なリジェクト状態を回避できると考えると PWA の許諾 UI はバナー型（設置型 UI）が最適だと考えています。


## PWA のまとめ

- PWA は開発リソース弱者の技術
    - 特に iOS で実現が難しい UI や機能が多いので iOS はネイティブが望ましい
- TWA はとりあえずストアにリリースする技術としては非常に優れている
- APP ストアの審査は、PWA をそのまま表示するガワネイティブアプリでも通過できる
- SPA でも SEO は大丈夫だけど反映が遅い
- PUSH の許諾は慎重に行うべき

# #Firebase について

## なぜ Firebase を採用したのか

このプロジェクト開始時、というか最近まで配属されているエンジニアはインフラからフロントエンドまで含めて自分 1 人だけでした。
この限られたリソースの中でサービスを最大限成長させていくためには、サービスの主機能に関係のない部分を何らかの BaaS に委譲する必要がありました。

また、直前のプロジェクトで Firebase RealtimeDB を利用していたという経緯もあり API を作成する工数と DB サーバー構築・運用コストをカットできる Firebase RealtimeDB をデータベースとして選定しました。

初期段階では RealtimeDB + Task Queue で運用していたのですが、直後に Firestore がリリースされたため、現在では Firestore + Firebase Functions（trigger/pub-sub）に移行しています。

## Functions の使い方



## Firestore + BigQuery で弱点保険

Firestore の欠点の 1 つは、大量のデータを扱いづらい点です。

数万件程度であればまだなんとかなるのですが、インデックスとして登録されていない情報を 10 万件以上のコレクションから探さなくてはならない場合は抽出用スクリプトを書かないといけませんし、何より実行に時間がかかります。

そんな弱点を BigQuery はすぐさま補完してくれます。

わざわざ書くまでもないかもしれませんが、BigQuery は Firestore と同じ GCP のデータウェアハウス（大量のデータを保存して解析できる場所）です。
[以前書いた記事でも紹介したのですが](https://qiita.com/MasanobuAkiba/items/31d2a9dc06aa78943a4a)、GCP 内のデータは提供されている API を利用すれば非常に簡単に BigQuery へ import することが可能です。
BigQuery へ import さえしてしまえば、後は SQL を記述するだけで容易に大量のデータ処理・集計が可能となります。

BigQuery のおかげで弱点だった大量のデータ処理は逆に「ぐーんと」伸びで長所とさえ言えるでしょう。
Firestore を使うのであれば、もはや BigQuery はセットで考えるべき必需品です。
ほとんどのケースで追加コストも微々たるものだと思うので、まだ利用していない方はぜひ連携してみてください。

## Google Data Portal で任意のデータを簡単に可視化

Firestore のデータを BigQuery に import する恩恵はそれだけに留まりません。
同じく Google が提供している [Data Portal](https://support.google.com/datastudio/answer/6283323?hl=ja)　では、BigQuery のデータをワンタッチで紐付けてグラフ化及び可視化できます。

<img width="600" alt="スクリーンショット 2019-12-09 22.40.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/5f851467-2048-5a05-a378-3051b9350e11.png">

サービスデータの可視化には多種多様な手法があると思いますが、定期実行バッチ等でデータベースから抽出した情報を整形し、スプレッドシート等に挿入、そのデータを利用してグラフ化するパターンが多いのではないでしょうか。
BigQuery + Data Portal を利用すればそういった集計バッチを作成することなく、データポータルの管理画面から誰でも好きなグラフを作成することが可能です。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/df063726-c3d1-db2b-b9c9-42cc52bdfdea.png)


これにより、ディレクターに頼まれがちな「あ！やっぱあのデータも追加で集計しておいて！」も回避できます。
データ集計に関わる不毛なコミュニケーションや開発・運用コストを軒並みカットできるため、BigQuery を利用しているのであれば是非とも使いたいサービスです。

ちなみにちょっと複雑で別途集計が必要な数値も BigQuery に標準で備わっている「指定時間に任意の Query を実行してその結果をテーブルに出力する」機能を使えば、その中間テーブルを介して簡単に出力することができます。

<img width="400" alt="スクリーンショット 2019-12-09 23.05.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/31441fda-147f-367f-aaf2-1b00eb8309e9.png">

## コレクションの命名規則

RealtimeDB 及び Firestore のコレクション名は単数形の名詞に統一しました。
コレは自分が英語が不得意で、可算名詞と不可算名詞を都度考慮するのが面倒だと感じている面が大きいですが、アプリケーション側で定義されるモデル名（interface 名）とコレクション名が一致していた方が余計な考えが発生しないという理由もあります。

ユーザーモデルの定義が

```typescript
interface User {
  name: string;
}
```

の場合に、`app/v1/user` と `app/v1/users` なら前者の方がわかりやすいだろう ということです。

## Firebase Functions について

Firebase Functions で特に重宝しているのは Firestore に更新があった場合に発火できる trigger という種類の関数です。利用例を挙げると User コレクション内に新たなユーザーが作成された際、そのユーザーデータを参照しつつ それに依存するユーザー設定等の新規ドキュメントを生成できます。

これの良いところは、アプリケーション側にユーザー登録に関する処理を記述しなくて済むようになることです。
最初は PWA だけだとしても、先の理由からネイティブアプリも作ることになった場合に各アプリケーションに処理が分散する状態を防げます。
API 作れば良くない？と言われればそれまでですが、認証や[セキュリティルール](https://firebase.google.com/docs/firestore/security/get-started?hl=ja)による権限設定が最初からセットになっているため、自前で API サーバーを建てるよりも早くて安くて安全な疑似 API として利用できます。


使い方は、[公式ドキュメントで説明されています](https://firebase.google.com/docs/functions/firestore-events?hl=ja)が、この定義がいくつもあるとコードの見通しが悪くなってしまいそうです。

```typescript
// Listen for any change on document `marie` in collection `users`
exports.myFunctionName = functions.firestore
    .document('users/marie').onWrite((change, context) => {
      // ... Your code here
    });
```

コレクションのパスや渡ってくるパラメーターは共通なので、個別に定義するよりもコレクション単位でまとめて定義した方が運用しやすいコードになると考えました。
担当サービスでは下記のような独自インターフェースに定義して、その後登録できる形式に変換しています。

```typescript
class CommentTrigger extends TriggerAbstract implements TriggerOnCreate<Comment>, TriggerOnDelete<Comment> {
    static readonly COLLECTION_PATH = 'app/v1/comment';

    @CloudFunctionRunWith({ memory: '1GB', timeoutSeconds: 120 })
    public async onCreate(createdComment: Comment, context: EventContext) {
        ...
    }

    // デフォルト設定
    public async onDelete(deletedComment: Comment, context: EventContext) {
        ...
    }
}
```

## Firebase Extensions

[Firebase Extensions](https://firebase.google.com/products/extensions) はその名の通り Firestore や RealtimeDB の機能を拡張してくれるサービスです。

テキストの翻訳や短縮 URL 生成等さまざまな Extension がありますが、大抵のサービスで活用できそうなのが [Delete User Data](https://firebase.google.com/products/extensions/delete-user-data/) です。
これは Firebase  Authentication で管理しているユーザーが削除された際に発火するもので、削除されたユーザーの ID をキーに持つドキュメントを自動で削除してくれます。


```
app/v1/user/{userId}
app/v1/userSetting/{userId}
app/v1/userFcmToken/{userId}
...
..
.
```

上記のようなユーザーに紐づくデータは UserId をキーにリレーションを行う構造にしておくことで、必ず実装しなければならない上に面倒な「退会機能」のほとんどを委譲することができます。

Firebase Extensions ではこの他にも便利な機能が提供されており、今後も拡充していく予定なので引き続きウォッチしていきたいですね。

## Firebase のまとめ

- Firestore を利用するなら BigQuery は連携するべき
- BigQuery を連携したら Data Portal でグラフ化・可視化する
- 自分で機能を作る前に Extensions で代用できないか考える

# #Angular について

## なぜ Angular を採用したのか

コレを説明するには担当プロジェクト発足時の状況から話さなくてはなりません。

自分は新卒入社した会社で 3 年弱 PHP でバックエンドを担当していました。思い返すとアプリ向けの API を設計開発していた時期が長かったです。

しかし私はもともとフロントエンドが好きだったため、View 周りを担当できるスキがあらば いの一番に立候補し、担当プロジェクトにて React や Vue をスポットで導入したりしていました。
傍から聞くとプロジェクトの負債を増やしているようで聞こえが悪いですが、それらのコードはプロジェクト終了まで責任を持って整備していたので許してください...（現在存在していないことが示す通り、あまり影響のない部分でのみやっていました。

そんなある日、ディレクターから新規サービスを立ち上げたいのでエンジニアを担当してくれないかと頼まれました。
メンバーは新卒 2 年目の新米デザイナーと自分の 2 人だけ。
どう見ても困難な道でしたが、自分が好きな技術を使える魅力が勝りました。

そういった状況の中で Angular に決めた理由は次の 8 つです。

1. **ちょうど Angular 4 がリリースされたタイミングだった**
    2. Angular 4 は Angular 2 に比べてかなり軽量化されていた
3. **当時 Angular だけが Typescript 前提だった**
    4. 当時、Vue はほぼ完全に Typescript 非対応で React もようやく対応し始めたかなぁという段階だったような気がします。自分はケアレスミスが多いため、PHP を書いていた頃は変数名が微妙に違っていたり...みたいなポカを良くしてしまいがちでした。そういったポカを Typescript であれば防いでくれる。そしてこれからは Typescript の時代だと考えていたため、いち早く採用していた Angular に魅力を感じました。
    5. エンジニアが一人しかいないためレビューをしてくれる人もいない状況でした。Typescript 以外を選んだ場合は破滅しか見えません。
5. **SPA の設計に自信も経験も、社内に知見もなかった**
    6. フロントエンドの作業は定期的に担当していたので多少の自信はありましたが、それまでフル SPA を開発・運用した経験はありませんでした。せいぜい自分で小規模の SPA を作ってみて遊んだくらいです。
    7. また、社内の空気がフロントエンドに消極的で頼れる人もほんの一握りでした。
    7. そんな状況では、自分より数倍優秀である Google のエンジニアが作成したレールの上を歩きたかったという背景があります。
8. **RxJS を使えるようになりたかった**
    9. 今までは上から下へと秩序立てて流れていくバックエンドの世界で生きてきたので、リアクティブプログラミングに触れる機会がありませんでした。
    10. 新たな考え方を手に入れることは仮にプロジェクトが失敗しても未来へつながる財産となるため、ぜひ勉強しておきたかったという理由があります。
3. **開発対象が完全新規のアプリケーションだった**
    4. React や Vue はページ単位で導入しやすいですが、Angular は向いてません。しかしこのケースでは関係ありませんでした。
5. **経験の浅いデザイナーと協力しやすそうだった**
    6. Angular のテンプレートは 3 ライブラリ/フレームワークの中で唯一、完全に分割されています。ファイル数が多くて煩わしいという方もいますが、デザイナー視点に立ってみると自分の編集対象だけに集中できます。
    7. 弊社のデザイナーはほとんど JavaScript を書けませんが、HTML/CSS コーディングは担当します。また、PHP 媒体が多いため PHP のテンプレートエンジンには慣れています。Angular のテンプレートはそれと似ているため、抵抗感なく作業できると考えました。
    7. 更には組織構造が歪でエンジニアよりディレクターとデザイナーの方が多くアサインできる状況だったため、テンプレート部分（HTML コーディング）はできるだけデザイナー側に任せたいと考えていました。以上から、弊社のデザイナーがより効率よく作業するには Angular のテンプレートが最良だろうという判断になりました。
7. **React → Vue ときたら次は Angular だろうという考え**
    8. AngularJS ←※ は、フロントエンドエアプの自分の耳にも届いてくるほどさまざまな問題点があるようでした。そのため触るのを敬遠していたのですが、Angular 2, 4 のリリースで問題点を解消したという事を聞き、であれば 3 大フレームワークをコンプリートするためにやってみたいなぁという思いも多少ありました。
9. **[HTML5 Experts の React vs Angular 2ガチ対決！エキスパートたちによるハイレベル対談](https://html5experts.jp/shumpei-shiraishi/21731/) を読んで**
    10. 最後のひと押しはこの記事だったような気がします。知識のない状態では自分で情報を得ようとも、その深部までたどり着くことは難しいです。知らないことは認識できないので問題の尻尾から本体にたどり着くことができません。そのような状態ではやはり有識者の意見にある程度身を任せる必要があると考えています。もちろんそれらは鵜呑みにせず、自分でも考え検証する必要はありますが。
    11. この記事には SPA を構成する骨組みである Router が Angular の優勢であると記載されています。実際自分もそう思いましたし有識者もそう感じているのであれば、その判断に従って良さそうだと確信することができました。

長くなりましたが、以上が SPA のフレームワークとして Angular を選定した経緯となります。
[Angularでの開発を快適に進めるために知っておきたいこと](https://qiita.com/okunokentaro/items/503ab7a4c7601b564de0) でも述べられていますが、結局はそれぞれのチームや環境・時期によってどのライブラリ/フレームワークを利用するかの判断を下さなければなりません。
自分のケースがまさにそれで、さまざまな要因が Angular を最良の選択肢に押し上げました。
これから選定を始める方は盲目的にではなく、ぜひ自分の環境にあったものを選んでみてください。

なお上記例は時期的要因を多く含んでいるため、現在以降参考になるかはわかりません。考え方の一つとして捉えてもらえれば幸いです。

---

ちなににそもそもなんで SPA 前提なのかは **自分がやってみたかった** からです。
当初は既存の PHP プロジェクトをコピーして作るように言われていましたが、「SPA でやっておけば仮にプロジェクトが失敗しても社内に知見が残る」というメリットを提示して説得しきりました...
結果的に成功して良かったと思います。


## NgRx の使い方

NgRx はモデル単位でストアを構成し基本的には Set 系のアクションしか用意していません。
Update 系のアクションはログイン済みのユーザーしか利用せず大半のユーザーには関係がないため、feature ストアとして切り出して対象の画面へ遷移した場合に遅延読み込みするようにしています。Update が正常に完了した場合は NgRx の Effect 経由で更新状態を Set するアクションを呼び出しています。

（後で図を追加予定）

ストアの力が特に発揮されるのは、リスト系画面から詳細系画面へ遷移する際です。
リストで得たモデル情報をストアにセットしつつ詳細画面へ移動することで、詳細画面のメインコンテンツが瞬時に表示され非常に優れた UX が提供できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/44c82668-0829-f71b-f8f8-60f86f4b4121.png" width="600">



## CDK

Angular の周辺ライブラリのひとつに [CDK（Component Dev Kit）](https://material.angular.io/cdk/categories) というものがあります。
これは Angular 公式 UI ライブラリである [Angular Material](https://material.angular.io/) の共通部分を切り出した、UI の見た目ではなく振る舞いに関する処理のツール郡です。そのためスタイルは提供されておらず、完全にカスタマイズされた独自 UI の構築に一役買ってくれます。

PWA の章で述べた通り、一見すると単純な UI でも Safari や一部のマイナーブラウザでは簡単に実装できないかもしれません。
CDK では前述したような泥臭いハック込の振る舞いをパッケージ化して提供してくれるのです。
自分は本当に Safari が恐ろしくてたまらないのですが、CDK のおかげでいくばくかの安寧を取り戻すことができます。

ハックの他にも完璧を目指そうとすると調整が面倒なオーバーレイ系 UI やキーアクションに関するモジュール、ドラッグアンドドロップと多種多様な振る舞いが用意されており、非常に柔軟な独自 UI 構築が可能です。
これ程便利なツールを使わない手はないので、ぜひ使い倒して時間を節約していきましょう。


## アプリのアップデート通知

`@angular/service-worker` で提供されている `SwUpdate` を利用すると、アプリの更新確認処理を簡単に実装することができます。
基本的には[公式ドキュメントに書いてある通り](https://angular.io/guide/service-worker-communications)に実装すれば問題ないのですが、一部手を加えた方が良いと思っているポイントがあります。
それは下図のようにユーザーへアップデート内容を伝えてあげるということです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41793/5aed62c9-3c0d-0525-0df9-d5fcfecc6ff7.png" width="300">

この一手間を行わないとユーザーとしては意味のわからないリロードを要求され、日に日にフラストレーションがたまっていくでしょう。

機能の実装には、任意の情報を伝えることのできる [appData](https://angular.io/guide/service-worker-config#appdata) を利用します。
`ngsw-config.json` には任意のデータを埋め込める `appData` というオプションフィールドが用意されており、ここにアプリの更新内容やバージョンを記載しておくと [UpdateAvailableEvent](https://angular.io/api/service-worker/UpdateAvailableEvent) を介してデータを受け取ることがことができます。
データを受け取ることができれば、後は任意の UI でユーザーへ表示するだけです。

この施策の定量的効果測定は難しく、いまだにどの程度の効果があるのかはわかりません。
しかし定性的には、ユーザーから「このアプリは更新頻度が高くて要望をすぐ反映してくれる」「運営頑張ってるなぁ」といった声が聞こえるようになったため、アプリの継続率を向上させる 1 要因となっているのではないかと考えています。


## Angular のまとめ

- フレームワーク選定は環境によってさまざま
- NgRx の一例紹介
- CDK で独自 UI の構築をアシスト
- NgSw で UX を向上

# おわりに

この 2 年間、自分がサービスやフレームワークの成長と共に学んだ知見を思い出しながらまとめてみました。
まだまだ まとめきれていない部分や詳細を説明できていない項目も多いので、また別の機会にアウトプットできたらなと思います。

思い返すと早いもので、最初は単なるエンジニア、チームメンバーもたった 2 人だったのに対して今では 11 人。自分の役職もいつの間にかエンジニアとプロダクトマネージャーを兼任するようになっていました。

ここまでエンジニア一人で走りきれたのは Firebase というツールと Angular というレールのおかげです。
これらのソリューションが存在していたからこそ、自分はアプリケーションのやりたいことに集中することができ、月間 1 億 PV という大台を達成するに至りました。
コミッター及びコントリビューターの皆様には本当に感謝しています。

なんか全体を通して多少ポエミーな感じになってしまいましたが、ここに記した情報が少しでも誰かの役に立てば幸いです。
長文でしたが最後までお付き合いいただき ありがとうございましたorz


---

[Angular Advent Calendar 2019](https://qiita.com/advent-calendar/2019/angular) 11 日目は @euxn23 さんです。よろしくお願いしますm(__)m