[실전 SEO 07] ?쿼리 붙은 주소가 만드는 중복 URL 지옥과 canonical

게시판을 만들면 자연스럽게 필터와 정렬이 붙는다. board=free, flair=질문, sort=recent, page=2 같은 쿼리 파라미터다. 사람에겐 더없이 편리한 기능이지만, 검색엔진에겐 같은 콘텐츠를 무한히 복제하는 지뢰밭이다. 신생 도메인의 크롤 예산을 가장 빠르게 태워먹는 주범 중 하나가 바로 이 물음표 뒤의 파라미터다.


이 문제가 특히 고약한 건, 코드상 버그가 하나도 없는데 발생한다는 점이다. 필터도 정렬도 완벽하게 동작하고 사용자도 만족한다. 그런데 그 정상 기능이 봇의 눈에는 거의 똑같은 수백 개의 유사 중복 문서로 비친다. 잘 만든 기능이 그대로 SEO 부채가 되는 대표적인 사례라, 기능이 멀쩡하다는 이유로 방치하기 딱 쉽다. 이번 편은 이 중복 URL이 왜 생기고 어떻게 막는지다.


1. 파라미터 하나가 URL을 무한 복제한다

구글봇은 URL 문자열이 단 한 글자라도 다르면 다른 페이지로 인식한다. 같은 글 목록이라도 ?sort=recent와 ?sort=popular가 서로 다른 URL이고, ?board=free와 ?flair=질문의 조합이 또 다른 URL이다. 사람에겐 같은 목록의 다른 보기지만 봇에겐 전부 별개 문서다.


파라미터가 몇 개만 겹쳐도 조합이 폭발적으로 늘어난다. 정렬 3종, 필터 10종, 페이지 20개만 단순히 곱해도 수백 개의 URL이 나온다. 실제 콘텐츠는 거의 같은데 주소만 수백 가지가 되는 것이다. 봇은 이 유사 중복 URL들을 일일이 크롤하느라 정작 새로 올라온 진짜 글은 못 본다. 크롤 예산 폭식이고, 심하면 중복이 너무 많은 저품질 사이트로 도메인 전체가 감점되기까지 한다.


이 문제를 처음 자각한 계기가 크롤 통계였다. 글은 수백 개인데 봇이 긁는 URL 수는 수천 건이었다. 대체 뭘 그렇게 긁나 봤더니 전부 필터와 정렬 조합이었다. 실체는 목록 하나인데 봇에겐 수천 개의 다른 문서로 보였던 것이다. 이 숫자를 보고 나서야 파라미터 문제의 심각성을 체감했다.


2. canonical로 대표를 지정한다

1차 방어선은 rel=canonical이다. 각 변형 URL의 head에, 파라미터가 없는 정규 주소를 대표로 지정해두는 것이다. 그러면 구글은 여러 변형을 하나의 대표 페이지로 묶어서 이해하려 시도한다. 색인에는 대표 하나만 남기려는 것이다.


다만 canonical은 어디까지나 힌트지 명령이 아니다. 구글이 종합적으로 판단해서 무시할 수도 있다. 그리고 결정적으로, canonical이 걸려 있어도 봇은 일단 변형 URL을 크롤은 한다. 렌더링해서 head의 canonical을 읽어봐야 대표가 누구인지 아니까 안 들를 수가 없다. 즉 중복 색인은 어느 정도 막아도 크롤 예산 낭비 자체는 못 막는다. 그래서 canonical만으로는 부족하고, 더 근본적인 수를 반드시 함께 써야 한다.


canonical을 걸 때 흔한 실수가, 변형 URL의 canonical을 자기 자신으로 걸어버리는 것이다. 이러면 대표를 지정하는 의미가 없어져 중복이 그대로 색인된다. 정렬이나 필터가 붙은 변형 페이지의 canonical은 반드시 파라미터가 없는 정규 주소를 가리켜야 한다. 자기 참조 canonical은 오직 그 정규 주소 자신에게만 걸어야 한다.


canonical과 함께 자주 언급되는 게 robots.txt로 파라미터 URL을 막는 방법인데 이건 신중해야 한다. robots.txt로 크롤을 막으면 봇이 그 URL의 canonical조차 못 읽어서, 오히려 중복 정리가 안 되고 색인에 붕 뜬 채 남을 수 있다. 크롤을 막는 것과 색인을 정리하는 것은 다른 얘기다. 정리가 목적이라면 크롤은 열어두고 canonical이나 301로 신호를 줘야 한다.


3. 진짜 해법은 파라미터를 URL에서 빼는 것

근본 해법은 애초에 이 파라미터들을 크롤되는 URL에서 없애는 것이다. 필터, 정렬, 페이지 맥락은 콘텐츠 그 자체가 아니라 보기 상태(view state)다. 콘텐츠가 아닌 상태를 굳이 URL 경로에 노출할 이유가 없다. 이런 상태는 프래그먼트(#)로 옮기거나, 진입 시 301로 파라미터를 떼어낸 클린 URL로 보내면 된다.


특히 301 방식은 강력하다. 봇이 ?board= 같은 주소로 진입하면 HTML을 렌더링하기도 전에 Location 헤더만 읽고 즉시 클린 URL로 큐를 옮긴다. 렌더 후 canonical을 확인하는 것보다 훨씬 빠르게 중복이 정리된다. canonical이 사후에 이건 대표가 아니에요라고 해명하는 거라면, 301은 사전에 아예 딴 데로 못 가게 막아버리는 것이다. 청소 속도의 체급이 다르다. 실제로 옛 파라미터 URL들이 301 앞에서 눈에 띄게 빨리 색인에서 빠졌다.


301 strip을 구현할 때 주의할 점은, 사람과 봇을 구분하려 들지 말라는 것이다. 사용자에겐 필터가 여전히 동작해야 하니, 실제로는 필터 상태를 프래그먼트나 클라이언트 상태로 옮기고 서버로 오는 파라미터 요청만 301로 정리하는 식이 된다. 서버가 보는 것과 사용자가 쓰는 것을 분리하는 설계가 핵심이다.


301 strip과 프래그먼트 중 무엇을 쓸지는 그 상태를 URL로 공유해야 하느냐로 갈린다. 필터 결과를 링크로 공유해야 한다면 프래그먼트가 낫고, 그럴 필요가 없다면 아예 서버에서 301로 떼는 게 깔끔하다. 나는 둘을 함께 썼다. 공유가 필요한 맥락은 해시로, 봇이 옛 파라미터로 들어오는 경로는 301로 막는 식이다.


4. 꼬리 자르기(Loop Breaking)

중복 URL 문제의 핵심 기법을 나는 꼬리 자르기라고 부른다. 봇이 어쩌다 옛 파라미터 URL로 진입하더라도, 그 페이지 안의 형제 링크나 페이지네이션 링크가 전부 클린 URL을 가리키게 하고 진입 자체를 301로 끊는 것이다. 봇이 파라미터를 물고 들어와도 나갈 땐 반드시 클린 URL로 나가게 하는 구조다.


이렇게 하면 봇이 파라미터 URL에서 또 다른 파라미터 URL로 자가 복제하며 무한히 파고드는 고리가 끊긴다. 링크가 파라미터를 물고 파라미터를 낳는 연쇄를 원천 차단하는 것이다. 목표는 봇을 항상 깨끗한 동선으로 끌어내는 것이다. 게다가 이미 구글에 쌓여 있던 옛 파라미터 URL들도, 이 꼬리 자르기가 걸려 있으면 재크롤될 때마다 하나씩 클린 URL로 대체되며 시간이 지나면 자연스럽게 소멸한다. 능동적으로 지우지 않아도 알아서 빠진다.


꼬리 자르기가 잘 걸렸는지는 GSC의 대체 페이지 항목 추이로 확인할 수 있다. 파라미터 URL들이 이 항목에 잡혔다가 시간이 지나며 서서히 줄어들면 정상적으로 정리되고 있는 것이다. 반대로 계속 늘기만 하면 어딘가에서 봇이 여전히 파라미터 링크를 새로 물고 있다는 뜻이니, 그 링크의 출처를 찾아야 한다.


파라미터 문제는 페이지네이션에서 특히 까다롭다. page=2, page=3 같은 페이지 파라미터는 필터와 달리 각기 다른 글 목록을 담고 있어서 무작정 하나로 합치면 뒤쪽 글들이 발견 통로를 잃는다. 그래서 페이지네이션은 중복이 아니라 별개 콘텐츠로 취급해 크롤 가능하게 두되, 정렬이나 필터 파라미터와는 분리해서 다뤄야 한다.


5. 기능성 파라미터는 반드시 보존

주의할 점. 파라미터를 정리한다고 무작정 전부 잘라내면 안 된다. board, flair, sort처럼 순수 맥락용 파라미터만 골라서 제거하고, 실제 기능에 쓰이는 파라미터는 그대로 보존해야 한다. 청소한다고 다 밀어버리면 멀쩡한 기능이 죽는다.


그래서 알려진 맥락 파라미터만 화이트리스트로 선별해 strip 하는 게 안전하다. 전역에서 물음표 뒤를 통째로 날리는 코드는 예상 못 한 기능성 파라미터, 예컨대 인증 토큰이나 결제 콜백, 추적 파라미터까지 죽여서 사이트를 통째로 망가뜨릴 수 있다. 반드시 제거 대상 목록을 명시적으로 정하고 그 외엔 손대지 않는 원칙을 지키자. 그런데 필터 상태를 프래그먼트로 옮긴다는 얘기를 하면 꼭 따라오는 질문이 있다. 해시(#) 라우팅은 SEO에 불리한 것 아니냐는 것이다. 다음 편에서 이 뿌리 깊은 오해를 정면으로 다룬다.


정리 대상 파라미터 목록은 문서로 남겨두는 걸 권한다. board, flair, sort는 제거, 인증과 결제 관련은 보존, 이런 식으로 명시해두면 나중에 새 파라미터가 추가될 때 어느 쪽인지 판단하기 쉽다. 목록 없이 감으로 관리하면 언젠가 반드시 기능성 파라미터를 실수로 제거하는 사고가 난다. 화이트리스트는 문서로 관리하자.