배포와 인프라12 분 읽기

서치콘솔이 404 메일을 퍼부었다. 로그를 보니 범인은 내 코드가 아니었다

구글 서치콘솔이 'Not found (404)' 메일을 수십 통 쏟아냈습니다. 사이트를 고치려다, 대시보드 대신 서버 로그를 봤습니다. 사이트맵 149개 전부 멀쩡, 내부 링크도 멀쩡, 검색봇이 실제로 404를 낸 URL은 전 로그 통틀어 딱 하나였습니다. 나머지는 전부 외부 유령이었습니다.

#first-principles#reality-check#seo#infra
개념 도식: 왼쪽은 404 경보 메일이 쌓인 서치콘솔 받은편지함, 오른쪽은 검색봇 404가 app-ads.txt 하나뿐이고 사이트맵·내부링크 404가 0인 서버 로그
대시보드는 경보를 퍼붓고, 서버 로그는 사이트가 멀쩡하다고 말했다.

받은편지함에 구글 서치콘솔 메일이 쌓였습니다. 전부 같은 제목이었습니다. "New reason preventing your pages from being indexed — Not found (404)." 한 통도 아니고 계속 쌓였습니다. 메일마다 상냥하게 권합니다. "고치면 색인되고 검색에 노출됩니다."

본능적으로 손이 코드로 갑니다. 어디 링크가 깨졌지? 사이트맵이 잘못됐나? 배포가 빠졌나?

그런데 그 전에 한 문장을 강제로 걸었습니다. 당신의 SEO 대시보드는 진짜 문제를 가리킵니까, 아니면 노이즈를 증폭합니까? 서치콘솔이 "404"라고 소리치는 것과, 내 사이트가 실제로 방문자·검색봇에게 404를 내는 것은 같은 사실이 아닙니다. 하나는 구글의 90일 누적 보고서고, 하나는 지금 서버에서 벌어지는 일입니다.

그래서 대시보드를 닫고 ground-truth를 봤습니다. 서버 access 로그입니다.

대시보드 대신 로그

먼저 자기유발 404부터 배제했습니다. 이건 내가 고칠 수 있는 유일한 부류입니다 — 내 사이트가 링크하는데 없는 페이지. 나머지(외부에서 걸린 유령 URL)는 내 코드로는 손댈 수 없습니다.

서버에 직접 붙어(Cloudflare 챌린지를 우회하지 않고 배포된 정본 파일을 그대로) 전수 대조했습니다.

  • 사이트맵의 URL 149개 전부 실제 파일로 해소됐습니다. 누락 0.
  • 배포된 HTML 460개의 내부 링크를 전부 훑었습니다. 깨진 내부 링크 0.
  • 확장자 없는 라우팅(/foo/foo.html)도 정상. canonical이 가리키는 URL도 전부 200.

여기서 이미 답이 반쯤 나왔습니다. 구조적으로 내가 만든 404는 없었습니다. 그럼 서치콘솔은 대체 뭘 보고 소리치는 걸까요.

검색봇이 진짜로 404를 낸 URL

로그에서 검색엔진 봇(Googlebot·Bingbot)이 실제로 404를 받은 요청만 뽑았습니다. 서브도메인 로그까지 전부 합쳐서요.

grep -hiE 'googlebot|bingbot' access-logs/* | grep '" 404 '

전 로그를 통틀어 결과는 딱 한 줄이었습니다.

1  /app-ads.txt

그게 전부였습니다. app-ads.txt는 구글 광고 크롤러가 모바일 앱 광고 인증 파일이 있나 루트를 한 번 찔러본 것입니다. 저는 모바일 앱이 없고 웹 광고만 씁니다. 그 파일은 필요 없고, 404가 오히려 "인증된 앱 판매자 없음"의 올바른 신호입니다. 게다가 이건 색인 대상 "페이지"가 아니라서 서치콘솔의 페이지 색인 보고서에도 안 뜹니다.

그럼 나머지 404들, 서치콘솔을 시끄럽게 만든 그 URL들은 다 어디서 왔을까요. 로그에 전부 있었습니다.

  • /~sanpta5/apple_cz_2020/..., /wp/signinebay/... — 애플·이베이 피싱킷과 워드프레스 익스플로잇을 찾는 취약점 스캐너. 한 오래된 로그에만 5,980건.
  • /@fs/..%2f..%2froot/.env, /proc/self/environ시크릿을 훔치려는 스캐너.
  • /.git/config — 노출된 깃 저장소를 찾는 스캐너.
  • /mindshift/null 류 — 페이스북 크롤러(meta-externalagent)가 빈 값을 경로에 붙여 요청하는 자체 quirk. 페이지엔 리터럴 null 링크가 없습니다.
  • /th1s_1s_a_4o4.html — 업타임 모니터가 404 처리가 되는지 일부러 없는 경로를 찔러본 것.

전부 외부에서 만든 유령 URL, 스캐너, 봇 quirk였습니다. 검색봇이 실제 콘텐츠 URL에 404를 낸 사례는 전 속성·전 로그 통틀어 0이었습니다.

값싼 수정이 있는 척하는 라벨

서치콘솔 메일의 "고치면 색인됩니다"는 값싼 수정이 있는 척합니다. 마치 어딘가 오타 하나 고치면 경보가 멈출 것처럼요. 하지만 없는 페이지에 대한 404는 구글 공식 가이드상으로도 정답입니다. 존재하지 않는 URL은 404를 내야 맞고, 시간이 지나면 보고서에서 알아서 빠집니다. 고칠 게 아니라 무시할 것입니다.

전에도 비슷한 함정을 겪었습니다. 고장 나 보이는 지표를 추적했더니 안 고치는 게 정답이었던 이야기, 그리고 대시보드가 초록 불인데 사실은 거짓말을 하고 있던 이야기입니다. 반대로 진짜 404 버그(죽은 다국어 링크)였던 경우도 있었습니다 — 그래서 매번 "이건 노이즈다"라고 단정하지 않고 로그로 확인하는 겁니다. 이번엔 노이즈였습니다.

그런데 진짜 병목은 따로 있었다

여기서 반전이 하나 더 있었습니다. "그럼 서치콘솔이 보고한 그 목록을 정확히 뽑아서 하나씩 검증하자"고 하는 순간 막혔습니다.

  • 서치콘솔의 404 목록은 API로 안 나옵니다. 검색 분석 API에도 없고, URL 검사 API는 후보 URL을 이미 알아야 씁니다. 그 목록은 웹 UI 안에만 있습니다.
  • 그리고 제 서버 access 로그는 13시간치만 보관됩니다. 아카이브가 없습니다.

즉, 방금 "검색봇 404는 app-ads.txt 하나뿐"이라고 확인했지만, 내일 다시 확인하려면 그 로그는 이미 사라지고 없습니다. UI를 안 열면 다시 볼 방법이 없는 겁니다. 진짜 병목은 404 자체가 아니라, 404의 ground-truth를 내가 지속적으로 못 본다는 것이었습니다.

여기서 당신이라면 어떻게 하시겠습니까. 매번 서치콘솔 UI를 열어 목록을 손으로 훑겠습니까, 아니면 ground-truth를 스스로 기록하게 만들겠습니까?

로그가 스스로 기억하게

저는 후자를 택했습니다. 값싼 방법으로요. 로그가 사라지기 전에 검색봇 404만 뽑아 영구 파일에 누적하는 크론입니다.

#!/usr/bin/bash
set -euo pipefail
OUT="$HOME/gsc-404-watch"; mkdir -p "$OUT"
STORE="$OUT/bot404.log"; TMP="$(mktemp)"
{ [ -f "$STORE" ] && cat "$STORE"
  grep -hiE 'googlebot|bingbot' "$HOME"/access-logs/* 2>/dev/null | grep -E '" 404 ' || true
} | sort -u > "$TMP"        # 겹치는 로그 창은 전체 라인 sort -u로 접힘
mv "$TMP" "$STORE"
awk -F'"' '{split($2,r," "); print r[2]}' "$STORE" | sort | uniq -c | sort -rn > "$OUT/summary.txt"

0 */6 * * * — 6시간마다. 로그가 13시간 남으니 6시간 간격이면 창이 겹쳐 빠지는 게 없습니다. 이제 UI를 안 열어도 cat ~/gsc-404-watch/summary.txt 한 번이면 검색봇이 실제로 404 낸 URL 누적 목록이 나옵니다. 지금은 한 줄, app-ads.txt뿐입니다. 진짜 404가 하나라도 생기면 여기 즉시 뜹니다.

프레임워크도, 대시보드도 아닙니다. grep 두 개와 크론 한 줄입니다. 이 정도가 딱 맞습니다.

자가진단 체크리스트

당신의 사이트에 404 경보가 쌓이고 있다면, 코드를 열기 전에 3개만 확인해 보세요.

  1. 참조 페이지(Referring page)를 봤습니까? 서치콘솔의 각 404에는 "누가 이 URL을 링크했나"가 있습니다. 참조가 외부거나 없으면, 내 코드로 고칠 수 없는 유령입니다.
  2. 사이트맵 URL이 전부 실제로 200입니까? 사이트맵이 없는 페이지를 가리키는 게 유일하게 100% 내 잘못인 404입니다. 이것부터 대조하세요.
  3. 당신의 ground-truth는 얼마나 오래 남습니까? 서버 로그 보관 기간을 아십니까? 모른다면, 그게 진짜 사각지대입니다.

솔직한 부분

이번 조사에서 코드는 한 줄도 안 고쳤습니다. 고칠 게 없었으니까요. 산출물은 "사이트는 깨끗하다"는 확인 하나와, 로그를 스스로 기억하게 만든 크론 한 줄이 전부입니다. 그리고 정직하게 말하면, 서치콘솔의 90일 목록을 자동으로 뽑는 건 결국 못 했습니다 — 그건 UI에만 있고, 그 부분은 사람이 5분 열어보는 게 맞습니다.

교훈은 단순합니다. 경보를 퍼붓는 대시보드와, 지금 실제로 벌어지는 일은 다른 사실입니다. 후자는 거의 항상 로그에 있습니다. 대시보드가 소리칠 때, 코드를 열기 전에 로그부터 여세요.

당신의 사이트도 지금 딱 한 줄 grep 해보세요. grep googlebot access-log | grep ' 404 '. 검색봇이 진짜로 뭘 못 찾고 있는지, 대시보드 말고 로그에게 물어보세요.

관련 글