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

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


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

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

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

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


# この記事の読者対象
* **Next.js**×**Supabase**で画像表示を含むアプリを作ってみたい方
* **Supabase**で画像アップロード機能を含むアプリを作ってみたい方
* **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",
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "@types/node": "^18.11.13",
    "@types/react": "18.0.26"
  }
```
# Storageを設定しよう
## バケットを作成しよう
最初に画像を保管する場所であるバケットの設定を行います。
1. 左端のバーにある`Storage`のアイコンを押します。
1. すると、まだバケットを作成していない場合下記の画面が出てきます。
`Create a new bucket`ボタンを押しましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/e01f3cd8-3ca6-d9a9-f968-c6ecda5e768e.png"  width="400" height="1００" />

3.　`Name of bucket`に設定するバケット名を書きます。また、バケットの設定には`Public`と`Private`の2パターンが存在します。今回は`Public`で設定します(誰でも写真を閲覧できるようになるので、変な写真はアップロードしないように気をつけましょう笑)。最後に`Create bucket`のボタンを押します。
  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/5093f9e9-b4a4-1657-2559-6906c7b55d9a.png"  width="400" height="1００" />

4.　これでバケットの作成は完了です！バケットは`Public`設定にしてある場合に、バケット名の右横に黄色で`Public`と表示されます。(また、設定を`Private`に変更したい場合は、バケット名に触れると出てくる縦の点表示を押せば可能です。)
 <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/7f47bcfc-cc9d-ed00-a6f8-85f782f0a57c.png"  width="800" height="5００" />

5.アップロードする写真をフォルダーごとに分けたいため、右上にある`Create folder`を押してフォルダーを作成します。今回のフォルダー名はcharactersに設定しました。

## RLSを設定しよう
Storageに保存された写真をブラウザ上でアップロードしたり閲覧したりするには、Policyの作成による認可が必要です。RLSを知らない人は下記の記事の「そもそもRLSとはなんぞや？」の章をご参考に🙌

https://qiita.com/KAHO23340582/items/6f9ac01d77ab198881f4

### Policyを作成しよう
次にPolicyを作成します。上記で紹介した記事の「Policyを作成しよう」の手順と基本的に同じです。
1. `Storage`にある`Configuration`の`Policies`を選択すると下記の画面が出てきます。今回は、`other policies under storage.objects`の列にPolicyを作成します。右端にある`New Policy`というボタンを押します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/c10cef95-7795-2dea-db5e-c767c92be765.png"  width="800" height="5００" />

2.　ボタンを押すと下記の画面のように選択肢が2つ出てきます。テンプレートを使用してPolicyを作成したい方は`Get started quickly`を選択し、完全にゼロからPolicyを作成したい方は`For full customization`を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/41d3cf4a-6068-4d95-388a-8b52b435f986.png"  width="500" height="186" />

3.　今回はテンプレートから作成したいので、`Get started quickly`を選択しました。下記のような画面ができます。上から`取得`、`挿入`、`更新`、`削除`の機能に対するSQLテンプレートがあります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/cdb2d347-bdbc-8e90-6a17-7a65a1a59bb0.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/a37e5aac-ec44-353b-5fa5-9d62a00dadee.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-27 16.38.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/d2138a5f-8e63-c235-6518-e4ed1cd128a4.png)
1. Policyが作成され、ログイン済みのアカウントのみが画像をアップロードできるように設定しました。上記のような方法と同様で、ブラウザで画像を誰でも閲覧できるようにPolicyを作成しましょう。4つのSQLテンプレートの中から`Enable read access to everyone`(上から1つ目)を使用し、設定を行なってください(説明は省略します)。



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


# 画像アップロード機能を実装しよう
次に、　画像アップロード機能についての説明を行います。
下記が`storage.tsx`の画像アップロード機能における全体像です。

```jsx:storage.tsx
import { v4 as uuidv4 } from "uuid";
import { supabase } from "../utils/supabase";

type UploadStorage = {
  folder: FolderList;
  bucketName: string;
};

type UploadPathname = {
  path: string;
};

export const uploadStorage = async ({
  folder,
  bucketName,
}: UploadStorage): Promise<UploadPathname> => {
    const file = folder[0]; // 1ファイルアップロード
    const pathName = `characters/${uuidv4()}`; // パス名の設定
    const { data, error } = await supabase.storage
      .from(bucketName)
      .upload(pathName, file, {
        cacheControl: "3600",
        upsert: false,
      });
    if (error) throw error;
    return {
      path: data?.path ?? null,
    };
};


```
主要な部分のコードを説明します。
supabase.tsを呼び出し、画像のアップロード部分の実装を行います。
今回は`from.upload()`を使用し、Storageのバケットに画像をアップロードします。

fromの引数には保存先のバケット名、uploadの引数にはフォルダー以下の設定パス名、保存するファイル数を表した変数、 `cacheControl`、`upsert`は固定で下記コードの値を設定します。

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

https://supabase.com/docs/reference/javascript/storage-from-upload

```jsx:storage.tsx
 export const uploadStorage = async ({
  folder,
  bucketName,
}: UploadStorage): Promise<UploadPathname> => {
    const file = folder[0]; 
    const pathName = `characters/${uuidv4()}`;
    const { data, error } = await supabase.storage
      .from(bucketName)
      .upload(pathName, file, {
        cacheControl: "3600",
        upsert: false,
      });
    if (error) throw error;
    return {
      path: data?.path ?? null,
    };
};
```


# 画像表示機能を実装しよう
次に画像表示機能についての説明を行います。
下記が`top.tsx`の画像の表示機能における全体像です。
```jsx:top.tsx
import { useState } from 'react';
import { uploadStorage } from './storage';
import { supabase } from "../utils/supabase";

export default function Example(){
  const [ path,setPathName ] = useState<string | undefined>();
  const handleUploadStorage = async (folder: FolderList | null) => {
    if (!folder || !folder.length) return;
    const { path } = await uploadStorage({
      folder,
      bucketName: "pictures",
    });
    const { data } = supabase.storage.from("pictures").getPublicUrl(path)
    if (path) setPathName(data.publicUrl);
    console.log(path)
  };
  return (
    <label htmlFor="file-upload">
      <span>アップロードする</span>
      <input
        id="file-upload"
        name="file-upload"
        type="file"
        className="sr-only"
				accept="image/png, image/jpeg"
        onChange={(e) => {
          const fileList = e.target?.files;
          console.log(fileList);
          handleUploadStorage(fileList);
        }}
      />
      <img src={path} alt="" width="800" height="500"/>;
    </label>
  );
};

```

画像表示機能の実装方法について、コードを上から順に説明します。
最初に、画像表示を行う際に使用する`img`タグのsrcに入れるパスの値を状態管理するための記述を行います。
```top.tsx
  const [ path,setPathName ] = useState<string | undefined>();
```
次に上記で実装した`uploadStorage`とsupabase.tsを呼び出し、画像表示部分の実装を行います。
`uploadStorage`の引数には上記で設定したfileListとbucketNameを入れます。
bucketNameにはバケット名を設定しましょう。
```typescript:top.tsx
    const { path } = await uploadStorage({
      folder,
      bucketName: "pictures",
    });
```

今回は`getPublicUrl()`を使用し、画像を閲覧できるパブリックな画像URLを生成します。
fromの引数には保存先のバケット名、`getPublicUrl()`の引数には`uploadStorage`によって設定されたフォルダー以下のパスを設定します。

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

https://supabase.com/docs/reference/dart/storage-from-getpublicurl

```typescript:top.tsx
const handleUploadStorage = async (folder: FolderList | null) => {
    if (!folder || !FolderList.length) return;
    const { path } = await uploadStorage({
      folder,
      bucketName: "pictures",
    });
    const { data } = supabase.storage.from("pictures").getPublicUrl(path)
    if (path) setPathName(data.publicUrl);
  };
```
画像表示機能に関わる全体のマークアップを下記のように行います。
画像アップロード時に、画像名の表示切り替えのために、onChangeイベントによって、`handleUploadStorage()`を呼び出します。
```jsx:top.tsx
 return (
    <label htmlFor="file-upload">
      <span>アップロードする</span>
      <input
        id="file-upload"
        name="file-upload"
        type="file"
        className="sr-only"
				accept="image/png, image/jpeg"
        onChange={(e) => {
          const fileList = e.target?.files;
          console.log(fileList);
          handleUploadStorage(fileList);
        }}
      />
      <img src={path} alt="" width="800" height="500"/>;
    </label>
  );
```
下記がマークアップした一覧画面の画像です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/e6b820c0-535e-7e81-1c01-ce274b50cc08.png"  width="500" height="186" />

# Storageで画像保存を確認しよう
上記のようにブラウザで画像表示が確認できたら、`Supabase`の`Storage`に画像が保存されているか確認しましょう。下記のように画像が保存されています。画像右下には`Download`と`Get URL`ボタンがあります。前者は保存した画像をダウンロードでき、後者は保存した画像URLを取得して、URLを叩くとブラウザで画像が表示できます。
![スクリーンショット 2023-01-27 21.12.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/d1bbf113-7033-50db-830d-37fa46d21f08.png)


# 最後に

これで実装はおしまいです！

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


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

# 追記
前回と前々回の**Supabase**画面がDarkだったのですが、今回はLightにしてみました〜٩(ˊᗜˋ*)و
**Dark**と**Light**しかまだバリエーションがないけれど、今後もっと増えると嬉しいなぁ♪
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/9ae01fc1-41b3-89a7-aae0-770f982c3cf8.png"  width="500" height="186" />



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

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

https://supabase.com/docs/reference/javascript/storage-from-upload

https://supabase.com/docs/reference/dart/storage-from-getpublicurl