SEO · SPA · 라우팅

글 159편을 써 두고도 검색에 한 편도 안 잡힌 이유

27만 자가 이미 있었다. 그런데 구글이 본 적이 없었다. 주소창의 # 하나 때문이었다.

발굴 콘텐츠를 쌓아 두는 도구를 따로 만들어 쓰고 있었습니다. 어느 날 세어 보니 159편, 27만 자가 들어 있었습니다. 사이트도 이미 떠 있었습니다. 그런데 검색에서는 한 편도 나오지 않았습니다.

원인은 단순했습니다. 그 사이트가 Vite + HashRouter로 만든 단일 페이지 앱이었고, 글 주소가 전부 이렇게 생겼습니다.

https://example.com/#/notes/42

#은 서버로 가지 않는다

URL의 해시 프래그먼트(# 뒤쪽)는 브라우저 안에서만 쓰이는 값입니다. HTTP 요청에 실려 서버로 전송되지 않습니다. 이건 SPA의 구현 디테일이 아니라 URL 규격 자체가 그렇습니다.

그래서 크롤러가 저 주소를 요청하면 서버는 # 앞부분만 받습니다. 즉 159편 전부가 서버 입장에서는 같은 하나의 URL이고, 돌려주는 것은 본문이 없는 빈 껍데기 HTML입니다. 실제 본문은 자바스크립트가 해시를 읽어 브라우저에서 그려 냅니다.

구글이 자바스크립트를 실행하기는 합니다. 하지만 그건 렌더링 대기열에 들어간 뒤의 이야기이고, 애초에 크롤러에게는 따라갈 개별 URL이 존재하지 않습니다. 링크가 없으니 발견도 없습니다. 159편은 검색엔진 관점에서 그냥 존재하지 않았습니다.

확인하는 법

브라우저에서 잘 보인다는 건 아무 근거가 못 됩니다. 자바스크립트를 실행하지 않는 요청으로 받아 봐야 합니다.

curl -s https://example.com/some/post | wc -c

여기서 나온 HTML 안에 본문 문장이 실제로 들어 있는지를 봅니다. 없으면 크롤러도 못 봅니다. 이 확인은 30초면 되는데, 저는 27만 자를 쌓는 동안 한 번도 하지 않았습니다.

고친 방법

SPA를 뜯어고치는 대신, 이미 정적 생성(SSG)을 쓰고 있던 본 사이트에 /blog를 만들고 콘텐츠를 동기화했습니다. 집필·발굴은 기존 도구에서 계속하고, 공개 정본은 정적 페이지 쪽이 되도록 역할을 나눴습니다.

여기서 뜻밖의 이점이 하나 있었습니다. 구글이 이 글들을 이전 주소에서 본 적이 자체가 없었기 때문에, 중복 콘텐츠 문제가 처음부터 없었습니다. 새 주소가 아무 다툼 없이 정본이 됐습니다. 색인이 안 되고 있었다는 사실이, 이전할 때는 오히려 유리하게 작용한 셈입니다.

정리

다른 제작 노트