2
1

【Next.js】画像投稿、更新機能ならSupabase RDB × storage

Posted at

これはなに

ここではSupabaseのRDBとstorageを使って、アップロードした画像の表示と、次回アクセス時にも画像を保持し、再びアップロードすれば更新できるようなフォームを作っていきます。
ここまで紹介しているような記事がなかなかなかったので、参考までに。

今回はNext.jsを使ったやり方を紹介します。API Routeも活用します。

Supabaseのセットアップ

今回は工程が多いので省きます。代わりにわかりやすく紹介している記事を紹介させていただきます。URLKEYを忘れずに保持しましょう。NEXT_PUBLIC_がないとクライアントでは取得できないのでつけておきます。
Nextの環境ができたらルートディレクトリにこの.env.localをおいてください。

.env.local
NEXT_PUBLIC_SUPABASE_URL = https://xxxxxxxxxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_KEY = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

RDB

「img-editor」というtable名で進めていきます。urlを挿入できるようにcolumnを定義しておきます。本当はあった方がよいですが今回はRLSはつけてません。

スクリーンショット 2024-03-16 10.56.29.png

Storage

「img-store」というbucket名で進めていきます。Publicにしておいてください。フォルダの作成は特に行いませんが、こちらはポリシーの設定が必要なので行っておきます。ここもテキトーです。「Policies」から...

→「New policy」
スクリーンショット 2024-03-16 11.01.24.png

→「Get started quickly」
スクリーンショット 2024-03-16 11.01.34.png

「Enable read access to everyone」を選択した状態で、→「Use this template」
スクリーンショット 2024-03-16 11.01.47.png

以下このように設定します。→「Review」
image.png

→「Save policy」
スクリーンショット 2024-03-16 11.04.59.png

とりあえずこれでAPIを叩けばすぐ使える状態になります。

Supabaseのクライアントを作る

今回はaxiosを使ってAPIにアクセスします。今回はわかりやすいようにClassを作って、メソッドで処理しましょう。@supabase/supabase-jsが必要なのでnpmの場合はこのようにインストールし、importしてください。

$ npm install @supabase/supabase-js
ImgEditor.js
import { createClient } from "@supabase/supabase-js";

export default class ImgEditor{
    constructor(supabaseUrl, supabaseKey){
        this.supabase = createClient(
            supabaseUrl,
            supabaseKey
        );
    };

    // 画像のURLをTableから取得する
    async getImageUrl(){
        const { data, error } = await this.supabase
            .from("img-editor")
            .select("src");
        if (error) {
            throw error;
        } else {
            return data[0];
        }
    };

    // 画像をアップロードする。成功したら画像のURLを返す
    async uploadImage(fileObj){
        const { data, error } = await this.supabase.storage
            .from("img-store")
            .upload(`upload-img.${fileObj.name.split(".")[1]}`, fileObj,
                { upsert: true }
            );
        if (error) {
            throw error;
        } else {
            const src = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/${data.fullPath}`
            const { _, error } = await this.supabase
                .from("img-editor")
                .upsert({ id: 1, src: src });
            
            if (error) {
                throw error;
            } else {
                return { src: src };
            }
        }
    };
};

ポイントなのは.upload(upload-img.${fileObj.name.split(".")[1]}, fileObj,{ upsert: true });の部分ですが、upload(保存先パス, fileObject, config)という引数になっており、{upsert: true}にすると上書き保存が許可されます。詳しくはこちら↓

Reactでフォームを作る

では、サンプルのフォームを作っていきます。今回は関数型コンポーネントで実装します。ハンドリングする箇所は一旦nullにしておきます。

page.jsx
export default function Index(){
    return (
        <>
            <input type="file" accept="image/*" onChange={null}/>
            <button onClick={null} disabled={null}>アップロード</button>
            <p>↓アップロードした画像</p>
            <img src={null} alt="selected-image" />
        </>
    )
};

こんな感じ
スクリーンショット 2024-03-18 1.03.11.png

選択画像のプレビュー

次に画像を選択するとimgに表示されるようにします。useStateを使います。Next v13からクライアントコンポーネントには先頭にuse clientがないと動作しないのでつけ忘れに注意しましょう。

page.jsx
'use client'

import { useState } from "react";
import ImgEditor from "@/src/ImgEditor";

export default function Page(){
    const [selectedImage, setImage] = useState();

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={null}
                disabled={
                    selectedImage == null ||
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};

imgのプレビューではFileReaderをつかうというアプローチもありますが、window.URL.createObjectURLを使う方が短く済むのでおすすめです。
ただし、nullが引数になった場合はエラーを吐くのでハンドリングします。
また、再アクセス時はアップロードされた画像のURLが入ることになるので、その場合もハンドリングしてやる必要があります。

初学者向けにまとめると、srcの部分をifで書くとこうです。

if (selectedImage) {
    if (typeof selectedImage == "string") {
        return selectedImage;
    } else {
        return window.URL.createObjectURL(selectedImage);
    }
} else {
    return null;
}

画像のアップロード

アップロードされた画像のFileObjectuseStateで管理「アップロード」ボタンのハンドリングで、作ったクラスメソッドのuploadImageを呼んでみましょう。

page.jsx
'use client'

import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";

// Supabaseクライアント
const imgEditor = new ImgEditor(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_KEY    
    );

export default function Page(){
    const [selectedImage, setImage] = useState();

    // 画像アップロードのハンドリング
    const saveImage = () =>{
        imgEditor.uploadImage(selectedImage)
            .then((data) => console.log(data))
            .catch((error) => console.error(error));
    };

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={saveImage}
                disabled={
                    selectedImage == null || 
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};

これでアップロードされた画像はstorageおよびtableに保存されるようになったはずです。
ではテキトーな画像をアップロードしてみましょう。コンソールに{src: ...}の形で返ってくれば成功です。
スクリーンショット 2024-03-18 2.57.29.png

Supabase側で確認してもちゃんと入っているか確認しましょう。

画像のURLを取得する。

最後に、再アクセス時に前回アップロードした画像をテーブルから拾ってくるようにしましょう。
useEffectとクラスメソッドのgetImgUrlを使いましょう。

page.jsx
'use client'

import { useEffect, useState } from "react";
import ImgEditor from "@/src/ImgEditor";

const imgEditor = new ImgEditor(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_KEY    
    );

export default function Page(){
    const [selectedImage, setImage] = useState();

    // 初回レンダリング時に画像URLを取得する
    useEffect(() => {
        imgEditor.getImageUrl()
            .then((data) => setImage(data.src))
            .catch((error) => console.error(error));
    }, []);

    const saveImage = () =>{
        imgEditor.uploadImage(selectedImage)
            .then((data) => console.log(data))
            .catch((error) => console.error(error));
    };

    return (
        <>
            <input 
                type="file" 
                accept="image/*" 
                onChange={(e) => setImage(e.currentTarget.files[0])}
            />
            <button 
                onClick={saveImage}
                disabled={
                    selectedImage == null ||
                    typeof selectedImage == "string"
                }
            >
                アップロード
            </button>
            <p>↓アップロードした画像</p>
            <img 
                src={
                    selectedImage ?
                    typeof selectedImage == "string" ? 
                    selectedImage :
                    window.URL.createObjectURL(selectedImage) :
                    null
                } 
                alt="selected-image"
            />
        </>
    )
};

これでページをリロードしても前回アップロードした画像が表示されたかと思います。

おわりに

今回は画像のアップロード、プレビュー、Supabaseへの保存、更新機能まで説明させていただきました。アカウント登録時のアバターやSNSなどの画像投稿機能なんかで使えそうですね。今回はパフォーマンスやセキュリティをガン無視しましたが、実際に使う場合はそこも意識して実装してみてはいかがでしょうか。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1