4年前のプログラミング合宿で作成したプロダクトのリプレイス(その１)

## 概要
4年前当時GAOGAOゲートで開発したwebアプリケーションを新たな技術スタックで作り直していきます

## そもそもGAOGAOゲートとは
そもそもゲートとは何かを簡単に説明します。
ゲートとは一言で言うとアプリ開発合宿、プログラミング修行です。プログラミングをはじめ、ソフトウェアエンジニアとして活躍するためのスキルを身につけることができるカリキュラムになっています
私は2020年1月に参加して、日本で1ヶ月、ベトナムで1ヶ月プログラミングの基礎学習とオリジナルアプリ開発を行いました。

## 当時作成したアプリ
そんなGAOGAOゲートで当時開発経験がない状態で作成したのがシンプルな画像投稿アプリです
主な機能は下記の通りです

1. 画像投稿
2. チャンネル作成機能
3. リアルタイム更新

### リプレイス
当時作成したアプリを新しく作り直していきたいと思います
今回はリアルタイム更新のロジック部分を作成していきます

#### as is
以前作成したwebアプリは主に下記技術を使用していました。
簡単にまとめると、見た目はbladeでサーバー側で生成していました。
投稿部分はjsでポーリングをして、一定期間ごとにリクエストを送り、リアルタイムな更新を実現していました

- javascript
- Laravel
- PHP
- blade(テンプレートエンジン)
- Cloudinary
https://note.com/di45/n/n73035288ba7c

#### to be
今回は下記を使って新たに作り直していきます

- supabse
- TypeScript
- Next.js
- React

## パッケージのバージョン
主なパッケージのバージョンは下記の通りです
```
node: 18.18.2
react: 18.2.43
next": 14.0.4
typescript: 5.3.3
```

## PJ作成
下記を実行してPJ作成

```
npx create-next-app image-upload-app
```

## supabaseの設定
チャンネルがそれぞれあり、各チャンネルに紐づいたPostが存在する構成となっています

### Channel用のDB(Postを紐付け)
添付画像のような構成で作成しました

![スクリーンショット 2023-12-21 22.10.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920178/89206af9-d038-c478-0ac4-c9283023bbdf.png)

### Post用のDB
添付画像のような構成で作成しました

![スクリーンショット 2023-12-21 22.09.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920178/72a1f04d-0fbd-28f7-7caf-9ec4cfc5c1c6.png)

## チャンネルに紐づいたメッセージ取得(初期表示時)
初期表示時はserver componentでチャンネルに紐づいたメッセージを取得しています
そちらのコードが下記となります

```typescript
import { notFound } from 'next/navigation'
import type { Database } from '../../../database.types'
import Posts from '../components/posts'

type Post = Database['public']['Tables']['posts']['Row']

type PageProps = {
  channelId: string
}

async function fetchPosts(channelId: string) {
  const res = await fetch(
    `${process.env.base_url}/rest/v1/posts?select=*`,
    {
      headers: new Headers({
        apikey: process.env.apikey as string,
      }),
      cache: 'no-store',
    }
  )
  const posts: Post[] = await res.json()
  return posts
}

export default async function channelDetailPage({ channelId }: PageProps) {
  const posts = await fetchPosts(channelId)
  if (!posts) return notFound()

  return (
    <div className="mt-16 p-8">
      <Posts posts={posts} channelId={params.channelId} />
    </div>
  )
}
```

## チャンネルに紐づいたメッセージのリアルタイム更新
クライアントコンポーネント内でsupabaseのリアルタイムリスナーを使用して、`posts`という変数に保存しています。
以前はポーリングを使用していましたが、今回はsupabaseの仕組みを利用させてもらいました
新規メッセージが投稿された場合(現時点では未実装)はリアルタイムに更新されるようになっています

```typescript
'use client';
import type { Database } from '../../../database.types'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { useEffect, useState } from 'react'

type Post = Database['public']['Tables']['posts']['Row']

type PageProps = {
  posts: Post[]
  channelId: string
}

export default function Posts({ posts: initialPosts, channelId }: PageProps) {
  const [posts, setPosts] = useState<Post[]>(initialPosts)

  useEffect(() => {
    const supabase = createClientComponentClient<Database>();
    const newPosts = supabase.channel('custom-filter-channel')
    .on(
      'postgres_changes',
      { event: '*', schema: 'public', table: 'posts' },
      (payload) => {
        console.log('Change received!', payload)
        const newPost = payload.new as Post;

        switch(payload.eventType) {
          case 'UPDATE': {
            setPosts((prevPosts) => prevPosts.map(x => x.id === newPost.id ? newPost : x))
            break;
          }

          case 'INSERT': {
            setPosts((prevPosts) => [...prevPosts, newPost])
            break;
          }

          case 'DELETE': {
            const oldPost = payload.old as Post;
            setPosts((prevPost) => prevPost.filter(x => x.id !== oldPost.id))
            break;
          }
        }
      }
    )
    .subscribe()

    return () => {
      newPosts.unsubscribe()
    }
  }, [])

  return (
    <div className="mt-16 p-8">
      {posts.map(x => (
        <div key={x.id}>
          <p>{x.text}</p>
          {x.image ? <img src={x.image} alt=""></img> : null}
          <p>{x.created_at}</p>
        </div>
      ))}
    </div>
  )
}
```

## デモ
DBを直接いじって試したところ、リアルタイムに更新できています

![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920178/471752de-ee4e-dc14-8942-3b4461d05c75.gif)


## まとめ
本記事ではリアルタイム更新の部分を作成したので、下記の機能を実装できたら、その2以降を書いていきます

1. 画像投稿
2. チャンネル作成機能
3. 認証部分
4. 全体的なUIの実装