Next.js + Cloudinary

jinvicky·2024년 9월 12일
0
post-custom-banner

Intro


cloudinary를 사용해서 이미지를 관리해보자.

Support


Next.js가 Cloudinary를 위한 별도 라이브러리가 있다. 지원 잘해주네

npm install cloudinary-next

아래 설정을 기능 구현 이전에 미리 해줘야 한다.
next.config.mjs

const nextConfig = {
  compiler: {
    styledComponents: true,
  },
  images: {
    remotePatterns: [{ protocol: "https", hostname: "res.cloudinary.com" }],
  },
};

export default nextConfig;

TIL


처음 목표는 cloudinary api를 통해서 포트폴리오 이미지들을 특정 폴더에 업로드하는 것이었다.
그 과정에서 알게 된 것들을 적었다.

Signed vs. Unsigned

Upload Presets 부분에 가면 Mode가 Signed가 있고, Unsigned가 있다.
처음에 블로그 보고 따라할 때는 다들 Unsigned로 많이 한다. 나도 처음에 생성할 때 그렇게 했고.

둘의 차이는 뭘까?

Signed는 signature를 요구한다.

영어블로그 뒤적뒤적하니 upload code example에 sha1 라이브러리등을 이용해서 꼭 signature를 추가하더라.. 걔들은 모두 프리셋이 Signed일 것이다.

publicId

얘는 cloudinary에서 업로드시 나오는 full url중에서 / 구분자로 했을 때 가장 맨 뒷부분 url이다.

Code


처음엔 기본 이미지 업로드로 들어간다.

src/app/api/file/route.ts

export async function POST(req) {
  try {
    const formData = await req.formData();
    const file = formData.get("file");

    if (!file) {
      return NextResponse.json({ success: false, message: "no file found" });
    }
 
    // 헤더로부터 image, video 등의 리소스타입을 받는다.
    const resourceType = headers().get('resourceType');

    const uploadResponse = await fetch(
      `https://api.cloudinary.com/v1_1/${process.env.CLOUDINARY_CLOUD_NAME}/${resourceType}/upload`,
      {
        method: "POST",
        body: formData,
      }
    );

    const uploadedImageData = await uploadResponse.json();
    return NextResponse.json({
      uploadedImageData,
      message: "Success",
      status: 200,
    });

  } catch (error) {
    return NextResponse.json({ message: "Error", status: 500 });
  }
} 

https://www.linkedin.com/pulse/how-upload-delete-images-using-cloudinary-api-nextjs-rishabh-tak-oxioc/

위 블로그대로 해서 upload 1차에 성공.

  • .env 파일에 필수 정보 등록하고 (gitignore에 추가)
  • /클라우드명/업로드 유형/upload라서 업로드 유형은 헤더에서 받아옴.


이거 formData로 보낼때 변수명 위의 걸로 잘 지켜야 한다. (내맘대로 fileList이런거 당연히 안됨;;)

이걸 하고 나니...난 폴더별로 이미지 분류하고 싶은데? 생각이 든다.

https://cloudinary.com/documentation/image_upload_api_reference

여기를 보니 선택 파라미터들 중에 asset_folder, folder 라는 게 있다.
이걸 추가하면 리턴값중에 asset_folder에 입력값이 나온다.

실제로 cloudinary 콘솔에도 profile 폴더가 신규 생성된 것을 볼 수 있다.
asset_folder와 folder의 차이점이 뭘까...

공식에서 POSTMAN을 예제로 들어서 API 샘플을 정리해놨다.

https://www.postman.com/cloudinaryteam/programmable-media/request/mbkq17c/upload-file-unsigned

이미지 출력하기

로스트아크 포폴용 이미지 리스트를 출력해보자.
일단 portfolio라는 테이블에 insert하는 api는 아래처럼 짰다.

/**
 * formData 필수 항목
 * 1. genre (이것 제외 변수명 변경 안됨 - cloudinary에서 사용)
 * 2. file
 * 3. upload_preset
 * 4. asset_folder
 *
 * @param {*} req
 * @returns
 */
export async function POST(req) {
  const formData = await req.formData();

  const resourceType = headers().get("resourceType") ?? "image";

  const uploadResp = await fetch(
    `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/${resourceType}/upload`,
    {
      method: "POST",
      body: formData,
    }
  );

  const {
    public_id,
    version,
    signature,
    format,
    resource_type,
    asset_folder,
    original_filename,
    created_at,
  } = await uploadResp.json();

  const sql = `
  INSERT INTO portfolio (uuid, public_id, version, signature, format, resource_type, asset_folder, original_filename, genre, created_at)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`;

  const db = await openDb();
  try {
    const result = await db.run(sql, [
      uuidv4(),
      public_id,
      `v${version}`,
      signature,
      format,
      resource_type,
      asset_folder,
      original_filename,
      formData.get("genre"),
      created_at,
    ]);

    return NextResponse.json({
      status: 200,
      message: "Success",
      data: result,
    });
  } catch (err) {
    return NextResponse.json({
      status: err.status || 500,
      message: err.message || "Internal Server Error",
      data: err,
    });
  }
}

url에는 버전 앞에 v가 붙어있던데 version 값 자체에는 없길래 내가 붙였다.

나는 응답값의 secure_url을 보고 아래처럼 src를 조합해서 출력해보았다.
(아래는 localhost 하드코딩 및 개선이 필요한 버전인데 일단 올린다)

"use client";

import { useEffect, useState } from "react";
import axios from "axios";

import Image from "next/image";

export default function LostarkPortfolioList() {
  const [data, setData] = useState([]);
  const fetchPortfolioList = async () => {
    const resp = await axios.get("http://localhost:3000/api/portfolio", {
      params: {
        genre: "LOSTARK",
      },
    });

    setData(resp.data.data);
  };
  useEffect(() => {
    fetchPortfolioList();
  }, []);

  return (
    <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
      {data.map((item) => (
        <div key={item.id} className="flex flex-col items-center">
          <Image
            src={`${process.env.NEXT_PUBLIC_CLOUDINARY_BASE_URL}/${item.version}/${item.public_id}.${item.format}`}
            alt={item.original_filename}
            width={300} // 원하는 너비
            height={300} // 원하는 높이
            className="object-cover rounded-lg"
          />
        </div>
      ))}
    </div>
  );
}

환경변수 주의

클라이언트 단에서 NEXT_PUBLIC_ 붙이지 않으면 undefined가 뜬다.

src={`${process.env.NEXT_PUBLIC_CLOUDINARY_BASE_URL}/${item.version}/${item.public_id}.${item.format}`}

이런식으로 나온다. 출력할 때는 Image 컴포넌트를 사용했다.
소나큐브 플러그인을 vscode에서 사용하는데 warning이 떠서 내용이 Image 로 대체하라는 식의 내용이었다.

Image 컴포넌트

별도의 npm install 없이 바로 사용할 수 있다.

import Image from "next/image";
  • width, height 속성과 동시에 fill 속성을 사용할 수 없다.

검색해보면 이미지 최적화에 관한 다양한 사례랑 블로그가 잘 나와서 공부하면 좋을듯.

https://velog.io/@onedanbee/NextJS-사용해-이미지-최적화-하기

공식에서도 이미지 최적화! 하고 박아버림

https://nextjs.org/docs/pages/building-your-application/optimizing/images

카카오 기술 블로그 사례

https://fe-developers.kakaoent.com/2022/220714-next-image/

profile
경험하고 공부한 것들 풀어서 쓰는 것을 좋아합니다
post-custom-banner

0개의 댓글