← 전체 글로 돌아가기

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초마다 배경에서 재생성한다.

최종 체크리스트

  1. curl 또는 개발자 도구에서 메타 태그 확인
  2. generateMetadata 함수 존재 확인
  3. generateStaticParams 설정 확인
  4. 이미지 경로가 절대 URL인지 확인
  5. NEXT_PUBLIC_SITE_URL 환경 변수 값 확인
  6. 빌드 후 .next/ 디렉토리에서 정적 HTML 확인
  7. SNS 봇 유에이전트로 테스트 (curl -A "facebookexternalhit")
  8. Facebook Debugger나 Twitter Card Validator로 최종 검증

이 순서로 진행하면, 대부분의 경우 메타 태그 문제를 찾을 수 있다.