React + Redux + Firebaseで「プログラミング独学ロードマップ」を共有できるサービスを作った。

こんにちは、現在転職活動中のけんぞうです。
先日「`React+Redux＋Firebase`」の技術スタックで、[Devmap | 独学ロードマップ共有サービス](https://devmap.work/)を作ってみました。



<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">誕生日に向けて１週間実装してきたサービスを公開します🎉<br><br>「誰でも独学ロードマップを作成できる」というコンセプトで、僕の独学したきた経験などを詳しく書いてます。<br><br>現在独学中の方は、参考にしたり、書き込んだりしてみてくださいませ〜😊<a href="https://t.co/an6MJNBBLM">https://t.co/an6MJNBBLM</a></p>&mdash; けんぞう@独学の人 (@kenzoooooB) <a href="https://twitter.com/kenzoooooB/status/1138604657138233344?ref_src=twsrc%5Etfw">June 12, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


この記事では、感想などを書きつつ、振り返りをしてみようかと思います。

[コードはこちら](https://github.com/kenzoukenzou/devmap)

## サービスの紹介

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">僕の独学経験が簡潔にまとまったアプリケーションとしての役割を果たしつつ、これから学習する言語のロードマップも作れるので、割と長期的に使用できる。我ながら良いものが作れたのでは。 <a href="https://t.co/a2fsEgQpof">pic.twitter.com/a2fsEgQpof</a></p>&mdash; けんぞう@独学の人 (@kenzoooooB) <a href="https://twitter.com/kenzoooooB/status/1138377387861692416?ref_src=twsrc%5Etfw">June 11, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

上記の動画のようなアプリケーションを作りました。僕自身が転職に向けてプログラミングを独学してきた過程を残しつつ、他の人の独学方法も知れたらいいなーとおもい、実装しました。

具体的には以下の項目が実装済みです。

- Google / Twitter / Facebook認証ログイン
- [Cloudinary](http://cloudinary.com)を用いて動的にアイキャッチ画像を生成
- Cloud Functionsを用いて動的にOGP画像を生成



## モチベーション

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">6/12が誕生日なので、あと１週間でReact＋Firebaseのアプリケーションを完成させて、誕生日のタイミングでリリースします👨‍💻</p>&mdash; けんぞう@独学の人 (@kenzoooooB) <a href="https://twitter.com/kenzoooooB/status/1136453201794818049?ref_src=twsrc%5Etfw">June 6, 2019</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

すごく個人的なことなのですがツイートの通り6/12が誕生日だということに、ちょうど１週間前に気が付きまして、「`せっかくだったら区切りも良いし１週間で何か作ってみるかー`」と腰をあげて、１週間かけてガリガリ実装しました。

Reactでしっかりしたアプリケーションを作るのは今回が初めてだったので、手探りな部分もあったのですが、ひとまず１週間で形になったので一安心です。


## 実装で苦戦したところ

時間がたつと苦戦したところを忘れてしまうので、備忘録として残しておきたいと思います。

### 1.認証ログイン

Firebaseでの認証をアプリケーションに取り入れているのですが、`signInWithPopup`と`signInWithRedirect `のどちらを採用するかで時間がかかってしまいました、、

僕が動かしてみた範囲では、上記の２つには以下のような違いがありました。


- `signInWithRedirect`：認証と同時に`users collection`が自動で生成されて、ユーザー情報が格納される。これが便利なんだけど、**リダイレクトした画面が数秒間真っ白になってしまう問題が発生した。**
- `signInWithPopup`：認証と同時にユーザー情報の保管は行われないが、画面真っ白問題は解消される。


結果的には、「**`signInWithPopupを採用しつつ、users collectionにデータを格納する`**」という実装をおこないました。

```React:Login.js

loginwithTwitter = () => {
    this.setState({
      clicked: !this.state.clicked // Loading Skeleton
    });
    const provider = new firebase.auth.TwitterAuthProvider();
    firebase
      .auth()
      .signInWithPopup(provider)
      .then(response => {
        this.props.loginAuth(response);
      });
  };


```



```authActions.js

// Login with authentification
export const loginAuth = res => {
  return (dispatch, getState, { getFirestore }) => {
    const firestore = getFirestore();
    // const authorId = getState().firebase.auth.uid;
    firestore
      .collection("users")
      .doc(res.user.uid)
      .set({
        // id: res.user.uid,
        displayName: res.user.displayName,
        avatarUrl: res.user.photoURL
      })
      .then(() => {
        dispatch({ type: "LOGIN_SUCCESS" });
      })
      .catch(err => {
        dispatch({ type: "LOGIN_ERROR", err });
      });
  };
};

```


### 2.動的なOGP画像の設定

そもそもSPAを開発すること自体が初だったので、「`クローラがJavascriptを実装しないから、react-helmetなどを使ってメタタグを書き換えても、TwitterCardが切り替わらない`」という事情をしらず、動的なOGP画像設定にかなり時間を食ってしまいました、、

最終的には、[firebaseでサーバレスなSPAアプリを作った話②](https://qiita.com/g0e/items/75429eb805f7879a6fba#cloud-functions%E3%81%A7seo%E7%94%A8%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%88%E3%83%AB%E3%83%A1%E3%82%BF%E3%82%BF%E3%82%B0%E6%8C%BF%E5%85%A5)を参考にさせていただきながら、Cloud Functionsでメタタグを挿入するという方法を採用しました。

上記記事の内容とほぼコードは一緒ですので、説明は割愛させていただきます。

```javascript:index.js

const fs = require("fs");
const path = require("path");
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

exports.returnHTMLWithOGP = functions.https.onRequest((req, res) => {
  fs.readFile("./hosting/index.html", "utf-8", (e, html) => {
    const overviewId = req.path.split("/")[2];
    const firestore = admin.firestore();
    try {
      firestore.settings({ timestampsInSnapshots: true });
    } catch (error) {
      // ignore
    }
    firestore
      .collection("overviews")
      .doc(overviewId)
      .get()
      .then(doc => {
        const overview = doc.data();
        html = html.replace(
          html.match(/<title>.*<\/title>/),
          `<title>${overview.title} | ${
            overview.authorName
          }さんのロードマップ</title>`
        );
        html = html.replace(
          html.match(/<meta property="og:image"[^>]*>/),
          `<meta property="og:image" content="${overview.eyeCatchImg}">`
        );
        html = html.replace(
          html.match(/<meta property="og:title"[^>]*>/),
          `<meta property="og:title" content="${overview.title} | ${
            overview.authorName
          }さんのロードマップ">`
        );
        html = html.replace(
          html.match(/<meta property="og:url"[^>]*>/),
          `<meta property="og:url" content="https://devmap.work/overviews/${
            overview.key
          }">`
        );
        res.set("Cache-Control", "public, max-age=3600, s-maxage=3600");
        res.status(200).send(html);
        return overview;
      })
      .catch(e => {
        res.status(200).send(html);
        throw e;
      });
  });
  return 0;
});

```


というわけで、以上です。
[Devmap | 独学ロードマップ共有サービス](https://devmap.work/)

よかったらのぞいてみてください。