【個人開発】匿名でアニメの感想を話せるサイトを作ってみた

## どんなアプリ？

アニメを視聴しながら**リアルタイムで感想を共有しているような体験ができる**サービスです。

https://nerd-plus.com/

https://github.com/shouta0715/nerd

沢山の人に使ってもらいたいので、ぜひ使ってみてください！

### なにができるの？

主要な機能は以下の 3 つです。

1. テレビで放送中のアニメをリアルタイムで感想を共有
2. 過去のアニメは配信サービスのタイムコードに合わせて感想を表示するチャット機能
3. アニメ全体の感想を投稿できるコメント機能

**チャット機能に関して**

イメージとしては、以下のような感じで配信サービスのタイムコードとアプリのタイムコードを合わせて、過去のチャットを表示しています。この機能で、過去のアニメを視聴しながらリアルタイムで感想を共有しているような体験ができます。

| 配信サービスのタイムコードとチャット             | アプリ内のタイマー          |
| ------------------------------------------------ | --------------------------- |
| ![realtime_explanation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/cc1b005a-dff7-aef2-f750-3acf88b8bd3e.png) | ![timer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/4a890c99-81a5-80c0-2e99-649b85f803b8.png)  |

### アプリ内のスクリーンショット

| チャット機能               | コメント機能                  |
| -------------------------- | ----------------------------- |
| ![chat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/d9558f7b-b1fb-364d-f3cb-96563ce23f33.png) | ![comment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/777e52e1-b12c-d96f-c061-5baaac73ece6.png) |

| リアルタイム機能               | トップページの一部        |
| ------------------------------ | ------------------------- |
| ![realtime.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/ea0ee990-dbcd-6370-a352-98ca6838b842.png)| ![top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/74413ef2-7582-eacd-bbe8-14f6b0f87948.png)|

| 作品の一覧ページ                   | トレンド機能                |
| ---------------------------------- | --------------------------- |
|![detail_works.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/af67fba6-cfc4-b1be-0fbb-187e850d9556.png)|![trend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/c93c8823-5451-125e-a2ab-ba0fe5844472.png)|

その他にもトレンド機能・作品の追加リクエストなどがあります。
コメントとチャットを投稿する際の名前も自由に変更でができ、それに応じて一意のアバターが生成されます。

## 技術スタック

技術スタックは以下のようになっています。

![nerd_skills.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/4609c2d5-e05e-b45a-ac0d-b8a2a8d2d3a6.png)


### フロントエンド

- Next.js
- TypeScript
- GraphQL
- [Tanstack Query](https://tanstack.com/query/latest)
- Tailwind CSS ... CSS フレームワーク
- [Tailwind UI](https://tailwindui.com/) ... Tailwind CSS のコンポーネント集
- [boring-avatars](https://boringavatars.com/) ... アバターの画像を生成するライブラリ・ユーザー ID と自由に変更できる投稿名で一意のアバターを生成しています

### バックエンド・インフラ

- Hasura
- Neon ... Hasura と接続している PostgreSQL のデータベースです。詳しくは[こちら](https://neon.tech/)
- Firebase Authentication ... 匿名認証と Google アカウント認証を使っています
- Cloudfare Pages

### ページの レンダリング手法

- アニメの個別ページやトレンド機能は CSR でレンダリングしています。
- トップページやその他のページはすべて SSG でレンダリングしています。

SSR を使用していない理由は Hasura を deploy している Cloud Run の Cold Start の影響で、ページの表示に時間がかかってしまうためです。

### 各技術の選定理由

#### Hasura

- リアルタイム通信ができる・セキュリティルールが個別に設定できる・バックエンドを気軽に構築できるという理由から採用しました。

当日放送されているエピソードのみ Hasura の Subscription を使用してリアルタイムで感想を投稿できます！

#### Firebase Authentication

- 匿名ユーザーにも Hasura のセキュリティルールを使用したかったからです。そのため簡単に実装できる Firebase Authentication を採用しました。

#### Next.js

- 自分が Next.js を勉強中だったので、実際にアプリを作りながら学習できると思ったからです。
- SSG などの機能を使って、パフォーマンスが良いアプリを作りたかったからです。

#### Tailwind CSS

- 初期開発時に[Mantine](https://mantine.dev/)をを採用していたが FOUC などが発生することが多かったため、Tailwind CSS に変更しました。

#### Tanstack Query(React Query)

今回作成したアプリでは Tanstack Query + graphql-request を使用しており、Apollo Client などは使用していません。理由としては[Placeholder Query Data](https://tanstack.com/query/v4/docs/react/guides/placeholder-query-data)を使用したいのと、[useInfiniteQuery](https://tanstack.com/query/v4/docs/react/reference/useInfiniteQuery)で簡単に無限スクロールを実装できるからです。
その他 useQuery の返り値や、stale time などの option 設定が豊富であると感じたので採用しました。

### Placeholder Query Data を使用したページ遷移

[Placeholder Query Data](https://tanstack.com/query/v4/docs/react/guides/placeholder-query-data) とは、実際のデータがフェッチされてくる前に表示される一時的なデータ（プレースホルダーデータ）を設定することができる機能です。

ページ遷移前のデータを遷移後のページでロード画面を挟まずに表示することができます。これにより、ユーザーがページ遷移したときにロード画面を表示することなく、ページの表示を早くすることができます。

以下のようにほぼ同じデータが必要な場合、先に表示できるものは表示し、その他必要な詳細データは裏で取得しておくことができます。

| 遷移前の トップページでの表示(SSG) | 遷移後の個別ページ (CSR)           |
| ---------------------------------- | ---------------------------------- |
|![top_ssg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/27b488c8-0f4f-2fa0-1adb-a9986f53b477.png)|![detail_query.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2628116/25299357-54c4-1752-ec3e-7285628920c9.png)|

本来であれば前のデータは以下のようにキャッシュから取り出して使用します。

```ts
const result = useQuery({
  queryKey: ["blogPost", blogPostId],
  queryFn: () => fetch(`/blogPosts/${blogPostId}`),
  placeholderData: () => {
    // Use the smaller/preview version of the blogPost from the 'blogPosts'
    // query as the placeholder data for this blogPost query
    return queryClient
      .getQueryData(["blogPosts"])
      ?.find((d) => d.id === blogPostId);
  },
});
```

> [Placeholder Query Data](https://tanstack.com/query/v4/docs/react/guides/placeholder-query-data)から引用

ですが、今回遷移前のページは SSG を利用しているためキャッシュが存在しません。そのため、以下のように query としてデータを渡しています。
as を使用してブラウザのアドレスバーには Query を表示しないことで、SEO やユーザー体験を損なうことなく、placeholderData を使用することができます。

```tsx
<Link
  as={`/episodes/${episode.id}`}
  href={{
    pathname: `/episodes/${episode.id}`,
    // 渡したいデータだけを渡す
    query: { episode: [episode.title, episode.id] },
  }}
>
  {episode.title}
</Link>
```

遷移後のページでは以下のように query を取得しています。

```tsx
export const useQueryEpisode = () => {
  const router = useRouter();
  const { episode } = router.query;

  return useQuery({
    placeholderData: () => {
      // undefinedを返すことで、suspenseなどでロード画面を表示できるようにする
      if (!episode || typeof episode === "string") return undefined;

      // 本来のデータの型と合わせる
      return getEpisodePlaceholder(episode);
    },
    // ... 省略
  });
};
```

as でブラウザの URL を書き換えているためリーロードしたときは query が存在しません。その時は、placeholderData から undefined が返されるため、suspense などでロード画面を表示できるようにしています。

#### Tanstack Query を使用してい苦労したこと

subscription を利用するのが大変でした。接続失敗時や、接続が中断されたときなどの処理をすべて自分で実装する必要がありました。
通常の query や mutation に関しては特に不便を感じませんでした。

## アニメのデータに関して

3 つの API を使用してアニメのデータを取得しています。

- [Annict](https://developers.annict.com/) ... アニメのデータ

- [しょぼいかれんだー](https://docs.cal.syoboi.jp/spec/feeds/) ... 当日放送しているアニメのデータ

- [メディア芸術データベース(ベータ版)](https://mediaarts-db.bunka.go.jp/) ... その他足りないアニメのデータ

https://developers.annict.com/

https://cal.syoboi.jp/

https://mediaarts-db.bunka.go.jp/

### データの更新方法

毎朝 GitHub Actions で以下のような処理を行っています。

1. しょぼいカレンダーから当日放送しているアニメの放送時間等を取得
2. データベースに保存されているアニメの放送時間を更新
3. Cloudflare Pages へ再デプロイし、今日放送されるエピソードを Hasura 経由で取得して SSG でレンダリング

1.2 に関しては別の GitHub で管理しています。
毎朝6時くらいに更新されるように設定してあります。

## 反省点

### インフラの選定

自分の勉強不足でデプロイ先を Vercel -> Firebase Hosting -> Cloudflare Pages と行き来しました。
結局コストが一番安いという理由と ISR を使用しないという理由から Cloudflare Pages になりました。

データベースも Neon -> Cloud SQL -> Neon と訳のわからないことをしてしまいました。
Cloud Run や Firebase を利用するので GCP で揃えたほうがいいと思い、Cloud SQL にしましたが、Cloud SQL の料金が高いという理由から Neon に戻しました。

## 学習したこと

- アプリの企画からデザイン、開発、デプロイまで一人で行うことで、どのようなことに時間がかかるのかを知ることができました。

- アプリを作る大変さを知ることができました。

- テストの大切さや、テストを書くことの難しさを知ることができました。

- その他フレムワークやライブラリの使い方を学ぶことができました。

- Tailwind UI がすごい

## 今後

- Google AdSense の申請が通らないので頑張って通す
- もっと面白い機能をたくさん追加する。
- 漫画や週刊誌、個人が作成した作品も感想を投稿できるようにしたい

## ぜひたくさん使ってください！

アニメの感想を沢山投稿してください！！

https://nerd-plus.com/

## 余談

### なぜアニメの感想を投稿できるサービスを作ったのか

アニメ見るとテンションが上って誰かに話したくなるからです。

### メディア芸術データベース活用コンテスト

第 4 回メディア芸術データベース活用コンテストで最優秀賞をいただきました。
嬉しいです。

https://mediaarts-db.bunka.go.jp/?locale=ja

### その他

インターンなどで実務経験をもっと積んでいきたいです。就活も頑張りたいです。
最後まで読んでいただきありがとうございました。