Open Graph 태그 활용
Open Graph의 제목·설명·URL·이미지 속성을 정적·동적 메타데이터로 설정해 공유 링크 미리보기를 제어합니다.
웹 페이지가 페이스북, X(트위터), 링크드인 같은 소셜 미디어에 공유될 때, 링크만 보이는 것보다 제목, 설명, 이미지가 담긴 미리보기가 함께 보이면 사용자가 링크의 내용을 판단하기 쉬워집니다. 클릭률이 실제로 나아졌는지는 이용 데이터로 확인합니다. 이런 미리보기를 제어하는 표준 프로토콜이 Open Graph(OG) 프로토콜입니다.
Open Graph 태그는 HTML <head>에 들어가는 메타 태그로, 소셜 미디어가 페이지 콘텐츠를 해석해 미리보기로 보여 주는 데 필요한 구조화된 정보를 제공합니다. App Router는 Open Graph를 설정하는 API를 제공합니다. 이 글에서는 Open Graph 태그가 왜 중요한지, 주요 속성은 무엇인지, App Router에서 어떻게 설정하는지, 그리고 효과적으로 활용하는 전략을 알아봅니다.
Open Graph 프로토콜이란?
Open Graph 프로토콜은 페이스북이 웹 페이지를 소셜 그래프의 객체로 통합하려고 2010년에 도입한 기술입니다. 이후 많은 소셜 미디어와 카카오톡, 슬랙 같은 메신저 앱이 이 프로토콜을 받아들여, 링크를 공유할 때 일관되고 풍부한 미리보기를 보여 줍니다. OG 태그가 제대로 설정되지 않으면 플랫폼이 페이지의 제목, 설명, 이미지를 임의로 뽑아 보여 줄 수 있고, 미리보기가 어색하거나 정보가 빠져 클릭률이 낮아질 수 있습니다.
주요 Open Graph 태그 속성
OG 태그는 og: 접두사로 정의하며, 가장 흔히 쓰는 속성은 다음과 같습니다.
og:title은 필수 속성인 웹 페이지의 제목으로, 페이지 내용을 짧고 분명하게 나타내며 표시 길이는 플랫폼과 화면에 따라 달라집니다. og:type도 필수이며 콘텐츠의 유형을 나타내고, 일반 웹사이트인 website, 블로그 게시물이나 기사인 article, 그 밖에 book, profile, video.movie 등이 있습니다. og:image는 미리보기에 보일 이미지의 URL로, 공유 대상이 읽을 수 있는 절대 URL을 씁니다. 1200 × 630px은 큰 미리보기에 흔히 쓰는 예시 크기일 뿐, Open Graph 프로토콜이 정한 고정 최소 규격은 아닙니다. og:url은 웹 페이지의 대표 URL로, 페이지에 접근할 수 있는 정식 주소여야 합니다.
선택 속성도 있습니다. og:description은 제목을 보충하는 짧은 설명이며, 프로토콜이 일정한 글자 수를 보장하거나 모든 플랫폼에 같은 길이를 요구하지는 않습니다. og:site_name은 제목과 겹치지 않게 웹사이트의 브랜드를 나타내는 이름이고, og:locale은 한국어의 ko_KR, 미국 영어의 en_US 같은 언어와 지역입니다. og:image:width와 og:image:height는 og:image에 지정한 이미지의 픽셀 너비와 높이를 전하는 정보일 뿐, 본문 이미지의 공간 예약이나 미리 로딩을 수행하는 설정이 아닙니다. og:type이 article이라면 article:published_time, article:author처럼 그 유형에 맞는 추가 속성을 정의할 수 있습니다.
이 속성들을 HTML로 쓰면 다음과 같습니다.
<meta property="og:title" content="Next.js로 배우는 SEO 가이드" />
<meta property="og:type" content="article" />
<meta property="og:image" content="https://commerce-lab.dev/images/seo-guide-og.png" />
<meta property="og:url" content="https://commerce-lab.dev/blog/nextjs-seo-guide" />
<meta property="og:description" content="Next.js App Router에서 Open Graph 태그를 활용하여 소셜 미디어 공유를 최적화하는 방법을 알아봅니다." />
<meta property="og:site_name" content="My Next.js Blog" />
<meta property="og:locale" content="ko_KR" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />이 태그들은 페이지의 <head> 안에 들어가며, 일반 설명 태그인 <meta name="description">과 달리 name이 아니라 property 속성에 이름을 적습니다. 공유 미리보기를 만드는 플랫폼은 링크의 HTML을 내려받아 이 property 값을 찾으므로, 속성 이름을 name으로 잘못 쓰면 Open Graph 태그로 읽히지 않을 수 있습니다.
Next.js App Router에서 Open Graph 태그 설정하기
App Router에서는 13장 1절과 2절에서 다룬 metadata 객체로 Open Graph 태그를 설정합니다. metadata.openGraph에 속성을 정의하면 Next.js가 관련 <meta property="og:..." /> 태그를 만들어 주며, 동적 메타데이터의 스트리밍과 봇별 처리 시점은 앞 글에서 구분한 방식을 따릅니다.
정적 Open Graph 태그 설정
애플리케이션 전체의 기본 Open Graph 정보는 루트 레이아웃인 src/app/layout.tsx에 설정합니다. 아래 코드의 metadataBase는 메타데이터 안의 상대 경로 URL을 절대 URL로 바꿀 때 기준이 되는 주소입니다. 이 예제는 이미지 URL을 처음부터 절대 URL로 적었지만, metadataBase가 있으므로 '/og-default.png'처럼 경로만 적어도 같은 주소로 해석됩니다.
import type { Metadata } from 'next';
// ... (기존 임포트 및 폰트 설정) ...
export const metadata: Metadata = {
metadataBase: new URL('https://commerce-lab.dev'),
// ... (기존 title, description, keywords 등) ...
// Open Graph 설정
openGraph: {
title: '나만의 Next.js 웹사이트',
description: 'Next.js App Router를 활용하여 구축된 현대적인 웹사이트입니다.',
url: 'https://commerce-lab.dev', // 웹사이트의 기본 URL
siteName: 'Next.js 데모 사이트', // 웹사이트 이름
images: [
{
url: 'https://commerce-lab.dev/og-default.png', // 기본 OG 이미지 (공유될 때 표시)
width: 1200,
height: 630,
alt: 'Next.js 데모 사이트 로고',
},
],
locale: 'ko_KR',
type: 'website',
},
// ... (twitter, robots 등 기타 메타데이터) ...
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>{children}</body>
</html>
);
}openGraph의 각 키는 앞 절의 태그로 바뀝니다. 예를 들어 siteName은 og:site_name이 되고, images 항목의 width와 height는 og:image:width와 og:image:height가 됩니다. 하위 페이지가 openGraph를 따로 선언하지 않는 한, 모든 페이지가 이 기본값을 그대로 씁니다.
동적 Open Graph 태그 생성
블로그 게시물이나 상품 상세 페이지처럼 URL 파라미터에 따라 내용이 달라지는 경우에는 generateMetadata로 Open Graph 태그를 만듭니다. 하위의 openGraph는 상위 객체를 통째로 바꾸므로, 다음 예제는 필요한 상위 이미지 배열만 parent에서 읽어 명시적으로 합칩니다. 6장의 products/[productId] 폴더가 같은 프로젝트에 있다면, 같은 위치에 이름이 다른 동적 세그먼트를 함께 둘 수 없으므로 별도의 프로젝트에서 시험하거나 폴더 이름을 맞춥니다.
import type { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';
// 가상의 상품 데이터 조회 함수
async function getProduct(id: string) {
const products = [
{ id: '1', name: '고급 Next.js 신발', description: '편안함과 스타일을 모두 잡은 신발입니다.', price: 120000, imageUrl: 'https://example.com/images/shoe1.jpg' },
{ id: '2', name: 'React 개발자 후드티', description: '개발자를 위한 편안하고 트렌디한 후드티입니다.', price: 55000, imageUrl: 'https://example.com/images/hoodie2.jpg' },
];
return products.find(p => p.id === id);
}
type Props = {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata // 상위 메타데이터 (루트 layout.tsx 등)
): Promise<Metadata> {
const { id } = await params;
const product = await getProduct(id);
if (!product) {
notFound();
}
// 상위 openGraph 이미지를 가져와 현재 이미지와 병합
const previousImages = (await parent).openGraph?.images || [];
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
url: `https://commerce-lab.dev/products/${product.id}`,
siteName: 'Next.js 쇼핑몰', // 웹사이트 이름 (필요에 따라 덮어쓸 수 있음)
images: [
{
url: product.imageUrl,
width: 800,
height: 600,
alt: product.name,
},
...previousImages, // 기본 OG 이미지도 함께 포함 (선택 사항)
],
type: 'website', // Metadata 타입이 지원하는 Open Graph 유형을 사용
},
// Twitter Card도 비슷하게 설정 가능
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.imageUrl],
}
};
}
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await getProduct(id);
if (!product) {
notFound();
}
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '20px auto', border: '1px solid #FF5722', borderRadius: '10px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
<h1 style={{ color: '#FF5722', textAlign: 'center', marginBottom: '20px' }}>{product.name}</h1>
<p style={{ textAlign: 'center', fontSize: '1.2em', color: '#666' }}>가격: {product.price.toLocaleString()}원</p>
{product.imageUrl && (
<img src={product.imageUrl} alt={product.name} style={{ maxWidth: '100%', height: 'auto', display: 'block', margin: '20px auto' }} />
)}
<p style={{ lineHeight: 1.6, fontSize: '1.1em', color: '#333' }}>{product.description}</p>
</div>
);
}이 예제의 getProduct는 고정된 배열을 조회하며, generateMetadata와 본문에서 각각 호출됩니다. 자동으로 한 번만 실행되는 네트워크 요청을 보여 주는 코드는 아니며, 외부 이미지 주소도 준비된 실제 자산으로 바꿔야 합니다.
배열이 어떻게 구성되는지 따라가 보겠습니다. 상품 id='1'의 페이지에서 부모의 OG 이미지가 한 개라면, 메타데이터 함수는 상품 이미지와 부모 이미지 순서로 OG 이미지 두 개를 돌려주고, Twitter 이미지 배열에는 상품 이미지 하나만 돌려줍니다. 코드가 두 배열을 각각 만들기 때문에 개수가 같을 필요는 없으며, 어느 이미지가 공유 카드에 선택될지는 플랫폼의 처리까지 확인해야 합니다.
Next.js의 Metadata 타입에는 Open Graph의 product 유형이 없습니다. 상품명, 가격, 재고처럼 검색 엔진에 전할 상품의 의미는 Product JSON-LD로 따로 표현합니다.
효과적인 Open Graph 태그 활용 전략
운영 환경에서는 태그를 작성하는 것만큼, 공유 디버거로 확인하고 플랫폼의 캐시를 갱신하는 절차도 중요합니다.
og:image는 소셜 미리보기의 핵심이므로, 대상 플랫폼의 표시 크기와 잘림 방식을 고려해 페이지 내용을 잘 나타내는 이미지를 준비합니다. 플랫폼이 이미지를 줄이거나 잘라 낼 수 있으므로, 중요한 글자를 이미지 위에 직접 얹는 것은 피하는 것이 좋습니다. 소셜 플랫폼에 출력되는 og:url과 og:image는 절대 URL이어야 합니다. Metadata API에서는 metadataBase를 설정하면 상대 경로를 써도 절대 URL로 해석되므로, 만들어진 <meta> 값이 올바른 공개 주소인지 확인합니다.
소셜 플랫폼은 OG 태그를 캐싱하는 경향이 있습니다. 태그를 고친 뒤에는 Facebook 공유 디버거처럼 플랫폼이 제공하는 검사와 재수집 기능으로 반영 상태를 확인합니다. 재수집을 요청해도 모든 공유 카드의 캐시가 곧바로 바뀌지는 않습니다. X(Twitter)의 Card Validator는 지금 미리보기 기능을 제공하지 않으므로, 실제 게시물 작성 화면에서 카드가 보이는지 확인합니다.
페이지 성격에 맞는 og:type을 설정해 검색 엔진과 소셜 플랫폼에 더 정확한 정보를 줍니다. 예를 들어 영화 페이지에는 video.movie를, 다른 범주에 속하지 않는 동영상에는 video.other를 씁니다. 레시피 페이지처럼 맞는 유형이 따로 없으면 article이나 website를 고르고, article을 골랐을 때만 article:published_time 같은 추가 속성을 함께 씁니다.
X(Twitter)는 자체 twitter: 메타 태그를 쓰지만 og: 태그를 대체값(fallback)으로 활용하므로, og: 태그를 잘 설정해 두면 기본적인 미리보기는 가능합니다. 다만 summary_large_image 같은 고유한 카드 타입을 쓰려면 twitter: 태그를 따로 설정하는 것이 좋습니다. og:locale과 og:locale:alternate는 언어와 지역 코드를 나타낼 뿐, 언어별 페이지 URL을 연결하는 hreflang 선언과는 별개입니다. og:url은 소셜 그래프에서 객체를 식별하는 주소이므로, 검색용 rel="canonical"이 필요하면 alternates.canonical도 선언합니다.
Open Graph 태그는 공유 미리보기의 제목, 설명, 이미지를 분명히 전하는 메타데이터이며, App Router의 내장 메타데이터 기능을 쓰면 이 태그들을 쉽게 관리해 사용자에게 알맞은 공유 경험을 줄 수 있습니다.
더 깊이: 소셜 크롤러가 동적 OG 태그를 받는 방식
Next.js 15.2부터 generateMetadata의 결과는 스트리밍될 수 있습니다. Next.js 문서의 generateMetadata에 따르면 Next.js는 generateMetadata가 끝나기를 기다리지 않고 첫 UI를 먼저 보내고, 메타데이터가 준비되면 그 태그를 <body>에 덧붙입니다. Googlebot처럼 JavaScript를 실행해 완성된 DOM을 읽는 봇은 이 태그를 올바르게 해석한다고 확인되었습니다. 반면 facebookexternalhit처럼 JavaScript를 실행하지 않고 HTML만 읽는 봇에게는 메타데이터가 계속 렌더링을 막고, 결과는 처음 HTML의 <head>에 들어갑니다. Next.js는 요청의 User-Agent 헤더를 보고 이런 HTML 전용 봇을 자동으로 가려냅니다.
자동 판별은 기본 User-Agent 목록을 기준으로 하므로, 목록에 없는 HTML 전용 크롤러라면 OG 태그가 <body> 쪽으로 스트리밍되어 미리보기에 반영되지 않을 수 있습니다. 이럴 때는 next.config의 htmlLimitedBots에 정규식을 지정해 기본 목록을 바꾸고, /.*/를 주면 메타데이터 스트리밍을 완전히 끌 수 있습니다. 다만 공식 문서는 이 값을 바꾸면 응답 시간이 길어질 수 있다고 경고합니다. 페이지를 사전 렌더링할 수 있고 generateMetadata에 동적인 동작이 없다면, 메타데이터는 이 문제와 관계없이 처음 HTML에 들어갑니다.
정리
- Open Graph 태그는 소셜 플랫폼이 공유 미리보기를 만들 때 읽는 메타 태그이며,
og:title,og:type,og:image,og:url이 핵심 속성입니다. - App Router에서는
metadata.openGraph나generateMetadata가 돌려주는openGraph로 OG 태그를 설정합니다. - 하위의
openGraph는 상위 객체를 통째로 바꾸므로, 상위 이미지를 함께 쓰려면parent에서 읽어 명시적으로 합칩니다. og:image와og:url은 최종적으로 절대 URL이어야 하며,metadataBase를 지정하면 상대 경로를 쓸 수 있습니다.- 플랫폼은 OG 태그를 캐싱하므로, 고친 뒤에는 공유 디버거로 반영 상태를 확인합니다.
연습 문제
1. 식별자 비교와 배열 구성
id='01'로 요청하면 id='1' 상품과 같다고 볼 수 있을까요? 부모 이미지가 두 개일 때, 정상인 id='1'의 OG 이미지와 Twitter 이미지는 각각 몇 개일까요?
해설 보기
getProduct는 문자열을 엄격하게 비교하므로 '01'은 '1'과 일치하지 않으며, 이 경우 메타데이터와 본문 모두 notFound()를 호출합니다. 숫자처럼 보이는 문자열이라도 이 함수가 숫자로 바꾸지는 않습니다. 정상 상품의 OG 이미지는 상품 이미지 하나와 부모 이미지 두 개를 합쳐 세 개이고, Twitter 이미지는 하나입니다.
2. 고친 OG 이미지가 보이지 않을 때
배포한 페이지의 og:image를 새 이미지로 바꿨는데, 메신저에 링크를 다시 붙여 넣어도 예전 이미지가 보입니다. 무엇을 먼저 확인해야 할까요?
해설 보기
먼저 배포된 페이지의 HTML에서 <meta property="og:image"> 값이 새 이미지의 절대 URL로 바뀌었는지, 그 주소에서 이미지가 실제로 열리는지 확인합니다. 태그가 올바르다면 플랫폼의 캐시 문제일 가능성이 크므로, Facebook 공유 디버거 같은 검사와 재수집 기능으로 다시 수집을 요청합니다. 플랫폼마다 캐시 갱신 시점이 다르므로 재수집 뒤에도 곧바로 바뀌지 않을 수 있습니다.
3. og:url과 canonical
상품 페이지에 openGraph.url만 설정해 두었습니다. 검색 엔진에도 이 주소가 대표 URL이라고 알린 것일까요?
해설 보기
알린 것이 아닙니다. og:url은 소셜 그래프에서 객체를 식별하는 주소일 뿐이고, 검색 엔진에 대표 URL을 알리는 것은 <link rel="canonical">입니다. Metadata API에서는 alternates: { canonical: 'https://commerce-lab.dev/products/1' }처럼 따로 선언해야 합니다.
Open Graph 메타데이터의 필드는 Next.js 공식 문서의 generateMetadata 레퍼런스와 Open Graph 프로토콜에서 확인할 수 있습니다.
다음 글 robots.txt 및 sitemap.xml 설정에서는 크롤러에게 사이트의 수집 규칙과 페이지 목록을 알려 주는 파일을 설정하는 방법을 알아봅니다.