인프라

다국어 SSR SEO: hreflang, canonical, noindex 예산

하나의 아티클을 여러 언어로 제공하는 것은 hreflang, 캐노니컬 URL, noindex 예산이 일치하지 않는 한 검색 순위를 분산시킵니다. 그 방법은 다음과 같습니다.

이 글은 영어 원문을 AI 모델이 번역한 것입니다. 표현이 원문과 다를 수 있습니다. 영어 원문 보기

하나의 영어 아티클을 여러 언어로 번역하고 각각을 서버사이드 렌더링으로 제공하는 것은 검색 트래픽에 순전히 이득인 것처럼 들립니다. 하지만 이는 자신의 순위를 분산시키고, 빈약한 페이지로 인덱스를 넘쳐나게 하며, 중복 콘텐츠로 플래그 지정될 수 있는 빠른 길이기도 합니다. 검색 기반 블로그의 모든 것은 인덱스가 깨끗하게 유지되는 것에 달려 있으므로, 언어 계층은 SEO를 마무리가 아닌 첫 번째 제약 조건으로 삼아 구축해야 합니다. 이 게시물에서는 인덱스를 깨끗하게 유지하는 세 가지 제어 방법, 즉 절대 URL이 생성되는 방식, canonical 태그와 hreflang 태그의 관계, 그리고 실제로 노출할 번역본을 noindex 예산이 어떻게 결정하는지를 살펴봅니다.

다국어 사이트의 중복 콘텐츠 함정

5개 언어로 된 동일한 기사는 여러분이 부주의할 경우 검색 엔진이 경쟁자로 취급할 수 있는 5개의 URL입니다. 이 문제가 잘못되는 두 가지 뚜렷한 방식이 있으며, 이들은 복합적으로 작용합니다.

첫 번째는 언어 중복입니다. 만약 한국어 버전과 일본어 버전이 구조상 거의 동일하고 엔진이 그것들이 하나의 원본을 번역한 것임을 구별할 수 없다면, 엔진은 하나를 선택하고 나머지를 무시하거나, 인바운드 링크의 권한을 모든 버전에 분산시킬 수 있습니다. 여러분은 언어별로 하나의 강력한 페이지를 원했습니다. 하지만 얻은 것은 한 페이지의 다섯 개 약한 조각이었습니다.

두 번째는 호스트 중복이며, 사람들이 예상하는 것보다 쉽게 발생합니다. 만약 서버가 들어오는 요청 호스트로부터 절대 URL을 생성한다면, 동일한 콘텐츠가 두 개의 호스트 이름으로 접근 가능해지는 순간, 예를 들어 베어 도메인과 www 하위 도메인, 또는 유출된 스테이징 별칭과 같이, 검색 엔진은 모든 페이지의 전체 사본을 두 개씩 보게 됩니다. 이제 5개의 언어 버전은 10개, 또는 15개가 되고, 각 분할은 순위 신호를 약화시킵니다. 두 문제에 대한 해결책은 동일한 지점에서 시작됩니다. 사이트는 자체 URL이 무엇인지에 대해 정확히 하나의 의견만을 가져야 합니다.

요청 호스트로 절대 URL을 생성하지 마세요

가장 중요한 단 하나의 규칙은 지루합니다. 절대 URL은 하나의 고정된 기본값에서 생성되며, 절대 요청에서 생성되지 않습니다. Go SSR 서버에서는 canonical, hreflang, Open Graph, sitemap URL이 모두 설정된 SITE_BASE_URL에서 나오며, 링크를 구성하기 위해 r.Host를 읽지 않는다는 것을 의미합니다.

// config.SiteBaseURL is the one source of truth, e.g. "https://lucidnote.net".
// Do not read r.Host here. A request arriving on any alias still emits
// the same canonical origin, so the index sees one copy, not one per host.
func absURL(base, lang, slug string) string {
	return base + "/" + lang + "/" + slug
}

r.Host로 링크를 생성하면, 서버로 확인되는 모든 호스트 이름이 인덱스에서 전체 사이트의 평행 우주가 됩니다. 고정된 기본값은 이 모든 것을 하나로 통합합니다. 이것은 페이지별 결정이나 템플릿의 세부 사항이 아닙니다. 이것은 전체 서버의 속성이며, 어떤 핸들러라도 링크를 만들기 위해 요청 호스트에 접근하면 실패하는 테스트를 만들 가치가 있습니다.

모든 페이지가 자신의 절대 주소에 동의하면, 중복을 관리하는 두 태그인 canonical과 hreflang은 확고한 기반을 갖게 됩니다.

캐노니컬은 하나의 URL을 원본으로 지정합니다

캐노니컬 태그는 콘텐츠에 대한 권위 있는 URL이 무엇인지 엔진에 알려줍니다. 잘 구축된 다국어 사이트에서는 각 언어 페이지가 자기 자신을 캐노니컬로 지정합니다. 한국어 페이지는 한국어 URL을 캐노니컬로 지정하고, 일본어 페이지는 일본어 URL을 캐노니컬로 지정합니다. 이들은 서로의 중복이 아니라 대체 페이지이며, hreflang이 그 관계를 별도로 표현합니다.

캐노니컬이 제 역할을 하는 곳은 단일 언어 내에서입니다. 추적 매개변수로 접근할 수 있는 페이지나 후행 정렬 인수가 있거나 없는 카테고리 목록을 생각해 보세요. 이러한 변형 페이지들은 모두 깔끔한 URL을 가리키는 캐노니컬을 포함해야 하며, 그래야 쿼리 문자열 사본이 실제 페이지와 경쟁하지 않습니다. 규칙은 간단합니다. 캐노니컬은 한 언어 내의 중복을 해결하며, 위와 같은 이유로 항상 고정된 기준에서 생성된 절대 URL을 가리켜야 합니다.

흔한 실수는 모든 번역본을 영어 원본에 대한 캐노니컬로 지정하는 것입니다. 이는 다른 언어들이 영어의 중복이며 자체적으로 색인이 생성되어서는 안 된다고 엔진에 알리는 것이므로, 번역을 통해 얻으려던 트래픽을 버리는 셈입니다. 언어별 자기 참조 캐노니컬이 거의 항상 원하는 방식입니다. 언어 간 관계는 hreflang의 역할입니다.

hreflang은 모든 언어 변형을 대칭적으로 매핑합니다

hreflang은 페이지가 다른 언어로 된 형제 페이지를 선언하는 방법입니다. 각 페이지는 자신을 포함하여 사용 가능한 모든 언어에 대해 link rel="alternate"를 나열하고, 지원하지 않는 언어 사용자를 위한 대체(fallback)를 지정하는 x-default를 추가합니다. 엔진은 이를 사용하여 우연히 먼저 크롤링된 버전을 보여주는 대신, 한국어 검색자에게는 한국어 버전을, 일본어 검색자에게는 일본어 버전을 보여줍니다.

다음은 한 기사의 영어 버전 헤드(head)로, 번역된 4개의 형제 페이지와 기본값을 선언합니다.

<link rel="canonical" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="en" href="https://lucidnote.net/en/multilingual-ssr-seo">
<link rel="alternate" hreflang="ko" href="https://lucidnote.net/ko/multilingual-ssr-seo">
<link rel="alternate" hreflang="ja" href="https://lucidnote.net/ja/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hans" href="https://lucidnote.net/zh-Hans/multilingual-ssr-seo">
<link rel="alternate" hreflang="zh-Hant" href="https://lucidnote.net/zh-Hant/multilingual-ssr-seo">
<link rel="alternate" hreflang="x-default" href="https://lucidnote.net/en/multilingual-ssr-seo">

두 가지 속성이 이것의 성패를 좌우합니다. 첫 번째는 대칭성입니다. 만약 영어 페이지가 한국어를 대체 페이지로 지정하면, 한국어 페이지도 영어를 다시 대체 페이지로 지정해야 합니다. hreflang은 상호 선언이며, 엔진은 원래 페이지를 다시 가리키지 않는 대체 페이지를 신뢰할 수 없으므로 일방적인 세트는 조용히 무시합니다. 서버가 이러한 태그를 렌더링할 때, 해당 기사에 사용 가능한 언어 목록 하나로부터 전체 대체 세트를 생성하여 모든 변형이 동일하고 완전하며 상호적인 블록을 출력하도록 해야 합니다. 템플릿마다 수동으로 목록을 작성하면 비대칭성이 슬그머니 들어오게 됩니다.

두 번째는 세트가 실제로 제공되는 내용을 반영해야 한다는 것입니다. 어떤 언어가 번역에 실패하여 페이지가 존재하지 않는 경우, hreflang 대체 페이지로 나타나서는 안 됩니다. 누락되거나 깨진 변형으로의 링크는 전체 클러스터를 오염시킵니다. 깔끔한 접근 방식은 사이트가 이론적으로 지원할 수 있는 정적 언어 목록이 아니라, 해당 슬러그(slug)에 대해 성공적으로 게시된 언어 버전 세트에서 대체 목록을 파생시키는 것입니다.

noindex 예산: 모든 것을 번역하고, 선별적으로 노출하기

대부분의 가이드가 건너뛰는 부분이 여기 있습니다. hreflang과 canonical을 올바르게 설정하면 모든 언어를 안전하게 색인화할 수 있습니다. 그렇다고 해서 그렇게 해야 한다는 의미는 아닙니다.

색인에 포함시키도록 요청하는 모든 URL은 엔진이 품질을 판단하는 페이지입니다. 신생 사이트에서, 총 3개의 게시물만 있는 언어로 새로 번역된 페이지는 빈약한 페이지입니다. 주변 콘텐츠가 거의 없고, 형제(sibling) 깊이가 없으며, 들어오는 내부 링크도 없습니다. 모든 언어와 카테고리에 걸쳐 이러한 페이지 수백 개를 색인화하면, 색인화된 집합의 평균 품질이 떨어집니다. 그 평균은 중요합니다. 검색 순위와 광고 네트워크 심사는 모두 사이트 전체를 읽으며, 수많은 빈약한 페이지들은 실제로 중요하게 생각하는 페이지들의 순위까지 끌어내립니다.

그래서 우리는 의도적으로 noindex 예산을 사용합니다. 번역과 저장은 비용이 저렴하고 모든 언어에 대해 실행됩니다. 왜냐하면 노출할 가치가 생기는 순간 콘텐츠가 준비되어 있기를 원하기 때문입니다. 색인화는 희소한 자원이며, 자동이 아니라 부여되는 것입니다. 두 가지 규칙이 이를 통제합니다.

첫 번째는 언어 허용 목록입니다. 색인화 가능한 언어 집합은 제공되는 언어 집합의 하위 집합입니다. 제공되지만 색인화되지 않은 언어 페이지에 방문한 방문자는 여전히 완전하고 정확한 페이지를 보게 되지만, 해당 페이지는 noindex를 포함하고 사이트맵에서 제외됩니다. 예를 들어, 5개 언어 모두를 제공하면서도, 자체적으로 설 수 있을 만큼 충분한 깊이를 가진 두 언어만 (색인에) 초대하는 것입니다.

두 번째는 카테고리별 임계값입니다. 특정 언어의 카테고리 페이지는 예를 들어 최소 3개와 같이 일정 수 이상의 게시물이 발행되었을 때만 색인화됩니다. 그 수에 미치지 못하면, 해당 카테고리 목록은 정의상 빈약하므로 noindex 처리되고 채워질 때까지 사이트맵에서 제외됩니다. 번역본은 여전히 존재하고, 페이지는 여전히 렌더링되며, URL은 해당 페이지에 도달한 사람에게 여전히 작동합니다. 단지 아직 크롤러에게 알려지지 않았을 뿐입니다.

결과적으로 언어와 카테고리의 전체 매트릭스를 번역하고 저장하되, (품질을) 희석시키기보다는 도움이 될 만큼 충분히 밀도가 높은 셀만 노출하게 됩니다.

하나의 표로 보는 정책

모든 페이지는 몇 가지 상태 중 하나에 속하며, 네 가지 SEO 컨트롤은 각 상태에 대해 동의해야 합니다. 저장 및 렌더링은 색인 생성과 독립적입니다.

페이지 상태 번역 및 저장됨 방문자에게 렌더링됨 canonical hreflang 대체 robots 사이트맵에 포함
색인 생성 가능 언어, 밀집 카테고리 자체 전체 상호 집합 index, follow
제공되지만 색인 생성 불가능한 언어 자체 색인 생성된 형제 페이지만 나열 noindex, follow 아니요
게시물 임계값 미만 카테고리 자체 해당 게시물의 전체 집합 noindex, follow 아니요
쿼리 매개변수 변형 해당 없음 정리된 URL 정리된 URL에서 상속됨 index, follow 아니요
번역에 실패한 언어 아니요 아니요 해당 없음 모든 곳에서 생략됨 해당 없음 아니요

사람들을 헷갈리게 하는 행은 두 번째 행입니다. 색인 생성이 불가능한 언어 페이지는 여전히 렌더링되고 hreflang을 통해 형제 페이지를 선언하지만, 색인을 생성하는 페이지에서 대체 페이지로 나열되어서는 안 됩니다. 왜냐하면 크롤러가 noindex 대상을 실제 대안으로 가리키도록 하고 싶지 않기 때문입니다. 색인 생성된 클러스터가 색인 생성된 멤버를 참조하도록 유지하고, 추가 언어는 직접 탐색하는 사용자를 위해 존재하도록 하십시오.

사이트맵, robots, canonical, hreflang의 일관성 유지하기

이 네 가지 컨트롤은 한 세트로만 작동합니다. 실패 모드는 하나의 잘못된 태그가 아니라 두 태그가 서로 다른 것입니다. 이는 크롤러에게 혼란스러운 사이트로 읽히며 보호하려던 크롤링 예산을 낭비하게 됩니다.

사이트맵에는 색인을 생성하려는 URL만 정확히 나열해야 하며, noindex가 있는 것은 포함해서는 안 됩니다. 사이트맵에 noindex 페이지가 있는 것은 모순입니다. 크롤러에게 페이지를 가져오도록 요청한 다음, 찾은 내용을 잊으라고 말하는 셈입니다. robots 메타 태그는 특정 페이지의 색인 생성 여부에 대한 권한을 가지며, 사이트맵은 단순히 이에 동의해야 합니다. canonical은 색인 생성이 가능한 URL만을 가리켜야 하므로, 빈약한 버전이 스스로를 어떤 것의 권위 있는 사본으로 지정하는 일이 없어야 합니다. 그리고 hreflang은 색인이 생성된 클러스터 내에서, 그 자체로 색인이 생성된 페이지만을 참조해야 합니다.

이것들의 일관성을 유지하는 방법은 한 곳에서 색인 결정을 한 번만 계산하고, 네 가지 출력이 모두 그 결정을 읽도록 하는 것입니다. 서버나 수집 단계에서 주어진 (언어, 카테고리, 게시물)이 색인 생성이 가능하다고 결정하면, 그 단일 불리언 값이 robots 태그, 사이트맵 포함 여부, canonical 대상, 그리고 어떤 hreflang 대체 항목이 생성될지를 결정합니다. 만약 각 출력이 자체적으로 결정을 내리면, 서로 달라지게 되며, 바로 그 차이 때문에 불이익을 받게 됩니다.

트레이드오프, 그리고 모든 것을 색인해야 할 때

색인을 좁히는 데는 분명한 대가가 따릅니다. 색인된 페이지가 적다는 것은 첫날부터 검색을 통한 진입점이 더 적다는 것을 의미하므로, 모든 스텁의 모든 번역을 노출했을 경우보다 초기 트래픽이 더 낮습니다. 이는 현재의 도달 범위를 시간 경과에 따른 품질 점수와 맞바꾸는 것이며, 소수의 강력한 페이지가 다수의 약한 페이지보다 순위가 높을 것이라는 데 베팅하는 것입니다. 그리고 검색 및 광고 기반 사이트에서는 실제로 그렇게 됩니다.

이 예산은 영구적이지 않습니다. 이는 신생 사이트나 콘텐츠가 부족한 사이트에 적합한 상태이며, 콘텐츠가 축적됨에 따라 완화되어야 합니다. 한 카테고리가 게시물 임계값을 넘어서면 자체적으로 색인되기 시작합니다. 한 언어가 여러 카테고리에 걸쳐 충분한 깊이를 확보하면, 이를 색인 허용 목록에 추가해도 더 이상 빈약한 페이지가 생성되지 않습니다. 제어 방식은 동일하며, 임계값만 바뀔 뿐입니다.

빈약한 페이지의 위험이 사라지면 모든 것을 색인할 수 있습니다. 이는 한 언어의 거의 모든 카테고리가 이미 임계값을 통과하고, 내부 링크가 각 페이지에 실제적인 형제 컨텍스트를 제공하며, 원어민 검색자가 도착하자마자 이탈하지 않을 정도로 번역 품질이 충분히 좋을 때 발생합니다. 그 시점에서는 noindex 예산이 제 역할을 다한 것이며, 더 넓은 색인은 부채가 아닌 자산이 됩니다. 그때까지는 전체 매트릭스를 번역하고, hreflang과 canonical을 엄격하고 대칭적으로 유지하며, 제 역할을 할 수 있는 페이지에 색인을 사용하십시오.

관련 글