cloudinary를 사용해서 이미지를 관리해보자.
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;
처음 목표는 cloudinary api를 통해서 포트폴리오 이미지들을 특정 폴더에 업로드하는 것이었다.
그 과정에서 알게 된 것들을 적었다.
Upload Presets 부분에 가면 Mode가 Signed가 있고, Unsigned가 있다.
처음에 블로그 보고 따라할 때는 다들 Unsigned로 많이 한다. 나도 처음에 생성할 때 그렇게 했고.
둘의 차이는 뭘까?
Signed는 signature를 요구한다.
영어블로그 뒤적뒤적하니 upload code example에 sha1
라이브러리등을 이용해서 꼭 signature를 추가하더라.. 걔들은 모두 프리셋이 Signed일 것이다.
얘는 cloudinary에서 업로드시 나오는 full url중에서 / 구분자로 했을 때 가장 맨 뒷부분 url이다.
처음엔 기본 이미지 업로드로 들어간다.
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 });
}
}
위 블로그대로 해서 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
로 대체하라는 식의 내용이었다.
별도의 npm install 없이 바로 사용할 수 있다.
import Image from "next/image";
width
, height
속성과 동시에 fill
속성을 사용할 수 없다. 검색해보면 이미지 최적화에 관한 다양한 사례랑 블로그가 잘 나와서 공부하면 좋을듯.
공식에서도 이미지 최적화! 하고 박아버림
https://nextjs.org/docs/pages/building-your-application/optimizing/images
카카오 기술 블로그 사례