링크를 공유했는데 카드가 아예 안 뜨거나, 제목은 맞는데 이미지가 빠지거나, 어떤 서비스에서는 잘 보이는데 X에서는 엉뚱한 미리보기가 뜨는 경우가 있습니다. 이 문제는 처음 겪으면 굉장히 추상적으로 느껴집니다. 브라우저에서 페이지는 잘 열리고, HTML에도 메타태그가 들어 있는 것 같은데, 막상 공유 카드만 이상하니까 어디부터 봐야 할지 막막해집니다.
하지만 공식 문서를 기준으로 보면 원인은 꽤 몇 가지로 좁혀집니다. Open Graph 프로토콜은 기본적으로 og:title, og:type, og:image, og:url 네 가지를 핵심 메타데이터로 설명합니다. X Developer Platform은 Twitter Cards에서 twitter:card와 twitter:* 메타태그를 사용하지만, Twitter 전용 태그가 없을 경우 일부 Open Graph 태그로 fallback 할 수 있다고 설명합니다. 그리고 Next.js 공식 문서는 App Router에서 metadata, generateMetadata, opengraph-image, twitter-image 파일 규칙을 통해 이 태그들을 자동 생성할 수 있다고 안내합니다.
즉 공유 카드 문제를 풀 때는 “플랫폼이 랜덤하게 깨진다”보다, HTML 응답, 메타태그, 절대 URL, 이미지 조건, 캐시, 프레임워크 병합 규칙을 하나씩 확인하는 편이 훨씬 빠릅니다. 저는 보통 이 글의 순서대로 좁혀갑니다.
공유 카드가 안 뜨거나 이상하게 보일 때 무엇부터 확인해야 하는지 정리합니다. HTML 응답, 절대 URL, 이미지 규격, 캐시, Next.js 메타데이터 병합 규칙까지 실제 점검 순서대로 설명합니다. 핵심 1 핵심 2 핵심 3Open Graph와 Twitter Card가 깨질 때 확인할 체크리스트
먼저 결론: 카드가 깨질 때는 이 순서대로 본다
가장 먼저 URL 자체가 공개적으로 열리는지 확인한다
“개발자 도구에서 보이는 DOM”이 아니라 “실제 HTML 응답”을 본다
먼저 결론: 카드가 깨질 때는 이 순서대로 본다
제가 실제로 보는 순서는 아래와 같습니다.
- 공유한 URL이 로그인 없이 200 응답을 주는지
- HTML 소스에
og:*와twitter:*메타태그가 실제로 들어 있는지 og:url,og:image,twitter:image가 절대 URL인지- 이미지가 접근 가능하고 규격을 만족하는지
twitter:card타입이 적절한지- canonical, 리다이렉트, 호스트가 일관적인지
- Next.js의 메타데이터 병합에서 필드가 덮어써지지 않았는지
- 마지막으로 플랫폼 캐시를 의심하는지
중요한 건, “소스코드에는 메타태그가 있다”는 사실만으로 충분하지 않다는 점입니다. 소셜 봇이 실제로 받는 HTML, 그 안의 절대 URL, 이미지 다운로드 가능 여부가 다 맞아야 카드가 안정적으로 뜹니다.
1. 가장 먼저 URL 자체가 공개적으로 열리는지 확인한다
공유 카드를 수집하는 봇은 브라우저 세션을 공유하지 않습니다. 로그인 상태도 아니고, 로컬 개발 서버에도 접근하지 못합니다. X의 Troubleshooting Cards 문서도 validator가 테스트 환경이나 제한된 환경에는 접근하지 못할 수 있고, robots.txt나 서버 설정 때문에 Twitterbot이 막힐 수 있다고 설명합니다.
그래서 저는 카드가 깨지면 가장 먼저 그 URL을 “익명 요청”으로 봅니다.
curl.exe -I https://www.example.com/blog/open-graph-debug-checklist
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String "<meta"
이 단계에서 확인할 건 단순합니다.
- 200 응답인가
- 로그인이나 쿠키 없이 HTML이 내려오는가
- 리다이렉트가 과하게 반복되지 않는가
브라우저에서 잘 보여도 봇이 막히면 카드 수집은 실패합니다. 특히 staging 환경, 비밀번호 보호, 특정 user-agent 차단, 방화벽 규칙이 문제를 만들 때가 많습니다.
2. “개발자 도구에서 보이는 DOM”이 아니라 “실제 HTML 응답”을 본다
공유 카드 문제에서 가장 흔한 착각 중 하나가 여기입니다. 브라우저 개발자 도구에서 <head>를 보면 메타태그가 보이는데, 실제 봇이 받은 원본 HTML에는 같은 내용이 없을 수 있습니다. Next.js는 App Router에서 metadata를 초기 HTML에 포함시키지만, 결국 중요한 건 봇이 받은 응답입니다.
그래서 저는 언제나 curl이나 페이지 소스 보기로 먼저 확인합니다.
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String 'og:title'
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String 'twitter:card'
Open Graph 프로토콜 기준으로 최소한 보고 싶은 기본값은 이렇습니다.
<meta property="og:title" content="Open Graph와 Twitter Card가 깨질 때 확인할 체크리스트" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://www.example.com/blog/open-graph-debug-checklist" />
<meta property="og:image" content="https://www.example.com/articles/open-graph-debug-checklist/share-card.png" />
그리고 X 카드에서는 이런 태그가 있으면 좋습니다.
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Open Graph와 Twitter Card가 깨질 때 확인할 체크리스트" />
<meta name="twitter:description" content="공유 카드가 안 뜨거나 이상하게 보일 때 무엇부터 확인해야 하는지 정리합니다." />
<meta name="twitter:image" content="https://www.example.com/articles/open-graph-debug-checklist/share-card.png" />
X 공식 문서는 Twitter 카드가 name과 content 속성을 쓰고, 일부 Open Graph 태그로 fallback 할 수 있다고 설명합니다. 하지만 fallback이 있다고 해서 Twitter 전용 태그를 아예 생략하는 쪽이 더 좋은 것은 아닙니다. 의도를 명확히 하려면 둘 다 맞춰두는 편이 안전합니다.
3. og:image, og:url, twitter:image는 절대 URL이어야 한다
Open Graph 프로토콜은 og:url을 그래프 안에서 객체의 영구 ID로 사용되는 canonical URL이라고 설명합니다. og:image도 마찬가지로 이미지 URL이어야 합니다. 이때 실무적으로 가장 중요한 건 상대 경로가 아니라 절대 URL을 안정적으로 출력하는 것입니다.
Next.js는 metadataBase를 설정하면 URL 기반 메타데이터를 절대 URL로 조합해 줍니다. 공식 문서도 metadataBase가 루트 세그먼트에서 URL 기반 필드 전반의 기준 URL이 된다고 설명합니다.
현재 이 레포는 루트 레이아웃에서 이렇게 설정돼 있습니다.
export const metadata: Metadata = {
metadataBase: new URL(getSiteUrl()),
title: {
default: "Signal Ledger",
template: "%s | Signal Ledger",
},
};
이 구조는 좋은 기본값입니다. 다만 여기서도 꼭 확인할 게 있습니다.
getSiteUrl()이 실제 운영 도메인을 반환하는가- preview 도메인이나 localhost가 섞이지 않는가
- page-level metadata에서 상대 경로를 써도 최종 HTML이 절대 URL이 되는가
공유 카드 문제는 생각보다 localhost, pages.dev, vercel.app, www/non-www 혼선 때문에 자주 생깁니다. 이건 이전에 정리한 www, non-www, canonical 정리 글과도 직접 연결됩니다.
4. 이미지가 존재하는 것과 “플랫폼이 가져올 수 있는 것”은 다르다
이미지가 브라우저에서 열린다고 끝이 아닙니다. Open Graph 프로토콜은 og:image:width, og:image:height, og:image:alt 같은 구조화된 속성을 함께 제공할 수 있다고 설명합니다. X Summary Card 문서는 더 구체적으로, summary card 이미지에 대해 최소 144x144, 최대 4096x4096, 5MB 미만, JPG/PNG/WEBP/GIF 지원, SVG 미지원이라고 안내합니다. Next.js의 twitter-image 파일 규칙 문서도 twitter image는 5MB를 넘으면 빌드가 실패한다고 설명합니다.
즉 저는 이미지 문제를 볼 때 아래를 같이 확인합니다.
- 실제 URL로 200 응답이 오는가
content-type이 이미지인가- 파일 크기가 과하지 않은가
- 너무 작은 썸네일이 아닌가
- SVG처럼 플랫폼이 싫어하는 포맷은 아닌가
간단한 확인은 이렇게 할 수 있습니다.
curl.exe -I https://www.example.com/articles/open-graph-debug-checklist/share-card.png
그리고 Open Graph 이미지라면 가능하면 alt와 크기도 같이 명시하는 편이 좋습니다.
openGraph: {
images: [
{
url: "/articles/open-graph-debug-checklist/share-card.png",
width: 1200,
height: 630,
alt: "공유 카드 점검 흐름 다이어그램",
},
],
}
5. Twitter Card 타입이 페이지 성격과 맞는지 본다
X docs를 보면 twitter:card는 카드 타입을 결정하는 핵심 태그입니다. 보통 블로그 글은 summary 또는 summary_large_image를 사용합니다. 여기서 종종 생기는 문제는 타입은 summary_large_image인데 이미지가 그에 맞는 비율이나 크기를 갖지 못해 어색하게 잘리거나, 반대로 너무 작은 이미지를 억지로 키우는 경우입니다.
그래서 블로그 글에서는 보통 이 기준이면 충분합니다.
- 이미지 강조형 글:
summary_large_image - 작은 아이콘 중심 또는 정사각형 썸네일:
summary
그리고 X Summary Card 문서는 generic image, 예를 들어 사이트 공용 로고만 반복 사용하는 방식을 권장하지 않습니다. 글마다 해당 페이지를 대표하는 고유 이미지가 있을수록 카드 품질도 좋아집니다.
6. Next.js에서는 page-level openGraph가 root openGraph를 통째로 덮어쓴다
이건 App Router에서 정말 자주 놓치는 함정입니다. Next.js 공식 문서는 metadata objects가 shallow merge 되며, nested field인 openGraph나 robots는 마지막 세그먼트에서 정의한 값으로 교체된다고 설명합니다.
이 말의 의미는 아주 큽니다. 예를 들어 루트 레이아웃에 이렇게 써두고:
export const metadata = {
openGraph: {
type: "website",
siteName: "Signal Ledger",
},
};
페이지에서 이렇게만 쓰면:
return {
openGraph: {
title: article.title,
description: article.summary,
images: [article.coverImage],
},
};
루트의 type, siteName 같은 필드는 그대로 합쳐지는 게 아니라 사라질 수 있습니다. 공식 문서도 이 경우 공유 필드를 별도 변수로 빼서 spread 하는 방식을 예시로 보여줍니다.
즉 카드가 일부 태그만 빠지는 이상한 현상이 있으면, 메타데이터 “병합”을 기대했는데 실제로는 “덮어쓰기”가 일어났는지 의심해야 합니다.
7. 그래서 page-level metadata에서는 필요한 필드를 다 명시하는 편이 안전하다
블로그 글 페이지라면 저는 보통 이렇게 씁니다.
export async function generateMetadata({
params,
}: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params;
const article = await getPublishedArticleBySlug(slug);
if (!article) return {};
return {
title: article.seoTitle ?? article.title,
description: article.seoDescription ?? article.summary,
alternates: {
canonical: `/blog/${slug}`,
},
openGraph: {
title: article.seoTitle ?? article.title,
description: article.seoDescription ?? article.summary,
url: `/blog/${slug}`,
type: "article",
images: [
{
url: article.coverImage,
alt: article.coverAlt ?? article.title,
},
],
},
twitter: {
card: "summary_large_image",
title: article.seoTitle ?? article.title,
description: article.seoDescription ?? article.summary,
images: [article.coverImage],
},
};
}
이 패턴의 장점은 분명합니다.
- canonical과
og:url이 같은 경로를 가리킵니다. - Open Graph와 Twitter 문구가 서로 어긋나지 않습니다.
- 페이지 단위에서 필요한 필드를 명시하므로 shallow merge 함정이 줄어듭니다.
8. opengraph-image와 twitter-image 파일 규칙을 쓰면 이미지 관리가 훨씬 단순해진다
Next.js 공식 문서는 opengraph-image와 twitter-image를 special file convention으로 제공합니다. 루트나 각 route segment에 이미지 파일을 두거나, 코드로 생성해서 자동으로 메타태그에 연결할 수 있습니다.
이 방식이 좋은 이유는 단순합니다.
- 경로별 카드 이미지를 자동으로 연결할 수 있음
- alt, width, height, contentType까지 일관되게 관리 가능
- 이미지 파일 크기가 너무 크면 빌드 단계에서 바로 알 수 있음
예를 들어 route segment에 이미지 파일을 두기만 해도 됩니다.
app/blog/[slug]/opengraph-image.png
app/blog/[slug]/twitter-image.png
혹은 코드로 만들 수도 있습니다.
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export const alt = "공유 카드용 커버 이미지";
export default function Image() {
return new ImageResponse(
<div style={{ background: "#f6f1e8", width: "100%", height: "100%" }}>
Open Graph Image
</div>,
size,
);
}
이 구조를 쓰면 공유 카드 이미지가 누락되거나, 페이지별로 다른 썸네일이 필요한 상황을 훨씬 깔끔하게 다룰 수 있습니다.
9. 캐시는 진짜 문제인데, 항상 첫 원인은 아니다
카드가 오래된 상태로 보일 때 많은 사람이 첫 번째 원인을 캐시로 잡습니다. 실제로 X의 Troubleshooting Cards 문서는 카드 메타정보가 대략 7일 정도 재인덱싱될 수 있다고 설명합니다. 하지만 제 경험상 캐시는 “마지막 단계”에서 보는 편이 좋습니다.
왜냐하면 그 전 단계가 더 자주 틀리기 때문입니다.
- 메타태그가 실제 HTML에 없음
- 이미지 URL이 상대 경로임
og:url이 잘못됨- 리다이렉트가 꼬임
- twitter image가 너무 큼
이 조건이 다 맞는데도 예전 카드가 계속 보이면 그때 캐시를 의심하는 편이 더 효율적입니다. X 문서도 Card Validator를 통해 테스트하고, 업데이트 반영이 즉시 되지 않을 수 있음을 안내합니다.
10. 결국 공유 카드 문제는 canonical과 메타데이터 문제이기도 하다
Open Graph의 og:url은 객체의 영구 ID로 쓰이는 canonical URL입니다. 따라서 공유 카드 문제는 이미지 문제만이 아니라 대표 URL 문제이기도 합니다. www와 non-www가 섞이거나, 프리뷰 URL과 운영 URL이 섞이면 카드 수집 결과도 흔들릴 수 있습니다.
그래서 저는 공유 카드가 깨질 때도 결국 아래를 같이 봅니다.
- 대표 도메인 하나
- canonical 하나
og:url하나- 내부 링크가 가리키는 호스트 하나
즉 이 문제는 메타태그를 예쁘게 만드는 것보다, 사이트 전체 URL 일관성을 지키는 쪽에 더 가깝습니다.
제가 실제로 쓰는 체크리스트
공유 카드가 이상하면 저는 보통 이 순서로 확인합니다.
- 공유 URL이 공개적으로 200 응답을 주는가
- HTML 원문에
og:*와twitter:*태그가 있는가 og:url,og:image,twitter:image가 절대 URL인가- 이미지 URL이 직접 열리고 5MB 미만인가
twitter:card타입이 페이지 목적과 맞는가- canonical과
og:url이 같은 주소를 가리키는가 - Next.js의 page-level
openGraph/twitter가 root 필드를 덮어쓰고 있지 않은가 - 마지막으로 validator와 캐시를 확인하는가
간단한 확인은 shell로도 됩니다.
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String 'og:'
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String 'twitter:'
curl.exe -I https://www.example.com/articles/open-graph-debug-checklist/share-card.png
curl.exe https://www.example.com/blog/open-graph-debug-checklist | Select-String 'rel="canonical"'
이 정도만 해도 “메타태그가 없는 문제인지”, “이미지 문제인지”, “호스트 문제인지”가 꽤 빨리 갈립니다.
마무리
Open Graph와 Twitter Card가 깨질 때는 보기보다 원인이 단순한 경우가 많습니다. 대부분은 HTML 응답에 태그가 실제로 없거나, 절대 URL이 아니거나, 이미지가 규격을 만족하지 않거나, Next.js 메타데이터가 shallow merge 특성 때문에 일부 필드를 잃어버리는 문제입니다.
공식 문서를 기준으로 보면 핵심은 이렇습니다. Open Graph는 og:title, og:type, og:image, og:url이 기본이고, X는 twitter:card와 twitter:* 태그를 쓰되 일부 Open Graph 필드로 fallback 할 수 있습니다. Next.js는 metadataBase, generateMetadata, opengraph-image, twitter-image로 이 구조를 잘 자동화해 주지만, page-level nested metadata가 통째로 덮어써질 수 있다는 규칙은 꼭 기억해야 합니다.
검색 결과 쪽 메타데이터 정리가 아직 헷갈린다면 meta title과 description 수정 글과 같이 보면 더 흐름이 잘 잡힙니다.