話題のSupabaseでサクッと投稿機能をつくってみた！

# はじめに
こんにちは！[かほ](https://twitter.com/kaho_eng)です♪🐥
現在、本業では技術広報業務やエンジニア業務などに従事しています。
今回の記事では、**Next.js**×**Supabase**を用いたアプリ開発における投稿機能の実装とそれに伴う**RLS**(**Row Level Security**)の使用方法について説明します。

今回は下記の2点を実装しました。
* 投稿機能・・・タイトルと内容を投稿する
* 一覧機能・・・投稿した内容を表示する


**Next.js**×**Supabase**×**vercel**の連携方法から知りたい方は、下記の記事を参考にしてください🙌

https://qiita.com/KAHO23340582/items/8a7faf77222a599fb31c

本記事の投稿機能では、下記の認証機能が実装済みです。
認証機能をまだ実装していない方は、下記の記事を参考にしてください🙌

https://qiita.com/KAHO23340582/items/cb8d735b5b6ca1b3a6c5



# この記事の読者対象
* **Next.js**×**Supabase**でCRUD処理(投稿機能、一覧機能)を含むアプリを作ってみたい方
* **Supabase**の**RLS**設定について知りたい方
* **Supabase**を今後使ってみたい方
* **Next.js**を使って高速でアプリを作りたい方

# 開発環境
```package.json
"dependencies": {
    "@supabase/supabase-js": "^2.2.1",
    "eslint": "8.28.0",
    "eslint-config-next": "13.0.6",
    "next": "13.0.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
  },
  "devDependencies": {
    "@types/node": "^18.11.13",
    "@types/react": "18.0.26"
  }
```
# 前提
今回は、postsテーブルを使って投稿機能を実装します。
テーブルは**Supabase**上のsqlエディターから作成します。
todosテーブルを作った事例があるため、よければ下記の記事「**Supabaseのプロジェクトを作成しよう！**」の項目を参考にしてください🙌

https://qiita.com/KAHO23340582/items/8a7faf77222a599fb31c

また、今回のテーブル構造は下記になります。
<table>
    <thead>
      <tr>
        <td>カラム</td>
        <td>型</td>
　　　　　　　　　　　　　　　　<td>デフォルト値</td>
      </tr>
    </thead>
    <tbody>
　　　　　　　　　　　　　<tr>
        <td>id</td>
        <td>int4</td>
    　　　　　　　　<td></td>
      </tr>
　　　　　　　　　　　　　<tr>
        <td>title</td>
        <td>text</td>
　　　　　　　　　　　　　　　　　<td></td>
      </tr>
　　　　　　　　　　　　<tr>
        <td>created_at</td>
        <td>timestamptz</td>
　　　　　　　　　　　　　　　　　<td>now()</td>
      </tr>
　　　　　　　　　　　　<tr>
        <td>user_id</td>
        <td>uuid</td>
　　　　　　　　　　　　　　　　　<td></td>
      </tr>
　　　　　　　　　　　　<tr>
        <td>content</td>
        <td>text</td>
　　　　　　　　　　　　　　　　　<td></td>
      </tr>
    </tbody>
</table>

# そもそもRLSとはなんぞや？
最初に、すべての機能に対する認可を**RLS**で設定します。
そもそも**RLS**を知らない人が多いと思うので簡単にご紹介。

**RLS**は**Row Level Security**の略称で、いわゆる**行単位セキュリティー**と呼ばれるものです。
**ポリシー**を設定することにより、テーブルに紐づけて、アクセス制御の記述を簡潔に書くことができます。

詳しくは下記の記事を参考にしてください🙌

https://www.supabase.jp/docs/guides/auth/row-level-security

# RLSを設定しよう
## RLSの有効化を確認しよう
最初に、`posts`テーブルに対してRLSを設定できる状態か確認します。
1. 左側のバーの人マークである`Authentication`を押します。
1. `Configuration`の`Policies`を選択します。
1. テーブル名「posts」の右横に、下記のようなマークが出ていることを確認します。
![スクリーンショット 2023-01-16 9.11.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/dadad20a-4b4a-6adf-262c-ea299174c6df.png)

## Policyを作成しよう
次にPolicyを作成します。
1. RLS有効化を確認した画面の右端にある`New Policy`というボタンを押します。
1. ボタンを押すと下記の画面のように選択肢が2つ出てきます。
テンプレートを使用してPolicyを作成したい方は`Get started quickly`を選択し、完全に0からPolicyを作成したい方は`For full customization`を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/7173fc48-bc51-21e3-fcd5-1a5ff6951788.png"  width="500" height="186" />.
1. 今回はテンプレートから作成したいので、`Get started quickly`を選択しました。下記のような画面ができます。上から`取得`、`挿入`、`更新`、`削除`の機能に対するSQLテンプレートがあります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/3418b2ef-28f8-7f5d-82bf-ce580df64e90.png"  width="700" height="186" />.

### 投稿機能に対する認可を設定しよう
最初に投稿機能に対する認可の設定を行います。
1. 投稿機能に対する認可を設定したいので、4つのSQLテンプレートの中から`Enable insert access for authenticated users only`(上から2つ目)を選択し、`Use this template`ボタンを押します。すると下記の画面が出てきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/95fd5f6c-7779-4f6a-552c-710ea1c1f299.png"  width="700" height="186" />.
1. `Policy name`を設定します。今回はログイン済みのアカウントのみが投稿できるようにしたいので、`認証済みのアカウントのみ投稿できる`という名前を設定します。
1. `Allowed operation`を設定します。選択肢が4つありますが、データを挿入したいので`INSERT`を選択します。
1. `Target roles`を設定します。今回はログイン済みのアカウントを対象とした認可のため、選択肢から`authenticated`を選択します。
1. `WITH CHECK expression`を設定します。ここに書くもののイメージとしてはSQLでいうWHERE句だと考えていいでしょう。今回は、ログイン済みのアカウントであれば、常に投稿できる状態にしたいので`true`と書きます。
1. 右下の`Review`のボタンを押します。設定したSQL情報が下記の画面のように出てくるため、情報に間違えがなければ`Save policy`を押してPolicyを保存します。
![スクリーンショット 2023-01-16 10.40.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/ab8fac42-9f49-fe74-b20c-35910b38e127.png)

### 一覧機能に対する認可を設定しよう
次に一覧機能に対する認可の設定を行います。
1. 一覧機能に対する認可を設定したいので、4つのSQLテンプレートの中から`Enable read access to everyone`(上から1つ目)を選択し、`Use this template`ボタンを押します。すると下記の画面が出てきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/9f732f97-3532-faf0-33e4-b653f88dc637.png"  width="700" height="186" />.
1. `Policy name`を設定します。今回は全アカウントが閲覧できるようにしたいので、`全員データの閲覧を可能にする`という名前を設定します。
1. `Allowed operation`を設定します。全てのデータを選択して表示させたいので`SELECT`を選択します。
1. `Target roles`を設定します。今回は全てのアカウントを対象とした認可のため、設定は不要です。
1. `USING expression`を設定します。今回は、全てのアカウントが常にデータを閲覧できる状態にしたいので`true`と書きます。
1. 右下の`Review`のボタンを押します。設定したSQL情報が下記の画面のように出てくるため、情報に間違えがなければ`Save policy`を押してPolicyを保存します。
![スクリーンショット 2023-01-16 11.22.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/f79c93ce-030c-c195-4eeb-0e3411680c4d.png)

## Policyの作成を確認しよう
投稿機能と一覧機能に対するPolicyの設定が終わりました。
Policyの設定が完了している場合は、下記の画像のように行として表示されるためご確認ください。また、Policyを編集・削除する場合は右端の縦3点のボタンを押すと可能です。
![スクリーンショット 2023-01-16 11.27.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/91e06d72-a273-c502-51cc-6939601fbbe8.png)


# 投稿機能を実装しよう
最初に、投稿機能についての説明を行います。
下記が`top.tsx`の投稿機能における全体像です。

```jsx:top.tsx
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import { useRouter } from 'next/router';
import { useState,useEffect } from 'react';


export default function Top(){
  const [newTitle, setNewTitle] = useState("");
  const [newContent, setNewContent] = useState("");

  const addPost = async (e) => {
    e.preventDefault();
    try {
      const { error } = await supabase.from("posts").insert([
        {
          title: newTitle,
          content: newContent,
        },
      ]);
      if (error) throw error;
      await indexPost();
      setNewTitle("");
      setNewContent("");
    } catch (error) {
      alert("データの新規登録ができません");
    }
  };

  return(
    <>
      <div className={styles.container}>
        <main className={styles.main}>
        <div>
          <form onSubmit={addPost}>
             <div>
                <label>タイトル</label><br/>
                <input
                  type="text"
                  value={newTitle}
                  onChange={(e) => setNewTitle(e.target.value)} />
              </div>
              <div>
                <label>内容</label><br/>
                <textarea value={newContent} rows={10} cols={40}  onChange={(e) => setNewContent(e.target.value)}/>
              </div>
              <div>
                <button type="submit">登録</button>
              </div>
          </form>
        </div>
      </main>
      <footer className={styles.footer}>
      </footer>
    　　　　</div>
    </>
  )
}

```
では、コードを上から順に追って説明します。
最初にタイトル、内容の各値を状態管理するための記述を行います。
```typescript:top.tsx
const [newTitle, setNewTitle] = useState("");
const [newContent, setNewContent] = useState("");
```

次にsupabase.tsを呼び出し、認可部分の実装を行います。
今回は`insert()`を使用し、postsテーブルにデータを挿入します。
引数には挿入したいデータカラムとそれに伴う登録フォームの変数を設定します。
`insert()`を使用する方法は、下記のsupabase公式ドキュメントにも記載されていますので、ご興味のある方はご参照ください🙌

https://www.supabase.jp/docs/reference/javascript/insert

`await indexPost()`に関しては、投稿機能の実装後に説明する一覧機能のメソッドを呼び出しています。登録した値が一覧画面に表示されるように設定しています。

投稿完了後は投稿フォームの値を空にしたいため、`setNewTitle("")`と`setNewContent("")`によりタイトルと内容の変数の値を空に設定しています。
`try-catch`構文を用いることにより、例外処理を行い、エラーが出た場合の対処を行います。
```jsx:top.tsx
 const addPost = async (e) => {
    e.preventDefault();
    try {
      const { error } = await supabase.from("posts").insert([
        {
          title: newTitle,
          content: newContent,
        },
      ]);
      if (error) throw error;
      await indexPost();
      setNewTitle("");
      setNewContent("");
    } catch (error) {
      alert("データの新規登録ができません");
    }
  };
```

投稿機能に関わる全体のマークアップを行います。
タイトルと内容のフォームに`onChange`イベントを設定し、入力した各値を保持します。
```jsx:top.tsx
return (
    <>
       <form onSubmit={addPost}>
          <div>
            <label>タイトル</label><br/>
            <input
              type="text"
              value={newTitle}
              onChange={(e) => setNewTitle(e.target.value)}
            />
          </div>
          <div>
            <label>内容</label><br/>
            <textarea value={newContent} rows={10} cols={40}  onChange={(e) => setNewContent(e.target.value)}/>
          </div>
          <div>
            <button type="submit">登録</button>
          </div>
      </form>
    </>
  )
```
下記がマークアップした投稿画面の画像です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/b67313a5-24d1-1256-a6db-e1d1051d9575.png" width="500" height="186" />.

# 一覧機能を実装しよう
次に一覧機能についての説明を行います。
下記が`top.tsx`の一覧機能における全体像です。
```jsx:top.tsx
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import { useState,useEffect } from 'react';


export default function Top(){
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    (async () => await indexPost())();
  }, []);


  const indexPost = async () => {
      try {
        const { data, error } = await supabase.from("posts").select("*");
        if (error) throw error;
        setPosts(data);
      } catch (error) {
        alert(error.message);
        setPosts([]);
      }
    };

  return(
    <>
      <div className={styles.container}>
      <main className={styles.main}>
        <div>
          <h1>トップページ</h1>
            <div>
              <table>
                <thead>
                  <tr>
                    <th>投稿日</th>
                    <th>タイトル</th>
                    <th>内容</th>
                  </tr>
                </thead>
                <tbody>
                  {posts.map((post) => (
                    <tr key={post.id}>
                      <td>{post.created_at.substr(0, 10)}</td>
                      <td>{post.title}</td>
                      <td>{post.content}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
        </div>
      </main>
      <footer className={styles.footer}>
      </footer>
    </div>
    </>
  )
}

```

一覧機能の実装方法について、コードを上から順に説明します。
最初に一覧表示をする投稿(posts)の値を状態管理するための記述を行います。
また、投稿内容の繰り返し表示を防ぐために`useEffect`で`indexPost()`を呼び出します。
```top.tsx
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    (async () => await indexPost())();
  },[]);
```
次にsupabase.tsを呼び出し、認可部分の実装を行います。
今回は`select()`を使用し、postsテーブルのデータを全選択します。

`select()`を使用する方法は、下記のsupabase公式ドキュメントにも記載されていますので、ご興味のある方はご参照ください🙌

https://www.supabase.jp/docs/reference/javascript/select

一覧表示するデータをマークアップの際に使用したいため、`setPosts()`の変数に`data`を設定しています。`try-catch`構文を用いることにより、例外処理を行い、エラーが出た場合の対処を行います。
```typescript:top.tsx
const indexPost = async () => {
    try {
      const { data, error } = await supabase.from("posts").select("*");
      if (error) throw error;
      setPosts(data);
    } catch (error) {
      alert(error.message);
      setPosts([]);
    }
  };
```
一覧機能に関わる全体のマークアップを行います。
posts変数には上記で設定した`setPost(data)`の`data`(挿入した投稿の値)が入っています。
そのため、postsを用いて`map`構文で値を出力します。
```jsx:top.tsx
 return(
    <>
      <div className={styles.container}>
      <Head>
        <title>トップページ</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
        <div>
          <table>
            <thead>
              <tr>
                <th>投稿日</th>
                <th>タイトル</th>
                <th>内容</th>
              </tr>
            </thead>
            <tbody>
              {posts.map((post) => (
                <tr key={post.id}>
                  <td>{post.created_at.substr(0, 10)}</td>
                  <td>{post.title}</td>
                  <td>{post.content}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </main>
      <footer className={styles.footer}>
      </footer>
    </div>
    </>
  )
```
下記がマークアップした一覧画面の画像です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/df682477-9416-0690-ab9a-747d2e5a4e37.png"  width="500" height="186" />.

# 認可ができているか確認しよう！
**RLS**の設定と投稿機能の実装を終えました！
認可の設定が上手くいっているかを確認してみましょう♪
下記画像のデモは、以下の順ですすみます。
1. ログイン済みの状態で投稿
1. ログアウト
1. ログアウト状態で投稿

「すべてのユーザーが投稿の一覧表示を見ることができる」「ログイン済みのユーザーのみがタイトルと内容を投稿できる状態になっている」ことを確認することができました😚成功です✌️
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/972366b6-b79f-b493-573d-535c3545c1bf.gif" width="500" height="186" />.

# 最後に
これで実装はおしまいです！

今回は、**Next.js**×**Supabase**を用いたアプリ開発における投稿機能の実装とそれに伴う**RLS**(**Row Level Security**)の使用方法について説明しました。今後は主にフロントエンド周辺、コミュニティ運営、Tech PR(技術広報)の記事を中心に書いていく予定ですので、気になる方はぜひフォローをよろしくお願いします♪ではでは〜🐥


Twitterアカウント:https://twitter.com/kaho_eng

# 参考資料
https://qiita.com/KAHO23340582/items/8a7faf77222a599fb31c

https://qiita.com/KAHO23340582/items/cb8d735b5b6ca1b3a6c5

https://www.supabase.jp/docs/guides/auth/row-level-security

https://www.supabase.jp/docs/reference/javascript/insert

https://www.supabase.jp/docs/reference/javascript/select