AngularとFirebaseで4ヶ月掛けて作ったポートフォリオ

<img width="914" alt="SS 2020-04-02 14.25.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349873/9cb2ef4c-56bd-8a38-44f2-522ad5ee4502.png">
# はじめに
フロントエンドエンジニアとして就職するために開発したSPAです。
制作期間: 2019年12月〜2020年3月 (約120日)

# 使用技術
- Angular9
- AngularMaterial
- Firebase (Auth, Database, Storage, Functions)
- Algolia
- Stripe
- GitHub Actions

# アプリの概要

### どんなアプリ？
URLがたくさん貼られたNAVERまとめのような記事が、シンプルなUIで作れます。
[![Image from Gyazo](https://i.gyazo.com/c8cc56ce3fdee0491cb4191c9b594ec4.gif)](https://gyazo.com/c8cc56ce3fdee0491cb4191c9b594ec4)

## ページ紹介

### トップページ
ボタンをイラストで飾ることでAngularMaterialのUIキット感を抑えています。
未ログインの場合は、ログインを促すダイアログを表示されます。

（イラストは制作していません 🤦）
[![Image from Gyazo](https://i.gyazo.com/8acb5120a5076263f2a9b63f830750cd.gif)](https://gyazo.com/8acb5120a5076263f2a9b63f830750cd)

### 記事の作成
アイコンボタンでURLの入力欄を増やしたり、減らしたり出来ます。
各記事ページ下部から編集や削除も行えます。

また、入力されたURLから、自作のAPIによってOGPとFaviconを取得します。

[![Image from Gyazo](https://i.gyazo.com/dd785bb653b7cfa1c61be47a0574ff9f.gif)](https://gyazo.com/dd785bb653b7cfa1c61be47a0574ff9f)

### 記事の一覧
「自分の投稿」と「いいねが多い投稿」を一覧表示出来ます。
いずれのページも無限スクロールを用いた追加読み込みで、1度に読み込む記事の数を抑えています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【アプリ開発の進捗】<br><br>無限スクロールを実装しました😎<br>実装方法としては、最下部までスクロールされるたびに配列に記事のデータを追加して、それらを全て表示するイメージ。これで1000件投稿があっても表示出来ますね！<a href="https://twitter.com/hashtag/to_camp?src=hash&amp;ref_src=twsrc%5Etfw">#to_camp</a> 52日目 <a href="https://t.co/F97tEEljkj">pic.twitter.com/F97tEEljkj</a></p>&mdash; お茶マル🍵 (@uotya) <a href="https://twitter.com/uotya/status/1223104252437905409?ref_src=twsrc%5Etfw">January 31, 2020</a></blockquote>

### 記事の検索
Firestoreには全文検索の機能が無いので、[公式ドキュメント](https://firebase.google.com/docs/firestore/solutions/search?hl=ja)でも推奨されている外部サービスの[Algolia](https://www.algolia.com/)と連携することで実装しています。
[![Image from Gyazo](https://i.gyazo.com/1dd7a9c5a5cdcaf617bc880c96a992b7.gif)](https://gyazo.com/1dd7a9c5a5cdcaf617bc880c96a992b7)

### 開発者へ寄付
Stripeを使って課金機能も実装しました。
顧客データをStripe側に保管しているので、2回目以降の決済にはカード情報の登録は不要です。
[![Image from Gyazo](https://i.gyazo.com/198c89b47e95e742c94424fed8b67d89.gif)](https://gyazo.com/198c89b47e95e742c94424fed8b67d89)

## その他の機能

- PWA対応
- TwitterAPIでアバターの自動同期
- アバターの変更 (トリミング含む)
- データベースの定期バックアップ
- ダイナミックレンダリングによるOGP対応
- アカウントの削除

# ディレクトリ構成
この画像だけでは分かりづらいとは思うのですが、ログイン・CRUD・検索・課金を実装したアプリのファイル規模の参考程度にはなるかと思います。

- モジュールはページ単位で作成
- DRY原則に則ってミニマムなコンポーネントを複数箇所で使い回す
- 複数のModuleで重複したインポートをしない

これらのポイントを意識しながら開発しました。
<img width="1343" alt="SS 2020-04-02 16.47.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349873/a19078f1-fe2e-0b1b-e302-b006fa8bbc95.png">

# 苦労した実装
開発に着手したのは12月上旬ですが、当時はHTMLとSassとJavaScript(Progateでかじった程度)の知識しかありませんでした。

AngularとFirebaseをキャッチアップしながら開発したので、もちろん詰まる点ばかりで、しばらくは暗黒の森を彷徨うような気持ちで開発していました。

その中でも、特に苦戦した実装をピックアップしました。

## データベース設計
画像のように、記事のドキュメントの中にユーザー名やアバターのURLを含めてしまうと、名前やアバターが変更された際にそのユーザーが投稿したすべての記事を更新しなければなりません。
<img width="701" alt="SS 2020-04-03 13.06.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349873/b0b02d70-ca41-d108-b263-fdeff97d93e8.png">

Firestoreのリクエスト数を減らすためにも「記事」と「ユーザー」のデータは別々のコレクションで管理することにしました。

保存先を分けることはAngularFireの基本が理解出来ていれば、何ら難しいことではないのですが、別コレクションにある「記事のデータ」と「ユーザーのデータ」を取得して1つのObservableに統合するにはRxJSの解像度の高い理解が必須だったため、苦戦しました。

ちなみにこの実装は、同じように詰まった人のために[こちら](https://qiita.com/otyamaru/items/b5f5bc22d9cef95be43c)の記事で解説しています。

## いいね機能
いいねは想定外の不具合に見舞われたこともあり、実装手順を理解してから、実際に実装が終わるまで1番長かったです。
大まかな処理フローは以下のような感じです。具体的に詰まった点を紹介しておきます。

1. フロントで表示用の変数をカウントアップorダウン
2. Serviceでいいねを押した記事のIDを保存or削除
3. CloudFunctionsで記事データ内のいいね数をカウントアップorダウン

記事データ内にいいね数のフィールドを持たせているのに、フロントでも表示用の変数を使う理由としては、Functionsを伴う処理は数秒のラグが生じるためです。

### べき等処理
FunctionsでFirestoreトリガーを使うと、ごく稀に2回以上発火してしまうことがあり、[公式ドキュメント](https://firebase.google.com/docs/firestore/extend-with-functions?hl=ja#limitations_and_guarantees)でもべき等処理を実装することが推奨されています。

カウントアップorダウンが2回以上発火されるといいね数が正確性が損なわれるので、いいねにはべき等処理を実装しました。

### 一覧表示をする際の不具合
いいね数をフロントの変数で管理するため、記事のデータは常に最新のものをキャッチし続けるのではなく、take(1)で一度だけ取得する実装にしました。
その際にこの手順を踏むと、キャッシュの影響で1記事しか一覧表示されないことがありました。

1. 特定のドキュメントを取得する
2. コレクションを取得して一覧表示する

対処法についてはAngularFireの[Issue](https://github.com/angular/angularfire/issues/2012)で議論されており、以下のようにgetメソッドを用いてサーバーから直接データを取得する方法で対処しました。

```ts
collection('users').get({ source: 'server' });
```

# さいごに
今回のポートフォリオの制作を通じて、数日でCRUDアプリが作れるくらいの自力が付きました。
また、日本語情報の少ないAngularで開発したことによって、英語のドキュメントやIssueに対する抵抗感もさっぱり無くなりました。

挫折せずに最後まで楽しくやり切れたのは、間違いなく付きっきりで教えてくれたメンターの[Ninoさん](https://twitter.com/d151005)のおかげです。

リポジトリのCollaboratorとして、コードレビューもして頂きながら開発しました。

本当にありがとうございました🙇‍♂️