TechFeedTechFeed
Next.js

Next.js trailingSlash와 OG 이미지 2026 | 308 redirect 피하기

Next.js trailingSlash:true 환경에서 OG·PNG가 308로 깨지는 원인과 skipTrailingSlashRedirect, canonical 통일, curl 검증 방법을 정리한다.

by

한 줄: Next.js에서 trailingSlash: true만 켜면 OG·PNG 같은 정적 자원이 308로 붙어 네이버 Yeti 등이 이미지를 못 가져갈 수 있다. skipTrailingSlashRedirect: true와 canonical 슬래시 통일이 세트다.


콘텐츠 URL을 /post/foo/ 형태로 맞추면 sitemap·내부 링크 중복을 줄이기 쉽다. 문제는 프레임워크가 /opengraph-image·확장자 있는 정적 경로에도 trailing slash 리다이렉트를 걸 때다. 일부 검색·SNS 크롤러는 OG 수집 단계에서 redirect를 따라가지 않는다. 배포 전 curl -sI로 이미지 URL이 200인지 확인하고, 메타의 절대 URL도 슬래시 정책과 맞춘다.


trailingSlash를 쓰는 이유 | URL 단일화

Next.js trailingSlash: true는 페이지 경로 끝에 /를 붙인다. 팀·호스팅·기존 백링크가 슬래시 있는 형태면 중복 색인 위험을 낮춘다.


  • canonical, sitemap, RSS 링크를 모두 /slug/로 통일하기 쉽다.
  • 반대로 슬래시 없는 링크로 들어온 요청은 308/301로 붙을 수 있다.
  • App Router의 opengraph-image 특수 라우트, public/*.png, API 경로까지 같은 규칙이 적용되면 부작용이 난다.

Next 15 기능 전반은 Next.js 15 새 기능 가이드, 라우터 이전은 App Router 마이그레이션을 보면 되고, 이 글은 OG·정적 자산 redirect만 깊게 본다.


OG 이미지가 안 붙는 사고 | 308과 봇 동작

메타에 og:imagehttps://example.com/path/opengraph-image를 넣었는데, 서버가 /opengraph-image/로 308을 주면 일부 수집기는 본문 HTML만 가져가고 이미지 단계에서 멈춘다. 네이버 Yeti 등에서 “본문은 있는데 썸네일 없음”으로 나타나는 패턴과 맞다.


요청 URL trailingSlash만 true + skipTrailingSlashRedirect
페이지 /post/foo /post/foo/ 리다이렉트 가능 둘 다 200 가능 (중복 방지 정책 별도)
OG .../opengraph-image → slash 붙이며 308 위험 슬래시 없이 200 유지에 유리
/og-default.png 확장자 경로도 정책에 말릴 수 있음 정적 파일 200 우선
크롤러 OG 수집 redirect non-follow 시 실패 직접 200으로 수신

# 라이브/프리뷰에서 상태 코드만 확인
curl -sI "https://example.com/some-slug/opengraph-image" | head -15
curl -sI "https://example.com/og-default.png" | head -10
curl -sI "https://example.com/favicon.ico" | head -10

# 308/301 이면 Location 헤더를 반드시 확인
curl -sI "https://example.com/some-slug/opengraph-image" | grep -iE 'HTTP/|location:'

next.config 세트 | trailingSlash + skip 플래그

Pages Router·App Router 모두 설정 키 이름은 동일 계열이다. 문서 버전에 맞게 확인한다.


/** @type {import('next').NextConfig} */
const nextConfig = {
  trailingSlash: true,
  // 정적·OG 자원에 강제 trailing slash 308을 막기 위한 세트
  skipTrailingSlashRedirect: true,
  images: {
    unoptimized: true, // 프로젝트 정책에 맞게
  },
}

module.exports = nextConfig

skipTrailingSlashRedirect: true는 “슬래시 있는 URL과 없는 URL을 둘 다 200으로 받을 수 있게” 만드는 쪽에 가깝다. 그래서 canonical을 모든 페이지에서 슬래시 포함으로 명시하지 않으면 검색엔진이 두 URL을 따로 볼 위험이 있다.


  • generateMetadata / Headalternates.canonicalhttps://도메인/slug/로 고정
  • sitemap·RSS의 모든 <loc>도 동일 규칙
  • 내부 Link href도 trailing slash 포함 습관

피드 생성과 맞추는 방법은 prebuild sitemap·RSS를 따른다. 프로덕션 체크 항목은 App Router 프로덕션 체크리스트와 겹친다.


메타·절대 URL | og:image 쓰는 법

redirect를 없애도 메타가 상대 경로이거나 잘못된 호스트면 미리보기가 깨진다.


// 개념 예시 (App Router metadata)
export const metadata = {
  alternates: { canonical: 'https://tech.example.com/my-post/' },
  openGraph: {
    url: 'https://tech.example.com/my-post/',
    images: [{ url: 'https://tech.example.com/images/thumbnails/123.png', width: 1200, height: 630 }],
    locale: 'ko_KR',
    type: 'article',
  },
  twitter: { card: 'summary_large_image' },
}
  • OG 이미지는 가급적 확장자 있는 정적 PNG/JPG를 쓰면 특수 라우트 redirect 이슈를 피하기 쉽다.
  • 동적 opengraph-image를 쓸 때도 메타에 적은 URL로 curl -sI 200을 확인한다.
  • CDN 캐시가 옛 308을 물고 있으면 퍼지 후 재검증한다.

호스팅이 Vercel이면 프리빌트 업로드 이슈와 별개로, 응답 헤더만으로도 충분히 재현된다. 배포 파이프라인은 prebuilt 업로드·한도는 무료 플랜 한도를 참고한다.


검증 매트릭스 | 배포 전 필수 curl

설정 변경 후 프리뷰 URL에서 표를 한 바퀴 돈다.


  1. 페이지 HTML 200, canonical에 trailing slash
  2. og:image 절대 URL 추출 후 그 URL이 200 (308 아님)
  3. 슬래시 없는 페이지 URL 정책: 의도한 redirect 또는 200+canonical
  4. sitemap 해당 slug가 slash 포함인지
  5. SNS 디버거(지원되는 경우) 캐시 갱신

BASE=https://your-preview.example
# HTML에서 og:image 뽑기
curl -sS "$BASE/my-post/" | grep -oE 'property="og:image"[^>]+|content="https://[^"]+"' | head -20

# 이미지 직접
IMG=$(curl -sS "$BASE/my-post/" | grep -oE 'https://[^"]+/[^"]+.(png|jpg|jpeg|webp)' | head -1)
curl -sI "$IMG" | head -10

로컬 next dev와 프로덕션 빌드 동작이 다를 수 있다. 최종 판정은 프리뷰/프로덕션 응답으로 한다.


자주 나는 실수 | 설정 반쪽·메타 불일치

  • trailingSlash만 켜고 skip 미설정 → OG·PNG 308 사고
  • skip만 켜고 canonical 누락 → 슬래시 유무 중복 색인
  • sitemap만 옛 규칙 → 본문 canonical과 loc 불일치
  • 상대 경로 og:image → 크롤러·일부 플랫폼 실패
  • 미들웨어에서 또 redirect → next.config와 이중 정책. middleware matcher에서 정적·이미지 확장자 제외

// middleware에서 정적 자산 제외 예시 (개념)
export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.png$|.*\\.jpg$).*)'],
}

※ Next.js 마이너 버전마다 플래그 기본값·헤더 코드가 달라질 수 있다. 릴리스 노트와 실측 curl을 우선한다.


프레임워크·호스팅 기본값은 버전마다 다릅니다. 프리뷰 URL에서 curl 상태 코드로 최종 확인하는 습관을 권장합니다.


Next.jstrailingSlashOG image308SEO

관련 도구

함께 보면 좋은 문제 해결

EXPLORE / Next.js

이어서 읽어보기

전체 토픽 둘러보기