시작하기
이 글에서는 Next.js App Router에서 SSG 빌드 시 동적으로 OGP 이미지를 생성하는 방법을 소개합니다.
이하, Next.js가 언급되는 경우 Next.js App Router를 의미합니다.
이 글의 대상 독자
- Next.js App Router에서 Static Site Generation (SSG)을 사용하는 분
- OGP 생성 구현을 원하는 분
- 빌드 시 OGP를 생성하고 싶은 분
이 글의 환경
- Next.js 15.3.2
- @vercel/og (Next.js에 포함되어 있습니다)
OGP 기본
OGP란?
OGP는 Open Graph Protocol의 약자로, SNS 등에서 공유될 때 표시되는 이미지, 제목, 설명 등의 정보를 지정하기 위한 프로토콜입니다.
아래와 같은 링크 카드에 표시되는 정보가 OGP의 예시입니다.
OGP의 주요 메타 태그
OGP에서 사용되는 주요 메타 태그는 다음과 같습니다:
<meta property="og:title" content="페이지 제목" />
<meta property="og:description" content="페이지 설명" />
<meta property="og:type" content="website" />
<meta property="og:url" content="페이지 URL" />
<meta property="og:image" content="이미지 URL" />
<meta property="og:site_name" content="사이트 이름" />
Next.js에서의 OGP 구현 방법
1. 정적 OGP 설정
Next.js에서는 opengraph-image.(jpg|jpeg|png|gif)
라는 파일을 임의의 라우트에 배치하여 정적 OGP를 설정할 수 있습니다. 메타데이터에 자동으로 포함됩니다.
<meta property="og:image" content="<generated>" />
<meta property="og:image:type" content="<generated>" />
<meta property="og:image:width" content="<generated>" />
<meta property="og:image:height" content="<generated>" />
<meta name="twitter:image" content="<generated>" />
<meta name="twitter:image:type" content="<generated>" />
<meta name="twitter:image:width" content="<generated>" />
<meta name="twitter:image:height" content="<generated>" />
해당 라우트에 파일이 배치되지 않은 경우, 상위 라우트에 배치된 파일이 사용됩니다.
2. 동적 OGP 생성
App Router에서는 @vercel/og
가 포함되어 있어 next/og
를 사용하여 쉽게 OGP를 생성할 수 있습니다.
2.1 기본 구현
/posts/[slug]/og.png/route.tsx
와 같은 라우트에서 생성하면 https://example.com/posts/slug/og.png
에서 OGP를 가져올 수 있습니다.
이 구성에서는 /api/og
와 같은 엣지 함수를 사용하지 않고 빌드 시 OGP를 생성할 수 있습니다.
import path from 'path';
import { readFile } from 'fs/promises';
import { ImageResponse } from 'next/og';
// 빌드 시 정적으로 생성하도록 지정
export const dynamic = 'force-static';
// 생성되지 않은 경우 404 반환
export const dynamicParams = false;
interface Post {
slug: string;
title: string;
}
// 테스트용 데이터
const testPosts: Post[] = [
{
slug: 'test-post-1',
title: 'Test Post 1',
},
{
slug: 'test-post-2',
title: 'Test Post 2',
},
];
// 동적 매개변수를 사전에 정적으로 생성하기 위해
export async function generateStaticParams() {
return testPosts.map((post) => ({
slug: post.slug,
}));
}
export async function GET(_: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = testPosts.find((post) => post.slug === slug);
if (!post) {
return new Response('Not Found', { status: 404 });
}
try {
// 빌드 시 가져올 수 있는 이미지 URL이나 데이터가 없는 경우, 프로젝트의 이미지 파일을 다음과 같이 로드할 수 있습니다
const bgData = await readFile(path.join(process.cwd(), 'public', 'opengraph-image-bg.png'));
const bgSrc = Uint8Array.from(bgData).buffer;
const fontData = await loadFont(post.title);
return new ImageResponse(
(
<div
style={{
fontFamily: 'Noto Sans JP',
fontWeight: 700,
fontSize: 48,
color: 'white',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
}}
>
<img src={bgSrc} tw="w-full h-full object-cover" />
<div tw="flex flex-col items-center justify-center w-full h-full">
<div tw="text-4xl font-bold text-white">{post.title}</div>
</div>
</div>
),
{
fonts: [
{
name: 'Noto Sans JP',
data: fontData,
style: 'normal',
weight: 700,
},
],
width: 1200,
height: 630,
}
);
} catch (error) {
console.error('OG image generation error:', error);
return new Response('Failed to create OG image', { status: 500 });
}
}
2.2 폰트 로드
일본어를 포함한 OGP 이미지를 생성할 때는 폰트를 명시적으로 지정해야 합니다. 다음은 Google Fonts에서 서브셋 폰트를 로드하는 구현 예시입니다.
export async function loadFont(subset: string) {
try {
// Google Fonts에서 서브셋 폰트 가져오기
const fontResponse = await fetch(
`https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@700&text=${encodeURIComponent(subset)}`
);
if (!fontResponse.ok) {
throw new Error('Failed to fetch font CSS');
}
const fontCss = await fontResponse.text();
const fontUrl = fontCss.match(/url\((.*?)\)/)?.[1];
if (!fontUrl) {
throw new Error('Failed to extract font URL');
}
const fontDataResponse = await fetch(fontUrl);
if (!fontDataResponse.ok) {
throw new Error('Failed to fetch font data');
}
const fontData = await fontDataResponse.arrayBuffer();
return fontData;
} catch (error) {
console.error(error);
return new Response('Failed to load font', { status: 500 });
}
}
2.3 생성된 OGP 적용
생성된 OGP 이미지를 사이트에 적용하려면 다음과 같이 메타데이터를 설정합니다.
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata | undefined> {
const { slug } = await params;
return {
openGraph: {
images: [
{
url: `https://example.com/posts/${slug}/og.png`,
width: 1200,
height: 630,
alt: `Post ${slug}`,
type: 'image/png',
},
],
},
};
}
3. 구현 시 주의사항
-
폰트 로드
- 일본어를 포함할 경우 반드시 폰트를 명시적으로 지정해야 합니다
- 알파벳만 사용할 때도 제 환경에서는 오류가 발생했습니다
- Google Fonts의 서브셋 기능을 활용하여 필요한 문자만 로드할 수 있습니다
-
이미지 크기
- OGP 이미지의 권장 크기는
1200x630
픽셀입니다 - SNS에 따라 1:1로 크롭되는 경우가 많으므로, 중요한 내용을 중앙
630x630
영역에 배치하는 것이 좋습니다
- OGP 이미지의 권장 크기는
-
빌드 시 생성
dynamic = 'force-static'
을 지정하여 빌드 시 정적으로 생성됩니다generateStaticParams
에서 생성할 매개변수를 지정해야 합니다
-
오류 처리
- 폰트 로드 및 이미지 생성 시 적절한 오류 처리가 필요합니다
-
스타일 지정
div
의display
는 반드시flex | block | none
중 하나를 명시해야 합니다
-
메타데이터 설정
- 메타데이터는
generateMetadata
에서 설정할 수 있습니다 - 설정 누락이나 내용 불일치가 있으면 SEO에 부정적인 영향을 줄 수 있습니다
- 메타데이터는
요약
Next.js App Router에서는 라우팅에 약간의 수정을 통해 SSG 빌드 시 동적 OGP 이미지를 생성할 수 있습니다.
일본어 폰트의 적절한 로드와 메타데이터 설정에 주의하고, 적절한 이미지 크기를 설정하여 더 나은 OGP 이미지를 생성할 수 있습니다.