Next.js 15에서 동적으로 오픈그래프 이미지 생성 하기
소셜 미디어에서 링크를 공유할 때 보이는 이미지, 즉 오픈그래프(Open Graph) 이미지는 사용자의 클릭률을 크게 좌우하는 중요한 요소이다.
블로그나 콘텐츠 사이트에서 각 포스트마다 고유한 오픈그래프 이미지를 일일이 제작하는 것은 현실적이지 않기 때문에 동적으로 오픈그래프 이미지를 생성하는 것이 효과적이다.
Next.js v13.3.0에서 소개된 opengraph-image
메타데이터 파일을 사용하여 동적으로 오픈그래프 이미지를 생성해보겠다.
다음은 완성본이다.
로딩 중...
디스코드에서 공유할 때 보이는 이미지.
로딩 중...
사용법
기본적인 사용법은 다음과 같다.
import { ImageResponse } from 'next/og'
export default async function OpengraphImage() {
return new ImageResponse(
(
// JSX 형태
<div>
Hello world!
</div>
),
);
}
Props로 전달할 수 있는 옵션은 다음과 같다.
export default function Image({ params }: { params: { slug: string } }) {
// ...
}
폰트도 추가하고 싶다면 다음과 같이 추가할 수 있다.
//...
export default async function Image() {
const interSemiBold = await readFile(
join(process.cwd(), 'assets/Inter-SemiBold.ttf')
)
return new ImageResponse(
(
// JSX 형태
<div>
Hello world!
</div>
),
// ImageResponse options
{
...size,
fonts: [
{
name: 'Inter',
data: interSemiBold,
style: 'normal',
weight: 400,
},
],
}
)
}
ImageResponse
API는 이 문서 에서 자세히 설명하고 있다.
오픈그래프 이미지 jsx로 작성하기
오픈그래프를 생성하는데 사용되는 @vercel/og
패키지는 내부적으로 Satori를 사용하기 때문에
모든 CSS를 지원하지 않는다.1
따라서 이 점을 유의하며 오픈그래프 이미지를 생성하기 위한 JSX 코드를 작성한다.
import { getMarkdownFile, getMarkdownFileWithFetch } from "@/app/action/markdown";
import { readFileSync } from "fs";
import { ImageResponse } from "next/og";
import { join } from "path";
const size = {
width: 1200,
height: 630,
};
export async function generateImageMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const file = await getMarkdownFile(slug);
return [
{
id: slug,
size,
contentType: "image/png",
alt: file?.title || "",
},
];
}
async function loadSeouAlrimFont(font: string) {
const data = await fetch(`${prefixUrl}/fonts/${font}.ttf`).then(
(res) => res.arrayBuffer()
);
if (!data) {
throw new Error(`Font file not found: ${font}.ttf`);
}
return data;
}
export default async function OpengraphImage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const file = await getMarkdownFileWithFetch(slug);
const { title, tags } = file || { title: "", tags: [] };
const imageOptions: any = {
...size,
fonts: [
{
name: "SeoulAlrimTTF-Medium",
data: await loadSeouAlrimFont("SeoulAlrimTTF-Medium"),
style: "normal",
weight: 400,
},
{
name: "SeoulAlrimTTF-Heavy",
data: await loadSeouAlrimFont("SeoulAlrimTTF-Heavy"),
style: "normal",
weight: 700,
},
]
}
return new ImageResponse(
(
<div
style={{
flexDirection: "column",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
gap: "24px",
backgroundColor: "#09090b",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundImage: `
linear-gradient(rgba(39, 39, 42, 0.4) 1px, transparent 1px),
linear-gradient(90deg, rgba(39, 39, 42, 0.4) 1px, transparent 1px)
`,
backgroundSize: "40px 40px",
display: "flex",
}}
/>
{/* Subtle gradient overlay */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "radial-gradient(circle at center, rgba(24, 24, 27, 0.7) 0%, rgba(9, 9, 11, 0.9) 70%)",
display: "flex",
}}
/>
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: "1300px",
height: "1300px",
borderRadius: '100%',
border: '10px dashed #3f3f46',
}} />
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "120px",
borderRadius: "100%",
padding: "16px",
width: "160px",
height: "160px",
}}
>
✨
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: "64px",
fontWeight: 500,
fontFamily: "SeoulAlrimTTF-Heavy",
flexWrap: "wrap",
width: "100%",
textAlign: "center",
padding: "0 16px",
textShadow: "0 2px 4px rgba(0, 0, 0, 0.5)",
}}
>
{title}
</div>
<div
style={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
gap: "8px",
}}
>
{tags &&
tags.slice(0, 5).map((tag: string, index: number) => (
<div
key={index}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: "32px",
fontWeight: 500,
fontFamily: "SeoulAlrimTTF-Heavy",
padding: "16px 24px",
borderRadius: "100px",
backgroundColor: "#27272a",
border: "1px solid #3f3f46",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.3)",
}}
>
#{tag}
</div>
))}
</div>
</div>
),
imageOptions
);
}
로컬에서 OG 이미지 미리 보기
개발자 도구를 열고 Elements 탭에서 head 태그를 열어보면 meta 태그 중 og:image
속성을 찾을 수 있다.
로딩 중...
content
를 복사 한 후 접속하면 이미지를 미리 볼 수 있다.
로딩 중...
추가로
OG Image Playground에서 오픈그래프 이미지를 미리 그려볼 수 있다.
로딩 중...
기본적으로 생성된 이미지는 dynamic API나 캐시되지 않은 데이터를 사용하지 않는 한 빌드 타임에 정적으로 최적화되고 캐시된다.2