1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js14】×【GCS】最新のNext.js機能AppRouterに対応した非公開GCSからの画像取得を実装してみました

Last updated at Posted at 2024-05-12

最新のNext.js機能App Routerに対応したGCSからのデータ取得の記事が見つからなくて困ったので書いてみました!
すでにGCS上にデータのアップロードが完了しているところからの説明になります。参考になれば嬉しいです!

この記事でわかること

最新のNext.js機能App Routerに対応した

  • APIルートの作り方
  • リクエストBodyの書き方
  • 非公開GCS(Google Cloud Storage)から認証してもらう方法

1.APIルートを作る

1-1.APIルートを作るためのフォルダを用意する

AppRputerではこれまでと違い、下記のようなフォルダ構成にするとAPIルートになります
app/apiフォルダ内に任意のフォルダを作り、そのフォルダの名がAPIからデータを取るときのURL名になります(今回はgetProfImage)
フォルダに保管するデータは共通でroute.jsにするところもこれまでと違いますね

my-app  //名前は自由につけていいです//
├─ src
│   ├ app
│      ├ api
│        └ getProfImage  //名前は自由につけていいです//
│           └ route.js
│   ├ page.js
│   └ favicon.ico
│
├─ .gitignore
├─ package.json 
├─ package-lock.json
└─ README.md

1-2.route.jsを書く

ここはサーバーサイドでGCRにアクセスするための有効期間を指定してSigned URLを生成する機能を持ちます
サーバーサイドなのでより安全にURLを管理できるってことですね
環境変数は後で設定します

/src/app/api/getProfImage/route.js
import { NextResponse } from "next/server";
import { Storage } from '@google-cloud/storage';

export async function GET(request) {
  const urlParams = new URL(request.url).searchParams;
  const fileName = `items/${urlParams.get('file')}`;

  if (!fileName) {
    return new NextResponse(JSON.stringify({ error: 'File name is missing' }), { status: 400 });
  }

  const storage = new Storage({
    projectId: process.env.GCS_PROJECT_ID,
    keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS_JSON,
  });

  const bucketName = process.env.BUCKET_NAME ?? '';
  const bucket = storage.bucket(bucketName);
  const file = bucket.file(fileName);

  try {
    const options = {
      version: 'v4',
      action: 'read',
      expires: Date.now() + 5 * 60 * 1000, // 5 minutes from now
    };
    const [url] = await file.getSignedUrl(options);
    return new NextResponse(JSON.stringify({ url }), { status: 200 });
  } catch (error) {
    console.error('Failed to create signed URL:', error);
    return new NextResponse(JSON.stringify({ error: `Failed to create signed URL: ${error.message}` }), { status: 500 });
  }
}

NextResponseを使う、GETメソッドを明示する点がこれまでと違います

 1-2-2.従来からの変更点解説(コピペだけしたい人はスルーして大丈夫です!)

  • 従来
/pages/api/getProfImage.js
export default function handler(req, res){
  ...
  return res.status(200).json({url})
}
  • Approuter対応
      GETメソッドが明示され、resNextResponseに置き換えられていますね
/src/app/api/getProfImage/route.js
export async function GET(request) {
  ...
  return new NextResponse(JSON.stringify({ url })
}

1-3.next.config.mjsに追記

GCSから画像を取ってくることを許可するために下記を追記します

/next.config.mjs
  images: {
    domains: ['storage.googleapis.com'],
  },

1-4.@google-cloud/storage モジュールがプロジェクトにインストール

下記コマンドをターミナルで叩き @google-cloud/storage パッケージをインストールします

  npm install @google-cloud/storage

2.データを取ってくるサービスアカウントを準備する

GCPのページでデータを取得する権限のあるサービスアカウントをつくります
データを取ってきたいGCSのバケットのページ左上のハンバーガーアイコンをクリック

IAMと管理>サービスアカウント>をクリック

画面上部のサービスアカウントを作成をクリック

image.png

好きな名前とIDを入力

image.png

②このサービス アカウントにプロジェクトへのアクセスを許可する

でこのアカウントに付与したい権限のロールを選択します
PJの規模・運用方法に合わせて選択してください

image.png

その他はスルーして、認証キーをダウンロードします
作成したアカウントのキータブにある鍵を追加をクリック

image.png

◉ JSON形式を選択して作成をクリックしキーをダウンロードします

image.png

gcs_credential.jsonと名前をつけて.env.localと同じ階層に保管し
.gitignoreに追加します

/.gitignore
# local env files
.env*.local
gcs_credential.json

キーは大切なデータなので絶対に.gitignoreに入れるのを忘れないでください

3..env.localに必要な情報を入れる

BUCKET_NAMEとGCS_PROJECT_IDはPJによって違うので下記記載の通りに書いてみてください

BUCKET_NAME=バケットの名前を記載
GCS_PROJECT_ID= gcs_credential.json内にある"project_id"を記載
GOOGLE_APPLICATION_CREDENTIALS_JSON=./gcs_credential.json

.env.local内に記載する文字列は""’’は不要で直接記載します
サーバーサイドで使用するキーなのでNEXT_PUBLIC_は付けないでください

4.画像表示用のクライアントサイドのファイルを作る

ここまできたらあとは画像を表示するだけです!
先ほどサーバーサイドのroute.jsで作った時間制限のあるURLを使ってGCSにアクセスして画像を表示します

下記に全体のコードを示しますが、一例なのでPJに合わせて書き換えてみてください

/src/components/GCSImageLoader.js
import { useEffect, useState } from 'react';
import Image from 'next/image';

const fetchSignedUrlFromGCS = async (itemId, folder) => {
  const fileName = `${folder}/${itemId}.png`;
  const url = `/api/getProfImage?file=${encodeURIComponent(fileName)}`;

  try {
    const response = await fetch(url, {
      headers: {
        'Cache-Control': 'max-age=3600',
      },
    });
    if (!response.ok) {
      throw new Error(`Failed to load image with status: ${response.status}`);
    }
    const data = await response.json();
    return data.url;
  } catch (error) {
    console.error('Error fetching signed URL:', error);
    throw new Error(`Error fetching signed URL: ${error.message}`);
  }
};

const GCSImageLoader = ({ imageUrl, error, width, height, objectFit = 'cover',quality , loadingStrategy}) => {
  return (
    <div style={{ position: 'relative', width: `${width}px`, height: `${height}px`, overflow: 'hidden' }}>
      {error && <p>Error loading image: {error}</p>}
      <Image
        src={imageUrl}
        alt={error ? "Failed to load image" : "Product Image"}
        quality={quality}
        placeholder="blur"
        sizes="10vw"
        fill
        blurDataURL="/static/img/default-image.webp"
        loading={loadingStrategy}
        style={{
          objectFit: objectFit,
          objectPosition: 'center',
          width: '100%',
          height: '100%',
        }}
      />
    </div>
  );
};

const ParallelImageLoader = ({ items, folder, width, height, quality ,index ,objectFit }) => {
  const [images, setImages] = useState([]);

  useEffect(() => {
    const fetchImages = async () => {
      const promises = items.map(async (item) => {
        try {
          const url = await fetchSignedUrlFromGCS(item.id, folder);
          return { id: item.id, url, error: null };
        } catch (error) {
          return { id: item.id, url: '/static/img/default-image.png', error: error.message };
        }
      });

      const results = await Promise.all(promises);
      setImages(results);
    };

    fetchImages();
  }, [items, folder]);

  const loadingStrategy = index < 8 ? 'eager' : 'lazy';

  return (
    <>
      {images.map((image) => (
        <GCSImageLoader
          key={image.id}
          imageUrl={image.url}
          error={image.error}
          width={width}
          height={height}
          quality={quality}
          loading={loadingStrategy}
          objectFit= {objectFit}
        />
      ))}
    </>
  );
};

export { GCSImageLoader, ParallelImageLoader };

共通のポイント
URLに記載しているgetProfImageroute.jsを入れているフォルダ名にします

/src/components/GCSImageLoader.js
const url = `/api/getProfImage?file=${encodeURIComponent(fileName)}`;

PJごとに変える必要のある設定

  • 今回は保管しているデータ形式が.pngなので拡張子を指定しています。
  • 別ルートでバックからもらっているitemIdに紐づけてデータを取ってこれるように、ファイル名はitemId.pngとして保管してもらっているので変数${itemId}.pngとして記載しています
/src/components/GCSImageLoader.js
const fileName = `${itemId}.png`;
  • 画像を最適化できるNext.jsの機能を使用したいので<Image>タグを使用していますが、<image>でも問題なく動きます
/src/components/GCSImageLoader.js
import Image from 'next/image';
・・・
        <Image
        src={imageUrl}
        alt={error ? "Failed to load image" : "Product Image"}
        quality={quality}
        placeholder="blur"
        sizes="10vw"
        fill
        blurDataURL="/static/img/default-image.webp"
        loading={loadingStrategy}
        style={{
          objectFit: objectFit,
          objectPosition: 'center',
          width: '100%',
          height: '100%',
        }}
        />

参考にさせていただいたサイト

皆さんの情報を参考にさせていただきました。
本当にわかりやすい記事で感動しました、この場を借りてお礼させていただきたいです。ありがとうございます。

Next.js Appフォルダでの変更点を20個、時短で紹介
Next.jsからgcsに画像を登録したり呼び出したりしたい!!!!

最後に

最後まで読んでいただきありがとうございます!
AppRouter移行後の情報が少なく、悩んでしまったので記事にしてみました
少しでも参考になれば嬉しいです。

1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?