[시리즈 4부] 고급 시나리오별 Canonical 전략|가나투데이

[시리즈 4부] 고급 시나리오별 Canonical 전략|가나투데이

"이커머스 천 개 상품, 다국어 사이트, AMP...
복잡한 상황일수록 Canonical이 더 중요합니다."


왜 고급 전략이 필요한가?

기본 canonical 설정으로 해결되는 것:

  • ✅ 단순 중복 페이지
  • ✅ 파라미터 URL 몇 개
  • ✅ www/non-www 통합

하지만 이런 상황은?

❓ 상품 1,000개 × 색상 5개 × 사이즈 7개 = 35,000개 URL
❓ 한국어/영어/일본어/중국어 × 10개 국가 = 40가지 조합
❓ 데스크톱 + 모바일 + AMP = 3배 페이지
❓ React SPA로 URL은 같은데 콘텐츠가 JavaScript로 로딩

이 장에서 배울 것: 복잡한 상황을 체계적으로, 자동화하여 처리하는 실전 전략


Part 1: 다국어/다지역 사이트 - Hreflang × Canonical 마스터 조합

🌍 핵심 개념: Canonical ≠ Hreflang

많은 사람들이 혼동하는 부분:

기능 목적 사용 시점
Canonical 중복 콘텐츠 해결 같은 언어, 다른 URL
Hreflang 언어/지역 타겟팅 다른 언어, 같은 콘텐츠

📐 시나리오별 설정법

케이스 1: 완전히 독립된 언어 버전 (권장)

사이트 구조:

한국어: https://site.com/ko/products
영어: https://site.com/en/products
일본어: https://site.com/ja/products

각 페이지는 자기 자신을 canonical로 지정:

한국어 페이지 (/ko/products):

<!-- Canonical: 자기 자신 -->
<link rel="canonical" href="https://site.com/ko/products" />

<!-- Hreflang: 언어 관계 표시 -->
<link rel="alternate" hreflang="ko" href="https://site.com/ko/products" />
<link rel="alternate" hreflang="en" href="https://site.com/en/products" />
<link rel="alternate" hreflang="ja" href="https://site.com/ja/products" />
<link rel="alternate" hreflang="x-default" href="https://site.com/en/products" />

영어 페이지 (/en/products): 동일 패턴 일본어 페이지 (/ja/products): 동일 패턴

핵심 규칙:

  • ✅ 각 언어 페이지는 자기 자신을 canonical로
  • ✅ 모든 언어 버전을 hreflang으로 연결
  • ✅ x-default는 기본 언어 (보통 영어)

케이스 2: 국가별 도메인

사이트 구조:

한국: https://example.kr/products
미국: https://example.com/products
일본: https://example.jp/products

한국 사이트 (example.kr):

<link rel="canonical" href="https://example.kr/products" />
<link rel="alternate" hreflang="ko-KR" href="https://example.kr/products" />
<link rel="alternate" hreflang="en-US" href="https://example.com/products" />
<link rel="alternate" hreflang="ja-JP" href="https://example.jp/products" />
<link rel="alternate" hreflang="x-default" href="https://example.com/products" />

ISO 639-1 (언어) + ISO 3166-1 (국가) 형식:

  • ko-KR: 한국어, 한국
  • en-US: 영어, 미국
  • ja-JP: 일본어, 일본

케이스 3: 같은 언어, 다른 지역

사이트 구조:

미국 영어: /en-us/products
영국 영어: /en-gb/products
호주 영어: /en-au/products

미국 페이지:

<link rel="canonical" href="https://site.com/en-us/products" />
<link rel="alternate" hreflang="en-US" href="https://site.com/en-us/products" />
<link rel="alternate" hreflang="en-GB" href="https://site.com/en-gb/products" />
<link rel="alternate" hreflang="en-AU" href="https://site.com/en-au/products" />
<link rel="alternate" hreflang="en" href="https://site.com/en-us/products" />

주의: hreflang="en" (언어만) 추가 → 지역 미지정 사용자용


🚫 절대 하지 말아야 할 실수

실수 1: 모든 언어가 하나를 canonical로 지정

<!-- ❌❌❌ 대참사 코드 ❌❌❌ -->

<!-- 한국어 페이지 -->
<link rel="canonical" href="https://site.com/en/products" />

<!-- 일본어 페이지 -->
<link rel="canonical" href="https://site.com/en/products" />

결과:

  • 한국어, 일본어 페이지가 인덱싱 안 됨
  • 해당 국가에서 검색 결과 0건
  • 글로벌 트래픽 -70%

실제 사례:

글로벌 SaaS 기업 B사는 이 실수로 6개월간 한국/일본 시장에서 트래픽 0건. 수정 후 3개월 만에 월 방문자 2만 회복.


실수 2: Hreflang 불일치

<!-- 한국어 페이지 -->
<link rel="alternate" hreflang="ko" href="https://site.com/ko/products" />
<link rel="alternate" hreflang="en" href="https://site.com/en/products" />

<!-- 영어 페이지 (일본어 누락!) -->
<link rel="alternate" hreflang="en" href="https://site.com/en/products" />
<link rel="alternate" hreflang="ko" href="https://site.com/ko/products" />
<!-- ja 누락 → 구글 혼란 -->

규칙: 모든 언어 페이지에 동일한 hreflang 세트 포함


🛠️ 자동화 도구 및 템플릿

WordPress + WPML 플러그인

// functions.php에 추가
function wpml_hreflang_canonical() {
    global $sitepress;
    
    // 현재 페이지 언어
    $current_lang = ICL_LANGUAGE_CODE;
    
    // Canonical: 자기 자신
    echo '<link rel="canonical" href="' . get_permalink() . '" />';
    
    // Hreflang: 모든 언어 버전
    $languages = $sitepress->get_active_languages();
    foreach ($languages as $lang) {
        $url = apply_filters('wpml_permalink', get_permalink(), $lang['code']);
        echo '<link rel="alternate" hreflang="' . $lang['code'] . '" href="' . $url . '" />';
    }
    
    // x-default
    $default_url = apply_filters('wpml_permalink', get_permalink(), 'en');
    echo '<link rel="alternate" hreflang="x-default" href="' . $default_url . '" />';
}
add_action('wp_head', 'wpml_hreflang_canonical');

Next.js (동적 생성)

// components/MultilingualHead.tsx
import Head from 'next/head'
import { useRouter } from 'next/router'

const languages = ['ko', 'en', 'ja']

export default function MultilingualHead() {
  const router = useRouter()
  const currentPath = router.asPath.replace(/^\/[a-z]{2}/, '') // 언어 코드 제거
  
  return (
    <Head>
      {/* Canonical: 현재 언어 */}
      <link rel="canonical" href={`https://site.com${router.asPath}`} />
      
      {/* Hreflang: 모든 언어 */}
      {languages.map(lang => (
        <link
          key={lang}
          rel="alternate"
          hreflang={lang}
          href={`https://site.com/${lang}${currentPath}`}
        />
      ))}
      
      {/* x-default */}
      <link rel="alternate" hreflang="x-default" href={`https://site.com/en${currentPath}`} />
    </Head>
  )
}

📊 검증 도구

Google Search Console:

설정 → 국제 타겟팅 → 언어

경고 확인:
- "hreflang 태그에 반환 태그가 없습니다"
- "hreflang 값이 잘못되었습니다"

Hreflang Tags Testing Tool:

https://www.sistrix.com/hreflang-validator/

URL 입력 → 자동 검증

Part 2: 이커머스 대량 상품 자동화 전략

🛒 문제 정의: 조합 폭발

일반적인 이커머스 상황:

상품 수: 1,000개
변형 옵션:
- 색상: 5가지
- 사이즈: 7가지
- 재질: 3가지

총 URL: 1,000 × 5 × 7 × 3 = 105,000개

수동 설정 불가능 → 자동화 필수


🎯 전략 1: 마스터 SKU 방식 (권장)

개념

마스터: /products/tshirt (기본 옵션)
변형 1: /products/tshirt?variant=red-large
변형 2: /products/tshirt?variant=blue-medium
변형 3: /products/tshirt?variant=green-small

모든 변형이 마스터를 canonical로 지정:

<!-- 변형 페이지들 -->
<link rel="canonical" href="https://shop.com/products/tshirt" />

Shopify 구현 (Liquid)

<!-- theme.liquid 또는 product.liquid -->
{% if product %}
  <link rel="canonical" href="{{ shop.url }}{{ product.url }}" />
{% endif %}

자동 효과:

  • 모든 색상/사이즈 URL이 자동으로 기본 상품 페이지를 canonical로 지정
  • 파라미터(?variant=...) 무시
  • SEO 신호 통합

WooCommerce 구현 (PHP)

// functions.php
add_action('wp_head', 'woocommerce_canonical_variations', 1);
function woocommerce_canonical_variations() {
    if (is_product()) {
        global $post;
        
        // 변형 상품이든 단일 상품이든 메인 URL로
        $canonical = get_permalink($post->ID);
        
        // 파라미터 제거
        $canonical = strtok($canonical, '?');
        
        echo '<link rel="canonical" href="' . esc_url($canonical) . '" />';
    }
}

Magento 구현

<!-- app/design/frontend/[theme]/Magento_Catalog/layout/catalog_product_view.xml -->
<referenceBlock name="head.additional">
    <block class="Magento\Framework\View\Element\Template" name="product.canonical">
        <arguments>
            <argument name="template" xsi:type="string">Magento_Catalog::product/canonical.phtml</argument>
        </arguments>
    </block>
</referenceBlock>
<!-- canonical.phtml -->
<?php
$product = $block->getProduct();
$canonicalUrl = $product->getUrlModel()->getUrl($product, ['_ignore_category' => true]);
?>
<link rel="canonical" href="<?= $block->escapeUrl($canonicalUrl) ?>" />

🎯 전략 2: 독립 SKU 방식 (차별화 콘텐츠 있을 때)

언제 사용?

각 색상마다:
- 다른 상세 설명
- 다른 리뷰
- 다른 이미지 세트
- 다른 가격

→ 실질적으로 다른 상품

이 경우:

<!-- 빨간 티셔츠 -->
<link rel="canonical" href="https://shop.com/products/tshirt-red" />

<!-- 파란 티셔츠 -->
<link rel="canonical" href="https://shop.com/products/tshirt-blue" />

각각 독립 페이지로 SEO


🎯 전략 3: 하이브리드 방식

주요 변형 (색상): 독립 페이지
부차 변형 (사이즈): canonical로 통합

예시:
/tshirt-red (독립)
  ↳ /tshirt-red?size=S → canonical: /tshirt-red
  ↳ /tshirt-red?size=M → canonical: /tshirt-red
  ↳ /tshirt-red?size=L → canonical: /tshirt-red

/tshirt-blue (독립)
  ↳ /tshirt-blue?size=S → canonical: /tshirt-blue
  ↳ /tshirt-blue?size=M → canonical: /tshirt-blue

📊 자동 검증 스크립트

Python (대량 상품 검증)

import requests
from bs4 import BeautifulSoup
import csv

def check_canonical(url):
    try:
        response = requests.get(url)
        soup = BeautifulSoup(response.text, 'html.parser')
        canonical = soup.find('link', {'rel': 'canonical'})
        
        if canonical:
            return canonical.get('href')
        else:
            return 'MISSING'
    except Exception as e:
        return f'ERROR: {e}'

# 상품 URL 리스트
products = [
    'https://shop.com/products/tshirt-red',
    'https://shop.com/products/tshirt-blue',
    # ... 1,000개
]

# 결과 저장
with open('canonical_audit.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['URL', 'Canonical', 'Status'])
    
    for url in products:
        canonical = check_canonical(url)
        status = 'OK' if canonical != 'MISSING' else 'MISSING'
        writer.writerow([url, canonical, status])
        print(f'{url} → {canonical}')

print('검증 완료: canonical_audit.csv 확인')

💡 Pro Tip: 필터 페이지 처리

문제:

/products?category=shoes&color=red&price=low&brand=nike&size=270

해결 1: 모든 필터를 기본 카테고리로

<link rel="canonical" href="https://shop.com/products?category=shoes" />

해결 2: SEO 가치 있는 조합만 독립

/products?category=shoes&color=red → 독립 (인기 조합)
/products?category=shoes&color=red&size=270 → canonical: 위로

Shopify Collection Canonical:

{% if collection %}
  <link rel="canonical" href="{{ shop.url }}{{ collection.url }}" />
{% endif %}

Part 3: 페이지네이션 고급 테크닉

📄 기본 vs 고급

기본 방식 (3부에서 다룸)

<!-- 각 페이지는 자기 자신 -->
<link rel="canonical" href="https://site.com/blog/page/2/" />

고급 방식: View All 페이지 활용

문제: 10페이지 블로그 아카이브
→ 깊은 페이지의 글이 잘 노출 안 됨

해결: "전체 보기" 페이지 생성

구조:

페이지 1: /blog/ (20개 글)
페이지 2: /blog/page/2/ (20개 글)
...
페이지 10: /blog/page/10/ (20개 글)

전체: /blog/all (200개 글 한 번에)

모든 페이지네이션이 "전체"를 canonical로:

<!-- 페이지 1, 2, 3... 모두 -->
<link rel="canonical" href="https://site.com/blog/all" />

장점:

  • 모든 글이 하나의 강력한 페이지에 통합
  • 크롤 예산 절약
  • 순위 집중

단점:

  • 페이지 로딩 느릴 수 있음 (무한 스크롤로 해결)
  • 사용자 경험 고려 필요

🔄 rel="prev/next" (2019년 폐지되었지만 여전히 유용)

Google 공식 입장 (2019년):

"We no longer use prev/next in our indexing."

하지만:

  • Bing, Yandex 등 다른 검색엔진은 여전히 사용
  • 사용자 경험 향상 (브라우저 prefetch)
  • 해가 되지 않으므로 포함 권장
<!-- 2페이지 -->
<link rel="canonical" href="https://site.com/blog/page/2/" />
<link rel="prev" href="https://site.com/blog/" />
<link rel="next" href="https://site.com/blog/page/3/" />

📱 무한 스크롤 페이지 처리

문제:

사용자가 스크롤할 때마다 새 콘텐츠 로딩
→ URL은 변하지 않음
→ 크롤러는 처음 화면만 봄

해결 1: Pushstate로 URL 변경

// 스크롤 시 URL 업데이트
window.history.pushState({}, '', '/blog/page/2');

// 동시에 canonical도 업데이트
document.querySelector('link[rel="canonical"]').href = 
  'https://site.com/blog/page/2/';

해결 2: Paginated Component 사용

<!-- 각 로딩된 섹션에 표시 -->
<div data-page="2" data-canonical="https://site.com/blog/page/2/">
  <!-- 콘텐츠 -->
</div>

Part 4: AMP, 모바일 버전, PDF 처리

AMP (Accelerated Mobile Pages)

구조

일반 페이지: https://site.com/article
AMP 페이지: https://site.com/article/amp 또는 https://amp.site.com/article

일반 페이지 설정

<!-- https://site.com/article -->
<link rel="canonical" href="https://site.com/article" />
<link rel="amphtml" href="https://site.com/article/amp" />

의미:

  • canonical: 이 페이지가 원본
  • amphtml: AMP 버전은 여기

AMP 페이지 설정

<!-- https://site.com/article/amp -->
<link rel="canonical" href="https://site.com/article" />

의미: AMP는 원본을 가리킴 (자기 자신 아님!)


검증

AMP Test Tool:
https://search.google.com/test/amp

URL 입력 → canonical 링크 확인

📱 모바일 별도 URL (M-dot)

2026년 현재 권장 안 함 (반응형 디자인 사용)

하지만 레거시 사이트는:

데스크톱: https://site.com/page
모바일: https://m.site.com/page

데스크톱 페이지:

<link rel="canonical" href="https://site.com/page" />
<link rel="alternate" media="only screen and (max-width: 640px)" 
      href="https://m.site.com/page" />

모바일 페이지:

<link rel="canonical" href="https://site.com/page" />
<link rel="alternate" media="only screen and (min-width: 641px)" 
      href="https://site.com/page" />

📄 PDF 및 다운로드 파일

HTTP 헤더 Canonical (HTML 태그 불가능할 때)

HTTP/1.1 200 OK
Content-Type: application/pdf
Link: <https://site.com/document>; rel="canonical"

서버 설정 (Apache .htaccess):

<FilesMatch "\.pdf$">
    Header set Link '<https://site.com/documents>; rel="canonical"'
</FilesMatch>

서버 설정 (Nginx):

location ~* \.pdf$ {
    add_header Link '<https://site.com/documents>; rel="canonical"';
}

HTML 래퍼 페이지 (더 나은 방법)

PDF 직접 URL: /files/report.pdf
HTML 페이지: /reports/annual-2025

HTML 페이지에서:
- PDF 소개
- 다운로드 버튼
- Canonical: 자기 자신

SEO 이점:

  • HTML 페이지가 인덱싱 (PDF보다 유리)
  • 메타 태그, 설명 추가 가능
  • 사용자 경험 향상

Part 5: JavaScript 렌더링 사이트 (SPA) 특수 케이스

⚛️ 문제: CSR (Client-Side Rendering)

React, Vue, Angular 등:
- 초기 HTML: 거의 비어있음
- JavaScript로 콘텐츠 렌더링
- Canonical 태그도 JS로 추가

문제: 구글봇이 JS 실행 전에 크롤 → canonical 못 봄

🔧 해결책

방법 1: SSR (Server-Side Rendering) - 최선

Next.js (자동 SSR):

// pages/[id].tsx
export async function getServerSideProps({ params }) {
  return {
    props: {
      canonicalUrl: `https://site.com/product/${params.id}`
    }
  }
}

export default function Product({ canonicalUrl }) {
  return (
    <Head>
      <link rel="canonical" href={canonicalUrl} />
    </Head>
  )
}

방법 2: SSG (Static Site Generation)

Next.js (빌드 시 생성):

export async function getStaticProps({ params }) {
  return {
    props: {
      canonical: `https://site.com/page/${params.slug}`
    },
    revalidate: 3600 // 1시간마다 재생성
  }
}

방법 3: Prerendering 서비스

Prerender.io, Rendertron 등:

봇 감지 → 미리 렌더링된 HTML 제공
일반 사용자 → 정상 SPA

Nginx 설정 예시:

location / {
    if ($http_user_agent ~* "googlebot|bingbot|yandex") {
        proxy_pass http://prerender.io/https://yoursite.com$request_uri;
    }
    
    try_files $uri /index.html;
}

방법 4: Dynamic Rendering (Google 권장)

개념: 봇에게만 서버 렌더링, 사용자에게는 CSR

Puppeteer 활용:

const puppeteer = require('puppeteer');

async function renderForBot(url) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle0' });
  
  const html = await page.content();
  await browser.close();
  
  return html; // Canonical 포함된 완전한 HTML
}

// Express.js 미들웨어
app.use((req, res, next) => {
  const isBot = /googlebot|bingbot/i.test(req.headers['user-agent']);
  
  if (isBot) {
    renderForBot(req.url).then(html => res.send(html));
  } else {
    next(); // 일반 SPA 제공
  }
});

검증: Fetch as Google

Google Search Console → URL 검사 → 실제 URL 테스트

"렌더링된 HTML" 탭에서 canonical 확인
→ 있으면 성공, 없으면 SSR/Prerendering 필요

Part 6: 실전 케이스 스터디

📈 Case 1: 글로벌 이커머스 (다국어 + 대량 상품)

회사: 패션 쇼핑몰
규모: 상품 5,000개 × 언어 5개 = 25,000 페이지

Before:

문제:
- 각 언어가 영어를 canonical로 지정 (❌)
- 상품 변형 URL 무분별 (색상별 독립 URL)
- 총 인덱스 페이지: 125,000개

결과:
- 한국/일본 트래픽: 거의 0
- 크롤 예산 초과
- 신상품 인덱싱: 평균 2주 소요

After:

해결:
1. 각 언어 페이지 → 자기 자신 canonical
2. Hreflang 정확히 설정
3. 상품 변형 → 마스터 SKU로 통합
4. 총 인덱스 페이지: 25,000개 (1/5 감소)

결과 (3개월 후):
- 한국 트래픽: 0 → 월 15,000명
- 일본 트래픽: 0 → 월 12,000명
- 크롤 예산: 80% 절약
- 신상품 인덱싱: 평균 2일
- 전체 매출: +45%

📈 Case 2: 뉴스 미디어 (페이지네이션 + AMP)

회사: 온라인 뉴스
규모: 하루 200개 기사 × AMP = 400 페이지/일

Before:

문제:
- 페이지네이션 모두 1페이지를 canonical로 (❌)
- AMP canonical 양방향 설정 안 됨
- 깊은 페이지 기사 인덱싱 안 됨

결과:
- 2페이지 이후 기사 검색 노출: 거의 없음
- AMP 트래픽: 전체의 5% (모바일 70% 시대에)

After:

해결:
1. 각 페이지네이션 → 자기 자신 canonical
2. AMP ↔ 일반 페이지 양방향 링크
3. rel="prev/next" 추가 (Bing 최적화)

결과 (2개월 후):
- 2페이지+ 기사 노출: 350% 증가
- AMP 트래픽: 5% → 42%
- 평균 체류 시간: +38% (AMP 속도 효과)
- 광고 수익: +52%

📈 Case 3: SaaS 플랫폼 (React SPA)

회사: 프로젝트 관리 툴
기술: React SPA (CSR)

Before:

문제:
- Canonical을 JS로만 추가
- 구글봇이 JS 실행 전 크롤
- Search Console: "Canonical 감지 안 됨"

결과:
- 블로그 글 50개 중 인덱싱: 8개만
- 오가닉 트래픽: 월 1,200명 (목표 대비 1/10)

After:

해결:
1. Next.js로 마이그레이션 (SSR)
2. 모든 페이지 getStaticProps로 canonical 생성
3. Sitemap에 canonical URL 명시

결과 (1개월 후):
- 블로그 인덱싱: 50개 전체
- 오가닉 트래픽: 월 1,200 → 9,800명
- 리드 전환: +420%
- 투자 대비 회수: 3주

Part 7: 고급 자동화 스크립트

🤖 대량 Canonical 감사 및 수정

Node.js 스크립트 (전체 사이트 크롤 + 검증)

const axios = require('axios');
const cheerio = require('cheerio');
const fs = require('fs');

async function auditCanonical(url) {
  try {
    const response = await axios.get(url);
    const $ = cheerio.load(response.data);
    
    const canonical = $('link[rel="canonical"]').attr('href');
    const count = $('link[rel="canonical"]').length;
    
    return {
      url,
      canonical: canonical || 'MISSING',
      count,
      status: canonical ? (count === 1 ? 'OK' : 'MULTIPLE') : 'MISSING'
    };
  } catch (error) {
    return {
      url,
      canonical: 'ERROR',
      count: 0,
      status: 'ERROR'
    };
  }
}

// 사이트맵에서 URL 추출
async function getUrlsFromSitemap(sitemapUrl) {
  const response = await axios.get(sitemapUrl);
  const $ = cheerio.load(response.data, { xmlMode: true });
  
  const urls = [];
  $('url > loc').each((i, elem) => {
    urls.push($(elem).text());
  });
  
  return urls;
}

// 메인 실행
(async () => {
  const sitemapUrl = 'https://yoursite.com/sitemap.xml';
  const urls = await getUrlsFromSitemap(sitemapUrl);
  
  console.log(`${urls.length}개 URL 검증 시작...`);
  
  const results = [];
  for (const url of urls) {
    const result = await auditCanonical(url);
    results.push(result);
    console.log(`${result.status}: ${url}`);
  }
  
  // CSV 저장
  const csv = results.map(r => 
    `"${r.url}","${r.canonical}",${r.count},"${r.status}"`
  ).join('\n');
  
  fs.writeFileSync('canonical_audit.csv', 
    'URL,Canonical,Count,Status\n' + csv
  );
  
  // 통계
  const stats = {
    total: results.length,
    ok: results.filter(r => r.status === 'OK').length,
    missing: results.filter(r => r.status === 'MISSING').length,
    multiple: results.filter(r => r.status === 'MULTIPLE').length,
    error: results.filter(r => r.status === 'ERROR').length
  };
  
  console.log('\n=== 검증 완료 ===');
  console.log(`총 페이지: ${stats.total}`);
  console.log(`정상: ${stats.ok} (${(stats.ok/stats.total*100).toFixed(1)}%)`);
  console.log(`누락: ${stats.missing} (${(stats.missing/stats.total*100).toFixed(1)}%)`);
  console.log(`중복: ${stats.multiple} (${(stats.multiple/stats.total*100).toFixed(1)}%)`);
  console.log(`오류: ${stats.error}`);
  console.log('\n결과: canonical_audit.csv');
})();

사용법:

npm install axios cheerio
node canonical_audit.js

🔄 정기 모니터링 (Cron + Slack 알림)

const cron = require('node-cron');
const { WebClient } = require('@slack/web-api');

const slack = new WebClient(process.env.SLACK_TOKEN);

// 매일 오전 9시 실행
cron.schedule('0 9 * * *', async () => {
  console.log('Canonical 모니터링 시작...');
  
  // 위의 audit 함수 실행
  const results = await runAudit();
  
  const issues = results.filter(r => 
    r.status === 'MISSING' || r.status === 'MULTIPLE'
  );
  
  if (issues.length > 0) {
    await slack.chat.postMessage({
      channel: '#seo-alerts',
      text: `⚠️ Canonical 이슈 발견: ${issues.length}개\n` +
            issues.slice(0, 5).map(i => `• ${i.url}`).join('\n')
    });
  } else {
    console.log('✅ 모든 페이지 정상');
  }
});

Part 8: 체크리스트 - 고급 시나리오별

다국어 사이트 체크리스트

  • [ ] 각 언어 페이지는 자기 자신을 canonical로 지정
  • [ ] 모든 언어 버전을 hreflang으로 연결
  • [ ] x-default 지정 (기본 언어)
  • [ ] ISO 639-1 + ISO 3166-1 형식 사용
  • [ ] 모든 언어 페이지에 동일한 hreflang 세트
  • [ ] Google Search Console "국제 타겟팅" 오류 0건

이커머스 체크리스트

  • [ ] 상품 변형 전략 결정 (마스터 SKU vs 독립)
  • [ ] 파라미터 URL canonical 설정
  • [ ] 필터 페이지 canonical 정책
  • [ ] 재고 없는 상품 처리 방침
  • [ ] 계절 상품 canonical 관리
  • [ ] 대량 검증 스크립트 구축

AMP/모바일 체크리스트

  • [ ] 일반 페이지 → amphtml 링크
  • [ ] AMP 페이지 → 일반 페이지 canonical
  • [ ] AMP Test 통과
  • [ ] 모바일 별도 URL이면 alternate 설정
  • [ ] 반응형이면 self-referencing만

SPA 체크리스트

  • [ ] SSR 또는 SSG 구현
  • [ ] 봇 감지 및 prerendering
  • [ ] Google Search Console "렌더링된 HTML" 확인
  • [ ] 동적 canonical 생성 로직 검증
  • [ ] sitemap에 canonical URL 포함

오늘 당장 할 일 (난이도별)

🟢 초급: 다국어 사이트 (30분)

  1. 현재 canonical 확인 (모든 언어 페이지)
  2. 잘못된 설정 수정 (자기 자신으로)
  3. Hreflang 추가
  4. Search Console 검증

🟡 중급: 이커머스 상품 (1시간)

  1. 상품 변형 전략 수립
  2. 템플릿에 canonical 로직 추가
  3. 100개 상품 샘플 검증
  4. 전체 롤아웃

🔴 고급: SPA 마이그레이션 (1일)

  1. Next.js/Nuxt 등 SSR 프레임워크 검토
  2. 핵심 페이지부터 SSR 적용
  3. Prerendering 서비스 설정
  4. 단계적 배포

다음 편 예고

[5부] Canonical 태그 검증 및 지속 관리

"설정했다고 끝이 아닙니다.
매주 10분으로 트래픽을 지키세요."

  • Google Search Console 완전 정복
  • Screaming Frog 고급 활용법
  • 실시간 모니터링 시스템 구축
  • 문제 발생 시 긴급 대응 매뉴얼
  • 자동화된 주간 리포트 생성

📅 3일 후 공개 예정


핵심 요약: 3줄 정리

  1. 다국어 사이트: 각 언어는 자기 자신 canonical + hreflang 필수
  2. 이커머스: 마스터 SKU 방식으로 변형 통합, 자동화 스크립트 필수
  3. SPA: SSR/SSG 없이는 canonical 무용지물, 봇 감지 prerendering 필수

복잡할수록 canonical이 더 중요합니다. 자동화가 답입니다.


📌 이 글이 도움이 되셨다면:

  • 💬 댓글: "우리는 __번 시나리오에 해당해요"
  • 🔖 북마크: 다국어/이커머스 론칭 시 필수 참고
  • 📧 알림 구독: 5부 검증편 놓치지 마세요

5부에서 만나요! 설정한 것을 지속적으로 관리하는 법(Canonical 태그 검증 및 지속 관리|올씽블로거)을 배웁니다.


작성: 2026년 1월 | 시리즈 4/7
참고: Google Search Central, Shopify Developer Docs, hreflang.org, Web.dev

#가나 투데이 #ganatoday

그린아프로