수영장(Sooyoung Archive)
홈개발일지이번캠App소개
전체보기
프로젝트
블로그 만들기그림톡캠핑 인스타
개발

© 2026 수영장 (Sooyoung Archive). All rights reserved.

개인정보처리방침연락처
개발일지 목록
개발

Next.js App Router에서 구글 서치 콘솔 '적절한 표준 태그가 포함된 대체 페이지' 해결기

2026년 9월 7일약 14분 소요0회 조회
목차
1. 구글이 말하는 '적절한 표준 태그가 포함된 대체 페이지'란?2. 원인 추적: Next.js App Router 메타데이터 상속의 함정왜 이런 일이 벌어졌을까?3. 해결책: Self-Referencing Canonical 구조 구축Step 1. 루트 레이아웃에서 canonical 제거Step 2. 메인 홈 화면에만 "/" 지정Step 3. 동적 포스트 페이지 (generateMetadata) 처리 & 한글 슬러그 주의점Step 4. 카테고리 및 기타 정적 페이지 일괄 적용4. 배포 후 실서버 curl 검증5. 구글 서치 콘솔에서 '수정 결과 확인' 요청💡 요약 및 체크리스트

Next.js App Router에서 구글 서치 콘솔 '적절한 표준 태그가 포함된 대체 페이지' 해결기

구글 애드센스 승인을 준비하며 블로그 글을 꾸준히 작성하던 중, 어느 날 구글 서치 콘솔(Google Search Console)로부터 한 통의 경고 메일을 받았습니다.

"페이지 색인이 생성되지 않는 새로운 이유: 적절한 표준 태그가 포함된 대체 페이지"
(Alternate page with proper canonical tag)

열심히 공들여 작성한 블로그 글들이 구글 검색 결과에 노출되지 않고 대거 색인에서 제외되어 있던 것입니다.

처음에는 구글 검색 로봇이 글의 품질을 낮게 평가한 건가 걱정했지만, 원인을 뜯어보니 Next.js 16 App Router의 메타데이터 상속(Metadata Inheritance) 구조로 인해 발생한 명백한 설정 실수였습니다.

이번 글에서는 왜 이런 색인 누락 현상이 발생했는지, 그리고 Next.js App Router 환경에서 동적 라우트와 한글 슬러그까지 고려하여 **Self-referencing Canonical(자기 참조 표준 태그)**을 완벽하게 세팅하는 방법을 정리해 보았습니다.


1. 구글이 말하는 '적절한 표준 태그가 포함된 대체 페이지'란?

웹에서 canonical(표준 태그)은 동일하거나 매우 유사한 콘텐츠가 여러 URL로 분산되어 있을 때, **"이 여러 주소 중 진짜 원본 주소는 바로 여기야!"**라고 검색 엔진 크롤러에게 알려주는 신호입니다.

html
<!-- 예: 이 페이지의 진짜 원본은 https://sooyoung.pe.kr/post/my-article 입니다 -->
<link rel="canonical" href="https://sooyoung.pe.kr/post/my-article" />

검색 엔진은 이 태그를 발견하면 파라미터가 붙은 URL이나 모바일용 페이지 등을 '대체 페이지'로 취급하고, canonical이 가리키는 '표준 URL' 하나만 검색 결과에 노출합니다.

그런데 제 블로그의 개별 글들이 이 이유로 색인 제외되었다는 것은, 구글 크롤러가 블로그 글을 읽으러 들어왔더니 글마다 엉뚱한 표준 태그가 달려 있었다는 뜻이었습니다.


2. 원인 추적: Next.js App Router 메타데이터 상속의 함정

실제 배포된 블로그 글 페이지의 HTML 소스를 curl로 열어보고 깜짝 놀랐습니다.

bash
$ curl -s https://sooyoung.pe.kr/post/my-post | grep -o '<link[^>]*rel="canonical"[^>]*>'
<link rel="canonical" href="https://sooyoung.pe.kr"/>

글 상세 페이지인 /post/my-post의 canonical 주소가 자기 자신이 아니라 **메인 홈 화면(https://sooyoung.pe.kr)**을 가리키고 있었습니다.

원인은 루트 레이아웃인 src/app/layout.tsx에 있었습니다.

typescript
// src/app/layout.tsx (수정 전 문제의 코드)
export const metadata: Metadata = {
  metadataBase: new URL("https://sooyoung.pe.kr"),
  alternates: {
    canonical: "/", // 🚨 범인은 바로 이 녀석!
  },
  title: "수영장 (Sooyoung Archive)",
  // ...
};

왜 이런 일이 벌어졌을까?

Next.js App Router의 Metadata API는 상위(Parent) 레이아웃의 메타데이터를 하위(Child) 페이지가 기본적으로 상속받는 구조를 가집니다.

하위 페이지인 post/[slug]/page.tsx나 about/page.tsx에서 title과 description은 오버라이딩했지만, alternates.canonical은 따로 지정하지 않았던 것입니다.

결과적으로 Next.js는 루트 레이아웃의 canonical: "/ "를 그대로 하위 페이지들로 전파했고, 구글 크롤러 입장에서는 다음과 같이 해석할 수밖에 없었습니다.

"개발자가 친절하게도 이 모든 글 페이지는 메인 홈 화면의 복사본(대체 페이지)이고 원본은 메인 홈이라고 선언해 뒀네? 그럼 글들은 검색 색인에서 빼고 홈페이지만 남겨야겠다."


3. 해결책: Self-Referencing Canonical 구조 구축

해결 방법은 명확합니다.

  1. 루트 레이아웃의 전역 canonical을 제거하여 하위 페이지로의 원치 않는 상속을 차단합니다.
  2. **모든 개별 페이지가 자기 자신의 완전한 URL을 canonical로 직접 선언(Self-referencing Canonical)**하게 만듭니다.

Step 1. 루트 레이아웃에서 canonical 제거

src/app/layout.tsx에서는 도메인 기준점이 되는 metadataBase만 남기고, canonical을 제거합니다.

typescript
// src/app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL("https://sooyoung.pe.kr"),
  // alternates: { canonical: "/" } 제거!
  title: "수영장 (Sooyoung Archive)",
  // ...
};

Step 2. 메인 홈 화면에만 "/" 지정

src/app/(main)/page.tsx 메타데이터에 메인 홈 전용 canonical을 부여합니다.

typescript
// src/app/(main)/page.tsx
export const metadata: Metadata = {
  title: "수영장 (Sooyoung Archive)",
  alternates: {
    canonical: "/",
  },
};

Step 3. 동적 포스트 페이지 (generateMetadata) 처리 & 한글 슬러그 주의점

가장 중요한 상세 페이지(src/app/(main)/post/[slug]/page.tsx)입니다.

여기서 아주 중요한 주의점이 있습니다. 한글 슬러그가 포함된 포스트의 경우, sitemap.xml에 등록된 URL 형태와 canonical URL이 토씨 하나 틀리지 않고 일치해야 한다는 점입니다.

보통 sitemap.ts에서는 URL을 생성할 때 한글 깨짐을 방지하기 위해 encodeURIComponent를 사용합니다. 따라서 canonical 태그도 동일하게 인코딩 처리를 해주어야 구글이 완벽한 1:1 매칭으로 인식합니다.

typescript
// src/app/(main)/post/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) return {};

  const encodedSlug = encodeURIComponent(post.slug);

  return {
    title: `${post.title} | 수영장 (Sooyoung Archive)`,
    description: post.excerpt,
    alternates: {
      canonical: `/post/${encodedSlug}`, // ✅ 자기 자신을 가리키는 정확한 Canonical
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: "article",
      url: `https://sooyoung.pe.kr/post/${encodedSlug}`,
      // ...
    },
  };
}

Step 4. 카테고리 및 기타 정적 페이지 일괄 적용

카테고리 동적 라우트와 /blog, /about, /contact, /privacy 등의 페이지에도 각자의 고유 경로를 canonical로 설정해 주었습니다.

typescript
// src/app/(main)/category/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const category = await getCategoryBySlug(slug);
  // ...
  return {
    // ...
    alternates: {
      canonical: `/category/${encodeURIComponent(category.slug)}`,
    },
  };
}

4. 배포 후 실서버 curl 검증

코드를 푸시하고 Vercel 배포가 완료된 직후, 터미널에서 실제 HTML 응답 헤더를 테스트했습니다.

bash
# 1. 메인 홈 검증
$ curl -s https://sooyoung.pe.kr/ | grep -o '<link[^>]*rel="canonical"[^>]*>'
<link rel="canonical" href="https://sooyoung.pe.kr"/>

# 2. 블로그 목록 검증
$ curl -s https://sooyoung.pe.kr/blog | grep -o '<link[^>]*rel="canonical"[^>]*>'
<link rel="canonical" href="https://sooyoung.pe.kr/blog"/>

# 3. 영문 슬러그 포스트 검증
$ curl -s https://sooyoung.pe.kr/post/first-meeting | grep -o '<link[^>]*rel="canonical"[^>]*>'
<link rel="canonical" href="https://sooyoung.pe.kr/post/first-meeting"/>

# 4. 한글 슬러그 포스트 검증
$ curl -s "https://sooyoung.pe.kr/post/%EB%82%B4-%EC%BA%90%EB%A6%AD%ED%84%B0%EC%99%80-%EB%8C%80%ED%99%94%ED%95%98%EA%B8%B0" | grep -o '<link[^>]*rel="canonical"[^>]*>'
<link rel="canonical" href="https://sooyoung.pe.kr/post/%EB%82%B4-%EC%BA%90%EB%A6%AD%ED%84%B0%EC%99%80-%EB%8C%80%ED%99%94%ED%95%98%EA%B8%B0"/>

모든 페이지가 더 이상 메인 홈이 아닌, 자신의 고유 URL을 정확하게 가리키고 있음을 확인했습니다!


5. 구글 서치 콘솔에서 '수정 결과 확인' 요청

배포와 검증이 끝났다면 마지막으로 구글 서치 콘솔에 가서 재크롤링을 요청해야 합니다.

  1. Google Search Console 접속
  2. 좌측 메뉴 색인 생성 > 페이지 이동
  3. '페이지 색인이 생성되지 않는 이유' 목록에서 [적절한 표준 태그가 포함된 대체 페이지] 클릭
  4. 상단에 있는 [수정 결과 확인 (Validate Fix)] 버튼 클릭

버튼을 누르면 상태가 **'시작됨'**으로 변경되며, 이후 며칠 동안 구글 로봇이 사이트맵과 페이지들을 다시 크롤링하면서 정상 색인으로 순차 전환합니다.


💡 요약 및 체크리스트

Next.js App Router 기반의 블로그나 웹 서비스를 만들고 계신다면, 다음 3가지는 배포 전 꼭 체크해 보세요.

  1. 루트 layout.tsx에 canonical: "/"를 넣지 말 것: 모든 자식 페이지가 메인 홈의 복사본으로 오인받아 색인에서 탈락합니다.
  2. 동적 라우트는 generateMetadata에서 반드시 self-referencing canonical을 명시할 것: 특히 한글 슬러그의 경우 encodeURIComponent를 적용해 sitemap.xml의 <loc> 포맷과 정확히 일치시켜야 합니다.
  3. 배포 후 curl 명령어로 실제 렌더링된 <link rel="canonical"> 태그를 눈으로 확인할 것.

작은 메타데이터 설정 하나가 검색 노출과 애드센스 승인 여부를 좌우할 수 있는 만큼, 이번 트러블슈팅이 비슷한 문제를 겪고 계신 개발자분들께 도움이 되길 바랍니다!

클릭할 때마다 작가에게 따뜻한 응원이 전달됩니다 ❤️

이 글이 유익하셨나요?
동료 개발자들과 경험과 노하우를 공유해 보세요.
이전 글개발
Next.js 16과 Tailwind CSS에서 다크/라이트 테마 깜빡임(FOUC) 없이 구현하기

함께 읽으면 좋은 다른 개발일지

'개발' 및 추천 아카이브 글 모음

🏊‍♂️
개발
Next.js 16과 Tailwind CSS에서 다크/라이트 테마 깜빡임(FOUC) 없이 구현하기
Next.js 16 App Router 환경에서 localStorage 기반 다크 모드 전환 시 발생하는 첫 화면 깜빡임(FOUC) 문제를 인라인 스크립트와 CSS 변수로 깔끔하게 해결한 과정.
2026. 9. 4.

댓글

목차
1. 구글이 말하는 '적절한 표준 태그가 포함된 대체 페이지'란?2. 원인 추적: Next.js App Router 메타데이터 상속의 함정왜 이런 일이 벌어졌을까?3. 해결책: Self-Referencing Canonical 구조 구축Step 1. 루트 레이아웃에서 canonical 제거Step 2. 메인 홈 화면에만 "/" 지정Step 3. 동적 포스트 페이지 (generateMetadata) 처리 & 한글 슬러그 주의점Step 4. 카테고리 및 기타 정적 페이지 일괄 적용4. 배포 후 실서버 curl 검증5. 구글 서치 콘솔에서 '수정 결과 확인' 요청💡 요약 및 체크리스트
2회
🏊‍♂️
개발
React 마크다운 렌더러에 스마트 목차(TOC)와 스크롤 스파이(Scroll-Spy) 적용기
긴 기술 블로그 아티클을 읽기 편하게 만들기 위해 마크다운 헤딩(#, ##)을 자동 추출하여 동적 목차(TOC)를 생성하고 IntersectionObserver로 실시간 스크롤 스파이를 구현한 경험.
2026. 9. 4.2회
vercel.app 도메인의 한계와 개인 도메인(.pe.kr) 도입기 (feat. 구글 서치 콘솔 색인 문제 해결)
개발
vercel.app 도메인의 한계와 개인 도메인(.pe.kr) 도입기 (feat. 구글 서치 콘솔 색인 문제 해결)
"Vercel 무료 도메인(vercel.app)으로 구글 서치 콘솔에 사이트맵을 제출하며 겪은 무한 '가져올 수 없음' 에러와 '할당량 초과' 문제. 이를 개인 도메인(.pe.kr) 구매로 단 1초 만에 해결한 삽질기를 공유합니다."
2026. 7. 10.17회