웹사이트를 운영하다 보면 같은 콘텐츠가 여러 URL로 접근 가능한 경우가 많다. 예를 들어 /product?id=123과 /products/123, 그리고 /product/123?utm_source=email 같은 식으로 말이다. 검색 엔진은 이들을 서로 다른 페이지로 인식해서 색인을 분산시키고, 결과적으로 SEO 순위를 깎아먹는다. 대부분의 개발자는 이 문제가 심각하다는 걸 알면서도, 어떻게 해야 하는지, 또는 canonical 태그를 썼는데도 왜 안 되는지 정확히 모르는 채로 넘어간다.
이번에는 URL 중복이 왜 생기는지, 어떻게 검색 결과에서 우리 의도대로 정규화할 수 있는지, 그리고 가장 흔한 실수들을 완벽하게 정리해서 소개하겠다.
검색 엔진이 같은 콘텐츠를 여러 URL로 크롤링하면, 어느 URL을 '정식 버전'으로 볼지 판단해야 한다. 아무것도 지시하지 않으면 엔진이 자의적으로 선택하는데, 이게 우리가 원하는 URL이 아닐 수도 있다. 그러면 검색 유입이 분산되고, 각 URL의 링크 파워가 약해진다. 누군가 우리 사이트를 링크할 때 여러 버전이 섞여 있으면, 한 URL에 집중되어야 할 권위도 쪼개진다.
URL 중복의 일반적인 원인은 이렇다. 쿼리 파라미터 추적 코드(utm_source, fbclid 등), 슬래시 여부(/product와 /product/), 프로토콜 차이(http와 https), 도메인 변형(www와 비www), 세션 ID, 정렬/필터 옵션 등이다. 특히 전자상거래 사이트나 블로그는 필터링이나 페이징으로 인해 수백 개의 중복 URL을 무의식적으로 만든다.
canonical 태그는 <head> 섹션에 정식 URL을 명시하는 방법이다. 검색 엔진에 '이 페이지의 진짜는 이 URL이다'라고 말해주는 것이다.
<!-- ✗ 잘못된 방법: canonical을 빈값으로 설정 -->
<link rel="canonical" href="" />
<!-- ✗ 잘못된 방법: 상대 경로 사용 -->
<link rel="canonical" href="/product/123" />
<!-- ✓ 올바른 방법: 절대 URL 사용 -->
<link rel="canonical" href="https://example.com/product/123" />
canonical은 항상 절대 URL(도메인 포함)을 가져야 한다. 상대 경로는 검색 엔진이 해석하지 못할 수 있다. 또한 HTTPS 사이트라면 canonical도 HTTPS여야 한다.
모든 페이지의 canonical이 자기 자신을 가리키게 하는 방식이다. 이렇게 하면 검색 엔진이 명확한 신호를 받는다.
<?php
// PHP에서 현재 페이지의 정식 URL 생성
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$canonical = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
// 쿼리 파라미터 제거 (utm_source 같은 추적 코드 무시)
if (strpos($canonical, '?') !== false) {
$canonical = strtok($canonical, '?');
}
echo '<link rel="canonical" href="' . htmlspecialchars($canonical) . '" />';
?>
결과: <link rel="canonical" href="https://example.com/product/123" />
필터링된 상품 목록 페이지라면 정렬/필터 파라미터를 포함해야 할지, 제거할지 판단해야 한다. 만약 같은 내용이라면 파라미터를 제거한 기본 URL을 canonical로 지정한다.
<?php
// URL 파라미터 중 필요한 것만 유지
$allowed_params = ['page', 'sort']; // utm_*, fbclid 등은 제외
$canonical_params = [];
parse_str($_SERVER['QUERY_STRING'], $query_params);
foreach ($allowed_params as $param) {
if (isset($query_params[$param])) {
$canonical_params[$param] = $query_params[$param];
}
}
$canonical = 'https://example.com/products';
if (!empty($canonical_params)) {
$canonical .= '?' . http_build_query($canonical_params);
}
echo '<link rel="canonical" href="' . htmlspecialchars($canonical) . '" />';
?>
결과: 방문자가 /products?sort=price&utm_source=email로 접근하면, canonical은 /products?sort=price로만 설정된다. utm_source 같은 추적 파라미터는 무시한다.
canonical은 검색 엔진에 대한 '제안'일 뿐, 반드시 따라야 할 명령은 아니다. 더 확실하게 하려면 서버에서 301 리다이렉트를 해야 한다. 예를 들어 /product와 /product/을 /product로 통일한다.
<?php
// ✗ 잘못된 방법: 302 사용
header('Location: https://example.com/product', true, 302);
exit;
// ✓ 올바른 방법: 301 영구 리다이렉트
header('Location: https://example.com/product', true, 301);
exit;
?>
301은 검색 엔진에 '이 URL은 영구적으로 다른 곳으로 이동했다'고 알려서 링크 파워도 전달한다. 302는 '임시 이동'이라는 신호라 검색 엔진이 여전히 원래 URL을 색인한다.
# Nginx - /product/을 /product로 리다이렉트
if ($request_uri ~* ^/product/+$) {
return 301 /product;
}
# Apache - .htaccess
RewriteRule ^product/$ /product [R=301,L]
Google Search Console에서 URL 파라미터 처리 방법을 명시할 수 있다. 하지만 현대적인 방법은 canonical을 사용하는 것이다.
✗ Canonical 태그가 상대 경로로 설정되어 있다. 검색 엔진이 전체 URL로 해석하지 못할 수 있다.
✓ 항상 https://example.com/product/123 같은 절대 경로를 사용한다.
✗ 모든 페이지의 canonical이 홈페이지를 가리킨다. 이렇게 하면 각 페이지가 색인되지 않는다.
✓ 각 페이지는 자신의 정식 URL을 가리켜야 한다.
✗ Canonical과 리다이렉트가 모순된다. 예를 들어 /product로 301 리다이렉트하면서 동시에 /product/의 canonical을 /product/로 설정한다.
✓ 리다이렉트와 canonical 설정을 일치시킨다.
✗ 파라미터를 제거한 canonical을 설정했는데, 실제로는 파라미터마다 다른 콘텐츠가 표시된다. 예를 들어 ?sort=price와 ?sort=date는 정렬 순서가 다르다.
✓ 실제로 같은 콘텐츠인 파라미터만 제거하고, 다른 내용을 만드는 파라미터는 유지한다.
Google Search Console에 로그인한 후, 좌측 메뉴에서 '페이지'를 클릭하면 어떤 URL이 '정식'으로 선택되었는지 볼 수 있다. 만약 우리가 지정한 canonical과 다르다면, 그 이유가 표시된다. 대개 리다이렉트 체인이 있거나 canonical 태그가 정확하지 않을 때다.
URL 정규화는 단순해 보이지만 매우 중요한 SEO 기초다. Canonical 태그 한 줄이 검색 결과 순위를 올리거나 내릴 수 있다. 이 글의 canonical 설정 패턴과 리다이렉트 규칙을 참고해서, 지금 바로 자신의 사이트를 점검해보자. 특히 쿼리 파라미터가 많은 전자상거래나 필터 기능이 있는 사이트라면 더욱 그렇다. 작은 정규화 작업이 모여서 SEO 효과를 크게 만든다는 점을 절대 잊지 말자.