書いたコードを言語別に集計してシェアするサービス「Commitly」を作ってみた

# :star2: 作ったもの

https://commitly-next-web.vercel.app

次のようにGitHubにコミットしたコードについて、どの言語を何行書いたかツイートするサービスです

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">2020年8月1日(土)<a href="https://twitter.com/mikan_the_third?ref_src=twsrc%5Etfw">@mikan_the_third</a> は12438行のコードを書きました！<br><br>TypeScript: 633<br>YAML: 344<br>JSON: 242<br>Markdown: 24<br>JavaScript: 2<br><br>.lock: 11176<br>.gitignore: 17<a href="https://twitter.com/hashtag/commitly?src=hash&amp;ref_src=twsrc%5Etfw">#commitly</a><a href="https://t.co/nPgFirPd14">https://t.co/nPgFirPd14</a></p>&mdash; みかん三世 (@mikan_the_third) <a href="https://twitter.com/mikan_the_third/status/1289863541386665984?ref_src=twsrc%5Etfw">August 2, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# :thinking: なぜ作ったか
- エンジニアとして日々コードを書くモチベーションになればと、個人的に作ったものをサービス化しました
- もちろんコード量が多いほど良いわけではありませんが、エンジニアとして生きていくのであればコードを書き続けることになるはずで、そういったエンジニア同士で知り合えるきっかけにもなってほしいというの理由です
- こうしてQiitaなどで記事を書くのも良いのですが、日々のコーディングの成果が自然とアウトプットになるようにしたいということで、このように自動化しました
- また、一言にエンジニアと言っても担当領域や使用する言語によってグループ化されると思うので、どのような言語についてコミットしているのか分かるようにしました

# :pencil:  技術スタック
オープンソースなので参考にしたい方はどうぞ
https://github.com/mikan3rd/commitly

## [React](https://reactjs.org/) + [TypeScript](https://www.typescriptlang.org/)
- 普段からReact + TypeScriptのSPAを作っており、自分としては（今は）最善手だと思っているのでいつもこの構成で作っています
- VueやAngularもやってみようかなと調べてみたのですが、やはりテンプレート構文というところに型定義の限界と状態管理の限界を感じたため、引き続きJSXで書けるReactを選択しました
- 自分が共感した記事
  - [本当に倒すべきだったのは jQuery ではなくテンプレートエンジンだった](https://scrapbox.io/fsubal/%E6%9C%AC%E5%BD%93%E3%81%AB%E5%80%92%E3%81%99%E3%81%B9%E3%81%8D%E3%81%A0%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%AF_jQuery_%E3%81%A7%E3%81%AF%E3%81%AA%E3%81%8F%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%B3%E3%81%A0%E3%81%A3%E3%81%9F)
  - [僕がTypeScriptを最善手の言語として認識している理由](https://note.com/erukiti/n/n6b1bf5426d63)

## No Redux
- 逆に使ってないのはReduxです
- これまでログインユーザー情報などグローバルな状態管理のためになんだかんだReduxを使ってきましたが、`React.createContext` `React.useContext` によりhooksで同等の実装ができるようになったので試してみました
- また、これまで非同期処理についてredux-thunkやredux-sagaを使ってきましたが、これも不要になりました
- 自分も「結局Reduxでやることと一緒ならRedux使えばいいかな？」と思ってましたが、コードもスッキリしてredux関連のライブラリも不要になったので、試しに使ってみるのがおすすめです（サービスの性質にもよるとは思いますが）

```example.tsx
import React from "react";

type UserContextType = {
  user: firebase.User | null;
  loadingUser: boolean;
  login: () => Promise<void>;
  logout: () => Promise<void>;

export const UserContext = React.createContext<UserContextType>(undefined);

export default function UserContextComp({ children }) {
  const [user, setUser] = React.useState<firebase.User | null>(null);
  const [loadingUser, setLoadingUser] = React.useState(true);

  const login = async () => {
    // pass
  };

  const logout = async () => {
    // pass
  };

  return (
    <UserContext.Provider
      value={{
        user,
        loadingUser,
        login,
        logout,
      }}
    >
      {children}
    </UserContext.Provider>
  );
}

export const useUser = () => React.useContext(UserContext);

```

## [immer](https://github.com/immerjs/immer)
- これまでイミュータブルなデータの取り扱いに `immutable.js` を使っていたのですが2018年10月に `v4.0.0-rc.12` がリリースされて以降ほとんど音沙汰がなくなってしまいました
- `immutable.js`のRecordでモデルクラスを作ってデータを操作するのが個人的には使いやすかったので残念ですが、固有メソッドが多く使い方に慣れる必要があるなどとっつきにくい点もあったため、代替として`immer`を検討してみました
- まだそんなに本格的に使えてはいませんが、`immer`は`immutable.js`に比べるとシンプルなAPIでイミュータブルなデータ操作ができ、アップグレードも定期的に行われているようなので今後はこっちを使ってみようかなと思っています

## [Next.js](https://nextjs.org/)
- ツイートした時にOGPの表示に対応させたかったため、SSR対応しやすいNext.jsを選択しました
  - 以前、仕事でSSR対応した時にSSRとSPAの両方の挙動に対応した作りにしないといけなくて面倒だったイメージがあり抵抗があったのですが、最新版では`getServerSideProps`が用意されているなど問題なく実装でき、Reactに慣れていれば困るポイントはそこまでなかったです
- 実は最初は [静的サイトジェネレーター Gatsby](https://www.gatsbyjs.org/) で作っていました
  - 前々から興味があったのと動的ページも作れそうという見込みで使ったのですが、あくまでbuild時に動的ページを全てgenerateする(Static Generation)というだけで、アクセス時に静的ページが用意される(Server-side Rendering)のは難しそうだったのでNext.jsに移行しました
  - ブログやLPページなどをReactで作りたいのであればGatsbyで良かったと思うのですが、webアプリを作る場合はやはりNext.jsの方が良さげです
  - 残骸: https://github.com/mikan3rd/commitly-gatsby-web

## [emotion](https://emotion.sh/docs/introduction)
- 以前は自分もscssをBEM記法で書いてましたが、DOMとstyleを別々に記述することや、別々のファイルにあるものをclass名でつなぎ合わせる作業が面倒なので、自分は CSS in JS が好きです
  - スタイルのスコープがグローバルな場合はclass名の競合に気をつけないといけなかったり、そもそもclass名の設計を考えること自体が面倒だったりチーム開発の場合は命名規則がズレがち
  - styled-componentsの場合はコンポーネントごとにファイルを分けている場合は名前の競合は基本発生しない
- 最近はstyled-componentsで記述していたのですが、Gatsbyがemotionを採用していたこともあり使ってみました
  - styled-componentsで個人的に気になる点としてはちょっとスタイルを当てたいだけでも`styled.XXX`を書かないといけないのと、その記述がJSXの記述の前後にあると長くなってしまうことでした
  - emotionの場合はstyled-componentと同等の機能が直感的にインラインスタイルで書けるのが普通に便利だったので今後も使いたい
- 自分が共感した記事
  - [フロントエンドにおける「関心の分離」は間違っていた](https://scrapbox.io/fsubal/%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%A8%E3%83%B3%E3%83%89%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%80%8C%E9%96%A2%E5%BF%83%E3%81%AE%E5%88%86%E9%9B%A2%E3%80%8D%E3%81%AF%E9%96%93%E9%81%95%E3%81%A3%E3%81%A6%E3%81%84%E3%81%9F)

## [Semantic UI React](https://react.semantic-ui.com/)
- 自分でコンポーネントのスタイルのデザインができないのと、さっと作りたいやっぱりこういうのを使っちゃってます
- BootstrapやMaterial UIよりかっこい気がする（主観的な意見です）
- 問題点はCSSの当て方が `.parent > .child` みたいな親子関係で指定されているとoverrideし辛い（`!important`を使うハメになる）ことですね

## [Firebase](https://firebase.google.com/)
- イメージ的には[GCP](https://console.cloud.google.com/)のweb/mobileアプリ向け簡易セットみたいな感じなので、スモールスタートで何か作るにはちょうどいいと思います
- DBはFirestoreを使っていますが、NoSQLなのとドキュメント呼び出しごとの従量課金となるので、設計や使い方は考えておかないとサービスに合わなくなってしまいます（無料枠はありますが）
- バックエンドはCloud Functionで実装しており、フロントと同じくTypeScriptでサッと書けるのが自分としては嬉しいです
- Cloud FunctionではCloud Schedulerを使った定期処理やPubSubをトリガーにした処理も書けるのでやりたいことは問題なくできた
- `firebase.analytics()` `firebase.performance()` でアナリティクスやパフォーマンスチェックも手軽に有効にできる
- SSRの場合、client用の`firebase`とnode用の`firebase-admin`を使い分ける必要があるのが微妙


## Vercel
- Firebaseを使っていたので最初はFirebase Hosting + Cloud Function でSSRのホスティングをしたのですが、体感でも結構遅かったのでやめました（たぶんregionが`us-central1`しか対応してないせい）
- Next.js公式からおすすめされている通りVercelを使ってホスティングしたらGitHubと連携するだけで簡単にホスティングができました
- また、PRを作るとPreviewとしてPRの内容を反映したホスティングも自動でしてくれるのも良い

![vercel.com_mikan3rd_commitly-next-web(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/2b493a2e-ef3c-3b0a-c6ff-3e987b1081d5.png)


## GitHub Actions
- Next.jsのデプロイのみVercelのGitHubインテグレーションで行っていますが、型チェック・lintチェック・buildチェック・build結果の保存・buildしたファイルのデプロイなどについてはGitHub Actionsで動かしています

```example.yaml
name: Node.js CI

on: [push]

jobs:
  functions-build:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
      - uses: actions/checkout@main

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: ${{ runner.os }}-yarn-

      - run: yarn install
        working-directory: ./functions

      - run: yarn lint
        working-directory: ./functions

      - run: yarn build
        working-directory: ./functions

      - name: Archive Production Artifact
        uses: actions/upload-artifact@main
        with:
          name: functions-build-files
          path: functions/lib

  functions-deploy:
    if: github.ref == 'refs/heads/master'
    needs: functions-build
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
      - uses: actions/checkout@main

      - name: Download Artifact
        uses: actions/download-artifact@main
        with:
          name: functions-build-files
          path: functions/lib

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Get yarn cache directory path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - uses: actions/cache@v2
        id: yarn-cache
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: ${{ runner.os }}-yarn-

      - run: yarn install
        working-directory: ./functions

      - name: deploy to Firebase Functions
        working-directory: ./functions
        run: yarn deploy:prod --token=${{ secrets.FIREBASE_TOKEN }}
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

```
 
# :sob: 困ったところ
- GitHub APIの仕様上、GitHub Appを作らないと今回の要件（privateリポジトリを含めたコミットの行数の取得）ができなかったなど、外部のAPIに対する理解と依存する部分とサービスのすり合わせ
- 今回の技術スタックで見本になるようなものが少なかったこと（Reactは書き慣れていたがそれ以外は新しい物を試しに使っていたりしたので）

# :star2: 今後やりたいこと
- 言語別のコミットランキング
- QiitaのようにOGP画像を動的に生成する
- GraphQL + Apollo or NestJS