색인 가치가 없다고 막은 경로가 유입 1위였다
옆 서비스에서 크롤러를 막은 김에 같은 작업을 했다. JSON이라 색인 가치가 없다며 /api/ 를 막았는데, 네이버에 제출한 RSS 주소가 그 아래였고 네이버가 비-direct 유입 1위였다. 홈 본문도 거기서 온다. 42분 뒤에 철회했다
공시 요약 서비스에서 서버 요청의 75%를 먹던 봇 하나를 막은 직후였다. 의정활동 트래커(의정보기)도 같은 상태일 게 뻔해서 같은 밤에 이어서 봇 차단 규칙을 손봤다. 이 사이트의 robots.txt는 정적 파일이 아니라 코드로 만들어지기 때문에 규칙 하나를 더하는 것도 코드 리뷰를 탄다. 올라온 규칙 중에는 색인할 내용이 없다고 판단해 통째로 막기로 한 경로가 하나 섞여 있었다.
하려던 것
같은 밤이었다. 공시 요약 서비스에서 백링크 도구 봇 하나가 서버 요청의 75%를 먹고 있던 걸 찾아 막은 직후였다. 의정보기도 같은 상태일 게 뻔했다.
사용자 유입에 도움이 안되는 크롤러들 차단하는 작업해줘 dartbrief-web 의 메모리 사용량과 네트워크 사용량이 높은 이유가 그거였음
안건을 받아 보고 짧게 답했다.
추천안으로 진행해
어떻게 시켰나
옆 서비스에서 이미 기준을 세워 둔 뒤였다. 사람이 따라올 경로를 만드는 봇은 남기고, 긁기만 하는 봇은 막는다. 그 기준을 그대로 적용하고, 이번에는 URL 패턴도 같이 정리하게 했다.
이 사이트의 robots.txt는 정적 파일이 아니라 코드다. frontend/src/app/robots.ts가 규칙을 만들고 빌드 때 파일로 떨어진다. 그러니 규칙 추가도 코드 리뷰를 탄다.
작업 중에 하나를 되짚었다. 옆 서비스에서 멈춰 둔 PR이 하나 있었는데, 그게 메모리 누수와 무관하다는 게 이번 조사로 드러난 상태였다.
249 부터 저거 머지 안해도 되는거 아냐? 저게 메모리 릭을 유발하는게 아니라고 확정된거아님?
그리고 유입 얘기가 나온 김에 하나 더 물었다.
음 ai 에서 유입이 늘었다는게 사용자가 직접 거기서 들어왔단거임?
이 질문이 나중에 도움이 됐다. 지표에 뜨는 숫자가 사람의 방문인지 봇의 요청인지를 구분해서 보는 습관이 그날 밤 내내 필요했다.
어디서 막혔나
올라온 규칙에 /api/ 차단이 들어 있었다. 이유는 그럴듯했다. 그 경로는 백엔드 JSON을 그대로 돌려주는 프록시일 뿐이라 색인할 내용이 없다. 근거도 붙어 있었다. 서치 콘솔 28일치에서 /api/ 아래 URL의 노출이 0건이다. 색인이 아예 안 되고 있으니 막아도 잃을 게 없어 보였다.
리뷰가 이걸 P1으로 잡았다. 두 가지를 동시에 죽인다는 것이었다.
첫째, 네이버에 제출한 RSS 주소가 그 아래였다. /api/feed/bills.xml이다. 아흐레 전에 서치어드바이저에 제출했고, 홈 문서의 헤더에도 대체 링크로 선언돼 있다. 그리고 네이버는 이 사이트의 비-direct 유입 1위다. 28일 기준 70세션 중 49세션이 거기서 온다. 1위 채널이 물고 있는 피드를 막을 뻔했다.
둘째, 홈 본문이 그 경로에서 온다. 목록과 주간 통계와 최근 통과 법안이 전부 클라이언트 컴포넌트라 브라우저가 /api/를 부른다. 여기서 헷갈리기 쉬운데, robots.txt는 브라우저를 막지 않는다. 사람이 보는 화면은 멀쩡하다. 문제는 자바스크립트를 실행하는 크롤러다. 그쪽이 /api/를 못 받으면 빈 홈을 본다.
실측이 이걸 못 박았다. 운영 홈의 초기 HTML이 33KB인데, 그 안에 법안 상세로 가는 링크가 0개다. 크롤러가 법안 페이지를 발견하는 경로가 전부 그 fetch를 거친다는 뜻이다. 색인을 도우려고 넣은 규칙이 색인을 끊는 규칙이었다.
어떻게 풀었나
철회했다. 규칙을 넣은 커밋이 21시 30분, 되돌린 커밋이 22시 12분이다. 42분 만이었고 배포 전이었다.
되돌리면서 세 가지를 같이 남겼다.
첫째, 판단이 틀린 지점을 적었다. 원시 JSON이 색인된 적은 한 번도 없다. 서치 콘솔 28일에 노출 0건이다. 즉 이건 없는 문제를 막으려다 있는 채널을 끊는 규칙이었다. 나중에 정말 JSON 색인이 문제가 되면 robots가 아니라 응답 헤더로 막으면 된다. 그러면 크롤은 되고 색인만 안 된다. 막는 층이 다르다.
둘째, 이 사이트의 성질로 일반화해서 문서에 넣었다. 초기 HTML이 얇고 본문을 클라이언트가 채우는 사이트에서 /api/ 차단은 JSON을 감추는 일이 아니라 본문을 감추는 일이 된다. 그러니 robots에 경로를 추가하기 전에 그 경로가 렌더에 필요한 서브리소스인지부터 본다.
셋째, 회귀 테스트를 붙였다. /api로 시작하는 금지 규칙이 없어야 한다는 단언이다. 왜 없어야 하는지를 테스트 이름에 적어 뒀다. 네이버 RSS와 홈 본문 fetch가 거기로 간다고. 이런 건 반년쯤 지나면 아무도 기억 못 하고, 그때 누가 다시 “JSON인데 왜 열어 뒀지” 하면서 막는다.
같은 작업에서 하나 더 나왔다. Cloudflare에서 봇을 걸러 주던 설정이 엿새 전 Railway 이관 이후로 무효가 돼 있었다. 앞단을 걷어내면 그 앞단이 해 주던 일도 같이 사라지는데, 이관 체크리스트에 그게 없었다. 크롤러 차단을 robots 쪽으로 다시 모으면서 같이 정리했다.
배운 점
- robots에 경로를 넣기 전에 그 경로가 렌더에 필요한지 본다. 초기 HTML이 얇은 사이트에서 데이터 경로를 막는 건 본문을 막는 것이다.
- robots.txt는 브라우저를 막지 않는다. 사람 화면이 멀쩡한 걸 보고 안심하면, 자바스크립트를 실행하는 크롤러가 빈 페이지를 보고 있는 걸 못 본다.
- 막는 층을 고른다. 색인만 막고 싶으면 응답 헤더로 막는다. robots로 막으면 크롤 자체가 끊겨서 피드까지 죽는다.
- “노출 0건”은 막아도 된다는 뜻이 아니다. 색인이 안 되는 경로가 유입 1위 채널의 피드일 수 있다.
- 되돌린 규칙에는 이유를 적은 테스트를 남긴다. 규칙이 왜 없는지를 적어 두지 않으면 다음 사람이 같은 이유로 다시 넣는다.
- 앞단을 걷어낼 때는 앞단이 해 주던 일의 목록을 만든다. 이관 엿새 만에 봇 차단이 조용히 꺼져 있었다.