Next.js
Next.js 동적 라우트 공유 미리보기가 깨질 때
SNS에 공유된 동적 라우트 링크의 미리보기(og:image, meta 태그)가 안 보일 때 확인 순서.
Next.js에서 동적 라우트 ([id].tsx)를 쓸 때, 콘텐츠를 SNS에 공유하면 미리보기가 깨진다. 제목, 이미지, 설명이 안 보이고 그냥 URL만 나타난다.
원인은 대부분 두 가지다: 1) 메타 태그가 제대로 생성 안 됨, 2) SNS 봇이 봤을 때는 콘텐츠가 로딩되지 않음.
1단계: 로컬에서 메타 태그 확인
# 실제 페이지 소스 보기
curl -s http://localhost:3000/posts/123 | head -50
# grep으로 메타 태그만 추출
curl -s http://localhost:3000/posts/123 | grep -E 'og:|twitter:|description|title'
<head> 섹션에 og:title, og:image 같은 태그가 있는가?
만약 없다면 generateMetadata 함수를 확인해야 한다.
2단계: generateMetadata 함수 확인
// app/posts/[id]/page.tsx
export async function generateMetadata(
{ params }: { params: { id: string } }
): Promise<Metadata> {
const post = await getPost(params.id)
if (!post) {
return { title: 'Not Found' }
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.imageUrl }],
},
}
}
함수가 있나? params를 제대로 받나? DB에서 데이터를 가져오나?
3단계: 빌드 후 정적 생성 확인
Next.js는 빌드 시 동적 라우트를 미리 생성할 수 있다 (SSG). 만약 설정하지 않으면 빌드 타임에 메타 태그를 생성할 수 없다.
// app/posts/[id]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({
id: post.id,
}))
}
export const revalidate = 3600 // 1시간마다 재검증
만약 generateStaticParams가 없으면, 빌드 타임에 getPost()가 실행되지 않는다. 그러면 빌드 후 페이지를 봐도 메타 태그가 없을 수 있다.
4단계: 빌드된 정적 파일 확인
# 빌드 실행
npm run build
# 생성된 정적 HTML 파일 확인
find .next/static -name "*.html" | head -5
# 또는 .next/server에서 확인
cat .next/server/app/posts/[id]/page.html | head -50
빌드 후 실제 HTML 파일에 메타 태그가 있는가?
5단계: SNS 봇이 봤을 때의 상황 시뮬레이션
SNS 봇(Facebook, Twitter, Slack)은 JavaScript를 실행하지 않는다. 초기 HTML만 본다.
따라서 클라이언트 사이드에서 데이터를 fetch하고 메타 태그를 추가하는 코드는 SNS 봇에게 보이지 않는다.
// 나쁜 예: 클라이언트에서 메타 데이터 로드
export default function Page() {
const [post, setPost] = useState(null)
useEffect(() => {
fetch(`/api/posts/${id}`).then(r => r.json()).then(setPost)
}, [])
// 메타 태그가 없음!
return <h1>{post?.title}</h1>
}
// 좋은 예: SSR이나 generateMetadata 사용
export async function generateMetadata({ params }) {
const post = await getPost(params.id) // 서버에서 처리
return { title: post.title }
}
6단계: 환경별 이미지 경로
export async function generateMetadata({ params }) {
const post = await getPost(params.id)
return {
openGraph: {
images: [
{
// 반드시 절대 URL이어야 함
url: `${process.env.NEXT_PUBLIC_SITE_URL}/images/${post.id}.jpg`,
width: 1200,
height: 630,
},
],
},
}
}
이미지가 상대 경로면 SNS 봇이 못 찾는다. 반드시 절대 URL을 써야 한다.
NEXT_PUBLIC_SITE_URL이 운영 환경에서 https://yoursite.com이 아니라 http://localhost:3000 같은 값이 아닌가 확인하자.
7단계: Canonical URL
export async function generateMetadata({ params }) {
return {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
alternates: {
canonical: `/posts/${params.id}`,
},
}
}
Canonical URL도 설정하면, SNS가 정확한 페이지를 가리킨다.
8단계: 배포 후 SNS 미리보기 테스트
# Facebook Debugger
https://developers.facebook.com/tools/debug/sharing/
# Twitter Card Validator
https://cards-dev.twitter.com/validator
# 또는 curl
curl -A "facebookexternalhit" https://yoursite.com/posts/123
curl -A "Twitterbot" https://yoursite.com/posts/123
SNS 봇 유에이전트로 요청하면, 초기 HTML을 본다. 메타 태그가 보이는가?
9단계: ISR (Incremental Static Regeneration) 설정
빌드 타임에 모든 동적 라우트를 생성할 수 없다면, ISR을 사용하자:
export const revalidate = 3600 // 60분마다 재생성
export async function generateStaticParams() {
// 인기 있는 게시글만 사전 생성
const popular = await getPopularPosts(100)
return popular.map(p => ({ id: p.id }))
}
첫 방문 시에는 on-demand 생성, 이후 3600초마다 배경에서 재생성한다.
최종 체크리스트
curl또는 개발자 도구에서 메타 태그 확인generateMetadata함수 존재 확인generateStaticParams설정 확인- 이미지 경로가 절대 URL인지 확인
NEXT_PUBLIC_SITE_URL환경 변수 값 확인- 빌드 후
.next/디렉토리에서 정적 HTML 확인 - SNS 봇 유에이전트로 테스트 (
curl -A "facebookexternalhit") - Facebook Debugger나 Twitter Card Validator로 최종 검증
이 순서로 진행하면, 대부분의 경우 메타 태그 문제를 찾을 수 있다.