웹 서비스를 운영하다 보면 동일한 콘텐츠가 여러 URL 경로로 접속되는 경우를 흔히 경험해봤을 것이다. 도메인 앞의 www 유무, HTTP와 HTTPS의 접속 혼용, 또는 쇼핑몰 상품 리스트의 정렬 파라미터(?sort=price_asc) 때문에 완전히 같은 내용의 페이지인데도 주소가 수십 개로 갈라지곤 한다. 문제는 검색엔진 크롤러가 이 주소들을 각각 서로 다른 페이지로 인식하거나 중복 콘텐츠로 판단해 사이트 전체의 검색 순위 점수를 깎아내린다는 점이다. 이번에는 canonical 태그가 정확히 무엇이고 왜 필요한지, 그리고 백엔드 환경에서 이를 올바르게 구현하는 방법까지 완벽하게 정리해서 소개하겠다.
Canonical 태그는 여러 개의 중복되거나 유사한 URL 중에서 검색엔진 크롤러가 '대표(원본) 페이지'로 인식해야 하는 주소를 명시적으로 알려주는 HTML 메타 요소다. 크롤러는 웹 사이트를 탐색할 때 수집한 URL마다 독립적인 점수를 부여하고 색인을 생성한다. 만약 동일한 내용이 3개의 다른 URL로 접근 가능하다면, 검색엔진이 부여하는 평가 점수(PageRank)도 3등분으로 쪼개지게 된다.
결국 대표 주소를 지정하지 않으면 정작 검색 결과에 노출되어야 할 원본 페이지의 순위가 떨어지거나, 검색엔진이 임의로 원치 않는 URL을 대표 페이지로 선정하는 문제가 발생한다.
| 비교 항목 | Canonical 미설정 (중복 URL 방치) | Canonical 설정 (대표 URL 지정) |
|---|---|---|
| 페이지 점수 (PageRank) | 여러 URL로 분산되어 개별 순위 하락 | 모든 평가 점수가 대표 URL 하나로 집중 |
| 크롤링 예산 (Crawl Budget) | 중복 페이지 수집으로 리소스 낭비 | 대표 페이지 중심으로 효율적 크롤링 진행 |
| 검색 결과 노출 URL | 파라미터가 붙은 지저분한 주소 노출 가능 | 정제된 대표 주소만 깔끔하게 노출 |
| 색인 생성 여부 | 중복 콘텐츠로 판정되어 색인 제외 위험 | 원본 콘텐츠로 정상 인식 및 안정적 색인 |
canonical 태그는 웹 페이지의 <head> 구역 내에 지정하며, 반드시 상대 경로가 아닌 프로토콜(https)과 도메인이 포함된 완전한 절대 경로를 사용해야 한다.
가장 보편적인 방법은 HTML 상단에 link 태그를 삽입하는 방식이다.
<link rel="canonical" href="https://example.com/products/view?id=100" />
PDF 파일이나 이미지 등 HTML 태그를 직접 넣을 수 없는 비-HTML 리소스의 경우, 웹 서버나 백엔드 코드에서 HTTP 응답 헤더로 canonical을 전달할 수 있다.
Link: <https://example.com/downloads/manual.pdf>; rel="canonical"
실무 개발 환경에서는 페이지마다 canonical 주소를 일일이 하드코딩할 수 없다. URL 접속 환경(HTTP/HTTPS, 파라미터 유무)이 달라지더라도, 불필요한 추적 파라미터(utm_source 등)나 정렬 옵션을 제거하고 순수한 대표 주소만 정제해서 출력해주는 서포트 로직이 필수적이다.
다음은 현재 요청된 URL을 분석하여 대표 주소만 추출하는 잘못된 예시와 올바른 구현 방식이다.
상대 경로를 사용하거나 요청된 GET 파라미터를 검증 없이 그대로 canonical에 포함하는 방식이다. 이렇게 작성하면 파라미터에 따라 canonical 주소도 같이 변경되어 중복 해결 효과가 완전히 사라진다.
<?php
// 잘못된 예시: 현재 접속된 Request URI를 그대로 사용하고 상대 경로로 출력함
$currentUrl = $_SERVER['REQUEST_URI'];
?>
<!-- 상대 경로 사용 및 불필요한 파라미터가 그대로 노출됨 -->
<link rel="canonical" href="<?php echo $currentUrl; ?>" />
허용할 필수 파라미터(예: 게시글 ID)만 남기고 나머지 추적용/정렬용 파라미터는 모두 제거한 뒤, 도메인과 프로토콜을 강제로 결합하여 완벽한 절대 경로를 만드는 함수다.
<?php
/**
* 안전한 정제된 Canonical URL 생성 함수
*
* @param string $baseUrl 기본 도메인 및 경로 (프로토콜 포함)
* @param array $allowedParams 유지할 핵심 GET 파라미터 키 배열
* @return string
*/
function getCanonicalUrl(string $baseUrl, array $allowedParams = []): string {
$parsedUrl = parse_url($baseUrl);
$scheme = 'https'; // 보안을 위해 HTTPS 고정
$host = $_SERVER['HTTP_HOST'] ?? 'example.com';
$path = $parsedUrl['path'] ?? parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// 허용된 파라미터만 추출하여 재구성
$filteredParams = [];
foreach ($allowedParams as $paramKey) {
if (isset($_GET[$paramKey]) && $_GET[$paramKey] !== '') {
$filteredParams[$paramKey] = $_GET[$paramKey];
}
}
$queryString = !empty($filteredParams) ? '?' . http_build_query($filteredParams) : '';
return $scheme . '://' . $host . $path . $queryString;
}
// 실전 적용: 상품 상세 페이지 (id 파라미터만 허용, utm_source나 sort는 무시)
$canonicalUrl = getCanonicalUrl('https://example.com/product/detail', ['id']);
?>
<link rel="canonical" href="<?php echo htmlspecialchars($canonicalUrl, ENT_QUOTES, 'UTF-8'); ?>" />
사용자가 https://example.com/product/detail?id=50&sort=price_asc&utm_source=facebook 으로 접속하더라도 아래와 같이 깔끔한 대표 주소로 정제되어 출력된다.
<link rel="canonical" href="https://example.com/product/detail?id=50" />
canonical 태그를 잘못 설정하면 검색엔진이 사이트의 주요 페이지 전체를 색인에서 삭제해버리는 치명적인 사고가 발생할 수 있다. 다음 4가지 핵심 주의사항을 반드시 체크해야 한다.
첫째, 상대 경로를 절대 쓰지 말자. <link rel="canonical" href="/posts/12" /> 처럼 상대 경로로 작성하면 일부 크롤러가 도메인을 오인하거나 잘못된 주소로 해석할 수 있다. 반드시 https://를 포함한 전체 주소를 적어야 한다.
둘째, 404 에러 페이지나 301 리다이렉트되는 주소를 canonical로 지정하면 안 된다. 크롤러는 최종적으로 HTTP 200 OK 상태 코드를 반환하는 정상적인 원본 주소만 인지한다.
셋째, noindex 메타태그와 canonical을 동일한 페이지에 동시에 사용해서는 안 된다. 두 명령이 충돌할 경우 크롤러는 canonical 신호를 무시하게 된다.
넷째, 자기 참조(Self-referencing) canonical을 적극 권장한다. 변형된 URL이 없는 순수한 대표 페이지라 할지라도, 자기 자신의 URL을 canonical로 스스로 가리키도록 설정해야 외부에서 불필요한 파라미터로 링크를 걸었을 때 페이지 점수 유실을 방지할 수 있다.
Canonical 태그는 검색엔진에 내 사이트의 대표 주소를 정확히 알려주는 가장 기본적인 SEO 최적화 도구다. 작은 URL 파라미터 하나로 발생하는 중복 데이터가 쌓여 사이트 전체의 검색 노출 성능을 떨어뜨린다는 점을 잊지 말자. 오늘 다룬 PHP 동적 생성 예제와 필수 체크리스트를 바탕으로 사이트의 메타 태그를 바로 점검하고 적용한다면, 분산되던 점수를 하나로 모아 훨씬 안정적인 검색 순위 향상 결과를 얻을 수 있을 것이다.