Angular+Firebaseで気になるレシピをストックできるサービスを作った

# TL;DR
* 転職決意を期にWebサービスを作成し、リリースした
* Angular+Firebaseは良い
* お仕事(フロントエンド)、探してます

# はじめに
兼ねてからWebエンジニアにクラスチェンジをしたいと思っていましたが、現役エンジニアの方から「サービスを一つくらい作れないとちょっとね・・・」とアドバイスを頂いたので、何かしらサービスを作ることを決意。
アドバイスをいただいてから2年、サービス作成開始から8ヶ月程度かかりましたが、7/8にリリースまでたどり着きました。
その振り返りを本記事にまとめます。

# 作ったサービスについて
>Recipe LogはWebで見つけたレシピを保存し、メモや感想と共に残すサービスです 
>あなたのお気に入りのレシピを、自分なりのアレンジメモと共に残しませんか？
https://recipeloglog.firebaseapp.com

レシピページのURLを保存し、いつ作るか/どんな感想を持ったかをストックするサービスを作りました。
Webからとってきたレシピで料理をした人ならば誰しも”失敗したな”という経験があるかと思いますが、私の場合、だいたい下記の点で失敗したことがあります。

* レシピ通りに作ったのにいまいち美味しくない
* レシピにちょっとアレンジしたら好評だったが、アレンジ内容を忘れた
* 以前に使ったレシピをもう一度使おうとしたが、どのレシピだったか忘れた

なので、Web上のレシピを保存し、いつ作ったかと感想を残しておくサービスを作ることにしました。
既存サービスで実現できると思いますが、サービスを作ることに意義があるとしてこの点は目を瞑ることにしました。

# アプリケーション構成
![recipelog.png](https://qiita-image-store.s3.amazonaws.com/0/246528/c1563ae8-7c22-6493-ba2b-4ca138414f5a.png)

# 使用した技術
なるべく早くリリースまで漕ぎ着けたかったので、サービスで頼れる部分は頼っていく方針に。
つまり、バックエンドはFirebaseに丸投げしてコーディングはアプリのみに専念します。

## バックエンド
* Firebase Authentication(ユーザ管理)
  + コンソールからクリックするだけでソーシャルログインとメールアドレスログインが可能に。
  + RecipeLogはレシピに対して自分だけのメモを残すので、ソーシャルログインはオフにしました。

* Firebase Realtime Database(データベース管理)
  + RESTでのアクセス方法に癖がなさそうだったので採用。
  + Cloud Firestore+Cloud Functions for Firebaseにしたほうが良かったと後悔。詳細は反省点に記述。
  
* Firebase Cloud Storage(画像データ管理)
  + レシピ画像投稿用に利用。
  
* Firebase Hosting(静的ファイル配信)
  + html,css,jsの配信に利用。
  + SSLが自動で設定されていたり、CDNが効いていたり、至れり尽くせり。
  
## フロントエンド
* Angular6
  
* Angular Material
  
* Angular 6.0+ calendar 

フロントエンドは現職で利用しているAngularに固定。

## その他
* GitHub

* CircleCI

* Trello

* Slack

GitHubにpushしたのちCircleCIでテストを回し、結果をSlackに通知するように構成。
テストに問題なければ、masterブランチへPRを出してmergeしてリリースという流れです。
Trelloには作りたい機能をカードに書き出しておき、少ない作業時間を効率的に利用できるようにしました。

# 工夫点/反省点

## 工夫点
### FirebaseへのアクセスをすべてRESTにした
RecipeLogはSPAなので、バックエンドと責任分解点を明確にしておくべきだと考えました。
AngularでFirebaseを利用する際は、angularfire2( https://github.com/angular/angularfire2 )で直接Firebaseに接続するのが一般的かなと思います。
しかし、この方法ですとFirebaseで予想外にレスポンスが出なかった/金額が高くなったなどでFirebaseの採用を見送られた場合、フロントエンド側に設計変更が入ることになり辛くなります。
これを避けるためには、バックエンドはあくまでRESTでのみアクセス可能とし抽象化を図ります。
Firebaseの各種サービスのRESTでのアクセス方法は公式と下記githubを参考。

* https://github.com/PhantomAppDevelopment/firebase-as3

### なるべくテストを書いた
一人開発をしていると基本的にドキュメントは書きません。仕様は自分が知っているはずですが、昨日の自分は他人なので、コーディング時の考えた仕様はなるべくコード上に表現されているべきです。
angular-cliで構築しているのでテストの雛形は自動で生まれるので、そこに追記していきます。
記載したテストはCircleCIによってリモートレポジトリのpush時に実行されるように構成しました。

予想外としては、テストファイル(*.spec.ts)には実サービスを接続しない、という制約が公式あるため、一つのコンポーネント/サービスを作る度にテスト用のmockを作成する必要があったこと。
そのため、あるサービスを作成した場合はこのような構成になります。

```
shared/apis/recipe
├── recipe.service.mock.ts   // 他のサービスがテストでDIするためのスタブ
├── recipe.service.spec.ts   // テスト
├── recipe.service.ts        // 本体
└── response.ts              //　　このサービスのテストのために、APIのレスポンスを定義
```

また、これに加えてAPIのレスポンスのmock、各種モデルのmockが必要となり、コード量がかなり増加しました。
こういう方針でよいのか、アドバイスいただきたいです。。。

## 反省点
### Cloud Functions for Firebaseを使わなかった
前章で、Firebase Realtime DatabaseはRESTでアクセスできるのをメリットにあげましたが、いくつかのポイントできついかなと言う点がありました。

* レスポンスが配列にならない
Realtime Databaseはデータが生成された際、配列にならずユニークID付きの子要素となります。 

```JSON
"recipes":{
    "-LBRGT_3s0y8_txW8SnS" : {
        "date" : "2018/01/01 14:45",
        "description" : "出汁は2倍で作るとうまい。毎年年末に作る",
        "timeline" : {
            "-LCsnqG9PeNh7OSE-xfA" : {
                "selected_date" : "2018/05/19"
            },
            "-LCsoEg4n1GFtOrroXHb" : {
                "selected_date" : "2018/05/27"
            },
            "-LG0Z3xOkRaySWmCspBC" : {
                "selected_date" : "2018/06/28"
            }
        },
        "title" : "年末用の鴨南蛮"
    },
    "-LBRH3lXabv8vhAkkKpL" : {
        "date" : "2018/03/20 14:45",
        "description" : "玉ねぎを入れるなら事前にちゃんと火を通す。",
        "timeline" : {
            "-LCsoNV2B8cNeqGxPttn" : {
                "selected_date" : "2018/05/27"
            }
        },
        "title" : "円盤餃子"
    }
}
```

上記のデータはRecipeLogの開発環境から取得したRealtime Databaseのデータの一部です。
"recipes"以下にユーザが登録したレシピ情報が格納されており、上の例では"年末用の鴨南蛮"と"円盤餃子"がデータとして登録されています。
GETで取得した際も同様のレスポンスが得られますが、Angularとしては、*ngIfのように対象に配列を想定しているため、フロントエンド内でデータ整形が必要となります。
  
* 第二階層のデータのみの取得ができない

あるレシピが作られた日をtimelineとしてかく"recipes"の下の階層に持っています。

```JSON
"recipes":{
    "-LBRGT_3s0y8_txW8SnS" : {
        "date" : "2018/01/01 14:45",
        "description" : "出汁は2倍で作るとうまい。毎年年末に作る",
        "timeline" : {
            "-LCsnqG9PeNh7OSE-xfA" : {
                "selected_date" : "2018/05/19"
            },
            "-LCsoEg4n1GFtOrroXHb" : {
                "selected_date" : "2018/05/27"
            },
            "-LG0Z3xOkRaySWmCspBC" : {
                "selected_date" : "2018/06/28"
            }
        },
        "title" : "年末用の鴨南蛮"
    },
    "-LBRH3lXabv8vhAkkKpL" : {
        "date" : "2018/03/20 14:45",
        "description" : "玉ねぎを入れるなら事前にちゃんと火を通す。",
        "timeline" : {
            "-LCsoNV2B8cNeqGxPttn" : {
                "selected_date" : "2018/05/27"
            }
        },
        "title" : "円盤餃子"
    }
}
```

ここであるユーザが今まで設定した"timeline"一覧を取得しようとした場合、各recipeのidとtimelineのみがレスポンスとして欲しいデータとなります。
つまり下記が期待するレスポンスとなります。

```JSON
"recipes" : {
        "-LBRGT_3s0y8_txW8SnS" : {
          "timeline" : {
            "-LCsnqG9PeNh7OSE-xfA" : {
              "selected_date" : "2018/05/19"
            },
            "-LCsoEg4n1GFtOrroXHb" : {
              "selected_date" : "2018/05/27"
            },
            "-LG0Z3xOkRaySWmCspBC" : {
              "selected_date" : "2018/06/28"
            }
          },
          "title" : "年末用の鴨南蛮"
        },
        "-LBRH3lXabv8vhAkkKpL" : {
          "timeline" : {
            "-LCsoNV2B8cNeqGxPttn" : {
              "selected_date" : "2018/05/27"
            }
          },
          "title" : "円盤餃子"
        }
}
```

配列にすることが可能であるなら、下記の様なレスポンスが処理しやすい形です。

```JSON
"timeline":[
    { 
        "recipe_id":"-LBRGT_3s0y8_txW8SnS",
        "selected_date":"2018/05/19",
        "timeline_id":"-LCsnqG9PeNh7OSE-xfA",
        "title":"年末用の鴨南蛮"
    },
    {
        "recipe_id":"-LBRGT_3s0y8_txW8SnS",
        "selected_date":"2018/05/27",
        "timeline_id":"-LCsoEg4n1GFtOrroXHb",
        "title":"年末用の鴨南蛮"
    },
    {
        "recipe_id":"-LBRH3lXabv8vhAkkKpL",
        "selected_date":"2018/05/27",
        "timeline_id":"-LCsoNV2B8cNeqGxPttn",
        "title":"円盤餃子"
    }
]
```

しかしRealtime Databaseから直接とる場合、"date"など他の要素も同時に取得します。
理想的なデータ構造にするためにはやはり前項と同じようにデータ整形を行う処理をフロントエンドに持つ必要があります。

上記のような点は本来APIが処理すべき点で、フロントエンドに処理があるのは筋悪です。
フロントエンドとRealtime Databaseの間にCloud Functionsをはさみ、データ整形処理はここに実装するべきでした。

### 利用者目線のデザインを考えなかった
レシピに他人と共有しない、自分用のメモを残すというサービスのため、アカウントを作らないとサービスの中身に触れられません。
そのためログイン画面の外側はすかすかになってしまって、サービス自体の魅力が欠けています。また、アカウント作成後も自分でレシピを登録するまでは画面が空っぽなでさみしい。
レシピをこのサービス内で検索できるなど、工夫が必要だったかなと思います。

# 最後に
反省点が多くなりましたが、当初考えていた機能を作りきることができて安心しています。
これで少しでもプロのエンジニアに近づくことができていれば嬉しいです。
また、アドバイスをいただいた2年前のビギナー時点からここまでの学習の軌跡は別記事にまとめようと思います。
お読みいただきありがとうございました。

作成したコード一覧はGitHubにまとめています。
https://github.com/lilik2229/recipelog

Webエンジニア(フロントエンド)で転職を希望しています。
ご興味を持たれた企業の方は https://twitter.com/crewfanq へご連絡をお願いします。