3줄 요약
- 새 글의 상당수가 구글에 색인되지 않은 원인은 콘텐츠 품질이 아니라 내부링크에 붙은 ?lang=ko 파라미터였다.
- 이 파라미터가 들어간 URL 패턴이 robots.txt에서 차단돼 있어 Googlebot이 내부링크를 하나도 따라가지 못했고, 크롤 예산이 굶었다.
- 기본 언어 링크를 파라미터 없는 클린 URL로 바꿔 배포한 뒤 색인률은 57.6%(2026-07-14, 129/224)에서 89.8%(2026-08-16, 238/265)로 올랐다.
| 질문 | 답 |
|---|---|
| 색인이 안 되는 진짜 원인은 무엇이었나 | 내부링크의 ?lang=ko 파라미터가 robots.txt 차단 패턴과 겹쳤다 |
| 콘텐츠를 다시 써야 했나 | 아니다. 콘텐츠 품질 문제로 오진해 오래 헤맸다 |
| 어떻게 구분했나 | URL Inspection API의 색인 상태를 네 갈래로 분류해 "미발견"이 많은지 "크롤됨-미색인"이 많은지 봤다 |
| 무엇을 바꿔서 해결했나 | 기본 언어 링크를 파라미터 없는 클린 URL로 전환하고 배포했다 |
| 효과는 얼마나 컸나 | 색인률이 57.6%(2026-07-14)에서 89.8%(2026-08-16)로 올랐다 |
이 글대로 하면 무엇이 달라지는가 — 색인이 안 되는 원인이 콘텐츠 쪽인지 크롤 경로 쪽인지를 추측이 아니라 API 응답의 분류 분포로 가른 뒤, 크롤 경로 쪽이면 어디가 막혔는지를 명령 두 줄로 찾을 수 있다.
새 글의 대부분이 왜 구글에 색인되지 않았는가?
색인은 크롤이 선행돼야 일어나는데, 크롤할 경로 자체가 막혀 있었기 때문이다. 색인 안 된 글의 비율이 85%에 가까웠던 시점이 있었다. 발행은 정상이었고 사이트맵에도 반영돼 있었다. 문제는 콘텐츠가 아니라 Googlebot이 사이트 내부를 얼마나 돌아다닐 수 있는가였다. 아무리 좋은 글을 발행해도 크롤러가 그 페이지의 존재를 발견하지 못하면 색인 큐에 들어가지 못한다.
콘텐츠 품질과 도메인 신뢰도를 의심한 진단은 왜 빗나갔는가?
미색인 비율이 사이트 전체에 걸쳐 균일했기 때문이다. 처음에는 콘텐츠 품질이나 신생 도메인의 신뢰도 부족을 의심했고, 문장을 다듬고 이미지를 늘리고 카드형 요약을 붙이는 하류 작업에 시간을 썼다. 그런데 이런 작업을 아무리 잘 실행해도 색인률은 그대로였다.
오진 사슬을 그대로 적으면 이렇다. 틀린 진단은 "글의 품질이 낮아서 구글이 색인 가치를 낮게 봤다"였다. 이 진단이 틀린 이유는 예측이 관측과 맞지 않았기 때문이다. 품질이 원인이라면 글마다 색인 여부가 들쭉날쭉해야 하고, 특히 공들여 쓴 글은 색인돼야 한다. 실제로는 글의 품질과 무관하게 비슷한 비율로 막혀 있었다. 진짜 원인은 개별 글의 속성이 아니라 사이트 전체에 공통으로 적용되는 구조, 곧 내부링크가 생성되는 방식이었다.
여기서 쓸 수 있는 판별식은 균일성이다. 미색인이 글마다 들쭉날쭉하면 개별 글의 속성(콘텐츠·중복·얇은 페이지)을 의심하고, 사이트 전체가 비슷한 비율로 막혀 있으면 전 페이지에 공통으로 걸리는 구조(링크 생성·robots.txt·서버 렌더링)를 의심한다.
색인 상태를 어떻게 측정하고 네 갈래로 분류했는가?
Search Console의 URL Inspection API를 발행 URL 전수에 돌려 응답을 네 갈래로 나눴다. 우리 쪽 구현은 scripts/analytics/gsc_coverage.py이고, 데이터베이스에서 발행 URL을 뽑아 한 건씩 점검한 뒤 미색인 URL을 색인요청 대기열 파일로 떨어뜨린다. 이 API는 할당량이 하루 2000건이라 수백 편 규모에서는 전수 점검이 가능하다.
분류 로직은 이 부분이 전부다.
# scripts/analytics/gsc_coverage.py 발췌 — 색인 상태를 네 갈래로 나눈다
resp = s.urlInspection().index().inspect(
body={"inspectionUrl": url, "siteUrl": SITE_URL}).execute()
r = resp.get("inspectionResult", {}).get("indexStatusResult", {})
verdict = r.get("verdict", "")
cov = r.get("coverageState", "")
if verdict == "PASS" or cov == "Submitted and indexed":
indexed.append(url)
elif "crawled" in cov.lower() and "not indexed" in cov.lower():
crawled_not.append((url, cov))
elif cov:
not_indexed.append((url, cov))
else:
unknown.append((url, verdict or "?"))
이 네 갈래가 그대로 원인 판별식이 된다. 어느 칸에 많이 쌓였는지가 다음에 손댈 곳을 지정한다.
| 분류 | 뜻 | 지목하는 원인 |
|---|---|---|
| 색인됨 | 검색 결과에 오를 자격이 있다 | 없음 |
| 크롤됨-미색인 | 크롤러가 페이지를 읽었지만 색인하지 않았다 | 발견 경로는 살아 있다. 콘텐츠·중복·얇은 페이지 쪽 |
| 발견-미색인 | 존재는 알지만 아직 읽지 않았다 | 크롤 예산 또는 우선순위 |
| 미발견 | 크롤러가 URL의 존재 자체를 모른다 | 발견 경로(내부링크·사이트맵) 쪽 |
우리 경우 원인은 내부링크 차단이었으므로 문제는 마지막 두 칸, 곧 발견 경로 쪽에 있었다. 반대로 크롤됨-미색인이 대부분인 사이트라면 링크를 고쳐도 색인률이 오르지 않는다. 그쪽은 크롤은 되고 있고 색인 판단에서 떨어진 것이므로 이 글의 처방과는 다른 처방이 필요하다.
호출할 때 소켓 타임아웃을 걸어 두는 편이 좋다. 응답이 오지 않는 건이 하나 생기면 전수 점검 전체가 멈춰 서기 때문이다.
import socket
socket.setdefaulttimeout(30) # URL Inspection 호출 hang 방지
robots.txt의 lang 파라미터 차단이 크롤 예산을 어떻게 굶겼는가?
내부링크가 전부 차단된 URL 형태로 생성되고 있었기 때문에 크롤러가 링크를 하나도 따라갈 수 없었다. 사이트 내부링크에 ?lang=ko가 자동으로 붙고 있었고, 이 파라미터가 붙은 URL 패턴이 robots.txt 차단 규칙에 걸려 있었다. 링크를 못 따라가면 새 페이지를 발견할 경로가 사이트맵 제출뿐인데, 크롤러는 발견하지 못한 페이지에 크롤 예산을 배정하지 않는다. 사이트 전체가 하나의 병목, 곧 차단된 링크 패턴 때문에 굶은 것이었다.
같은 증상을 자기 사이트에서 확인하는 절차는 다음과 같다.
- 발행 URL 목록을 만든다. 데이터베이스에서 뽑는다면 이런 형태다.
SELECT b.slug AS bs, p.slug AS ps FROM posts p JOIN blogs b ON p.blog_id = b.id
WHERE p.status = 'published' ORDER BY p.blog_id, p.id;
- 표본으로 색인 상태를 먼저 본다. 전수를 돌리기 전에 20건으로 분포를 확인한다.
python -X utf8 scripts/analytics/gsc_coverage.py --blog 6 --limit 20 --no-telegram
- 분류 분포로 방향을 정한다. 미발견·발견-미색인이 대부분이면 4번으로, 크롤됨-미색인이 대부분이면 콘텐츠 쪽 점검으로 간다.
- 차단 규칙을 그대로 읽는다.
curl -s https://<도메인>/robots.txt
- 실제로 렌더된 페이지의 내부링크를 뽑아 차단 패턴과 대조한다. 브라우저 개발자 도구가 아니라 크롤러가 받는 HTML을 봐야 한다.
curl -s "https://<도메인>/<블로그>/post/<슬러그>" | grep -o 'href="[^"]*"' | sort -u | head -40
- 링크가 자바스크립트로만 만들어지고 있지 않은지 확인한다. 5번 결과에 내부링크가 아예 없다면 차단이 아니라 서버 렌더링 부재가 원인이다. 우리 쪽에서는 이 부분도 서버 렌더로 바꿨다.
- 링크 생성부를 고쳐 배포하고, 며칠 뒤 2번을 다시 돌려 같은 분포를 비교한다.
무엇을 고쳤고 색인률은 얼마나 올랐는가?
기본 언어 페이지의 내부링크를 파라미터 없는 클린 URL로 바꾸고 배포했다. robots.txt 규칙 자체는 건드리지 않았다. 그 규칙이 틀린 것이 아니라 링크 생성 쪽이 불필요한 파라미터를 붙이고 있었던 것이 문제였기 때문이다. 차단 규칙을 완화하는 방향으로 고쳤다면 파라미터가 붙은 중복 URL이 색인되기 시작해 다른 문제를 만들었을 것이다.
| 측정일 | 색인 문서 수 | 전체 문서 수 | 색인률 |
|---|---|---|---|
| 2026-07-14 | 129 | 224 | 57.6% |
| 2026-08-16 | 238 | 265 | 89.8% |
수정 후 남은 미색인 URL은 대기열 파일에 쌓아 두고 색인 요청을 따로 돌렸다. 요청 쪽 자동화(scripts/image-gen/gsc_request_all.py)에서 실제로 필요했던 것은 성공 처리보다 중단 조건이었다. 화면에 보이는 버튼만 클릭하고, 할당량 메시지가 2회 연속이면 그날은 중단하고, 버튼을 찾지 못하거나 오류가 4회 연속이면 차단을 의심해 중단한다. 처리된 URL은 대기열에서 지워 다음 날 이어서 진행한다.
# scripts/image-gen/gsc_request_all.py 발췌 — 조기중단 조건
if quota_streak >= 2:
log("!! STOP: 할당량 도달(2연속) — 나머지는 내일"); break
if err_streak >= 4:
log("!! STOP: 4연속 실패(차단 의심)"); break
이 진단이 통하지 않는 조건은 무엇인가?
색인이 안 되는 원인이 크롤 경로가 아닐 때는 이 절차가 아무것도 해결하지 못한다. 분류 결과가 크롤됨-미색인에 몰려 있다면 크롤러는 이미 페이지를 읽고 있는 것이고, 링크를 고쳐도 숫자는 움직이지 않는다. 페이지 수가 수십 편 규모로 작은 사이트라면 사이트맵만으로도 발견이 되므로 같은 차단이 있어도 증상이 이 정도로 크게 나타나지 않을 수 있다. 색인 요청도 색인을 보장하지 않는다. 요청은 우선순위 큐에 넣어 달라는 신호일 뿐이고 최종 판단은 검색엔진이 한다. 우리 환경에서는 하루에 10건 남짓 요청한 뒤 할당량 메시지가 떴고, 나머지는 다음 날로 넘겼다. 그리고 수치가 반영되기까지 시간이 걸린다. 위 표의 두 측정 사이에는 한 달이 있다.
이 수치가 나오기까지 콘텐츠 품질 쪽 개선을 먼저 시도했었다. 진단 습관 자체를 되돌아본 계기는 발행글 전체를 신선한 컨텍스트로 다시 감사했을 때였고, 그 결과는 AI 두 기에게 발행글 12편을 감사시킨 결과에 정리돼 있다. 색인이 회복된 뒤에는 병목이 다음 단계로 옮겨갔는데, 그 과정은 병목이 클릭률로 옮겨간 사례에서 따로 다룬다.
자주 묻는 질문
Q. 왜 처음엔 콘텐츠 품질을 의심했나?
A. 미색인 비율이 워낙 커서 콘텐츠나 도메인 신뢰도 같은 익숙한 원인부터 점검했기 때문이다. 실제로는 크롤 자체가 막혀 있어 콘텐츠 개선이 색인률에 영향을 주지 못했다.
Q. 콘텐츠 문제와 크롤 경로 문제를 어떻게 구분하는가?
A. URL Inspection API 응답을 색인됨·크롤됨-미색인·발견-미색인·미발견으로 나눠 분포를 본다. 미발견과 발견-미색인이 많으면 링크나 사이트맵 같은 발견 경로 문제이고, 크롤됨-미색인이 많으면 콘텐츠·중복 쪽 문제다.
Q. 크롤 예산이 굶는다는 것은 무슨 뜻인가?
A. 크롤러가 새 페이지를 발견하는 주된 경로는 이미 아는 페이지의 내부링크다. 그 경로가 막히면 발견 자체가 안 되고, 발견되지 않은 페이지에는 크롤 자원이 배정되지 않는다.
Q. robots.txt 규칙을 완화해서 고쳤나?
A. 아니다. 규칙은 그대로 두고 내부링크가 파라미터 없는 클린 URL을 생성하도록 바꿨다. 규칙을 푸는 방향은 파라미터가 붙은 중복 URL을 색인시킬 위험이 있다.
Q. 이 원인은 다국어 사이트 전반에 해당하는가?
A. 우리 환경에서 실측한 사례다. 언어 파라미터를 URL에 노출하는 구조를 쓰고 있고 robots.txt에 관련 차단 규칙이 있다면, 렌더된 HTML의 링크를 뽑아 차단 패턴과 대조해 보는 것으로 같은 점검을 할 수 있다.
Comments
Comments (0)
Leave a Comment