크롤 버짓으로 다시 읽는 SEO — 웹 크롤러를 설계해보면 보이는 것들

· 시스템 설계

SEO 체크리스트는 어디에나 있다. “TTFB를 줄여라”, “canonical을 달아라”, “sitemap을 제출해라”. 문제는 이 목록들이 를 말해주지 않는다는 것이다. 왜 canonical이 페이지 속도보다 먼저인가? 왜 timestamp만 갱신한 글은 오히려 손해인가?

시스템 설계 스터디에서 웹 크롤러 설계 챕터를 공부하다가 관점이 뒤집혔다. SEO는 크롤러라는 분산 시스템의 자원 배분 정책을 상대로 하는 최적화 문제다. 크롤러의 내부 구조를 알면 체크리스트의 각 항목이 크롤러의 어느 컴포넌트를 겨냥하는지 보이고, 우선순위가 저절로 정해진다.

이 글은 그 관점 전환을 정리한 것이다. 마침 이 블로그를 직접 만들면서 여기서 도출한 기준을 그대로 적용했기 때문에, 마지막 절에서 실제 구현하며 내린 결정들도 함께 다룬다.

크롤러는 우리 사이트에 무한정 머물지 않는다

먼저 크롤러 쪽 사정부터 보자. 검색엔진 크롤러는 월 10억 페이지 규모를 수집한다고 가정하면 초당 약 400페이지를 처리해야 한다. 이 처리량을 유지하려면 크롤러는 한 사이트(host)에 오래 머물 수 없다. 특정 host에 스레드가 묶이면 전체 throughput이 떨어지고, 거대 사이트의 URL이 큐를 점령하면 다른 사이트의 수집 주기가 밀린다.

그래서 크롤러는 host마다 크롤 버짓(crawl budget) — 1회 방문에 소비할 자원의 상한 — 을 둔다. 버짓은 단일 차원이 아니라 복합 조건이다.

방문 종료 조건: (pages ≥ N) OR (seconds ≥ T) OR (bytes ≥ B)

시간만 재면 빠른 서버에 과부하를 주고, 페이지 수만 세면 무거운 파일에 대역폭이 폭주하고, 바이트만 재면 작은 페이지 수천 개가 커넥션을 점유한다. 셋 중 먼저 닿는 조건이 방문을 끝낸다.

버짓의 크기도 고정이 아니다. 사이트마다 다르게 산정된다.

budget(host) = base × priority_weight × adaptive_multiplier

priority_weight     = log(PageRank + 1)        # 중요한 사이트에 자원 비례 배분
adaptive_multiplier = 직전 방문의 신규 콘텐츠 비율에 따라
                      1.5 (> 50%) / 1.0 (10~50%) / 0.5 (< 10%)

두 가지가 눈에 들어온다. 첫째, PageRank가 log 스케일로 들어가므로 백링크는 버짓 총량 자체를 키우는 유일한 항목이지만, 효과는 완만하다. 둘째, adaptive_multiplier는 순전히 우리 손에 달려 있다. 방문할 때마다 새 콘텐츠를 보여주는 사이트는 버짓이 1.5배로 늘고, 매번 같은 페이지만 보여주는 사이트는 절반으로 깎인다.

버짓이 소진되면 남은 URL은 버려지는 게 아니라 deferred 큐로 넘어가 다음 방문을 기다리는데, 이때 우선순위가 한 단계 강등된다. 단, 신규 콘텐츠 비율이 높은 host는 강등이 면제된다. 즉 콘텐츠 갱신 실적은 이번 방문뿐 아니라 다음 방문의 대우까지 결정한다.

크롤러 파이프라인: 내 페이지가 통과해야 하는 관문들

버짓 안에서 크롤된다고 끝이 아니다. 페이지는 크롤러 내부 파이프라인의 여러 관문을 통과해야 인덱스에 도달한다.

중복

신규

신규

URL Frontier

우선순위 큐

HTML Downloader

Content Parser

Content Seen?

폐기

Link Extractor

URL Filter

URL Seen?

각 관문이 SEO에 갖는 의미를 정리하면 이렇다.

컴포넌트동작SEO 함의
URL Frontier우선순위 큐. priority 낮으면 늦게/적게 크롤내부 링크·백링크가 priority를 올린다
Politenesshost별 요청 간 delay응답이 느리면 같은 시간에 수집량 감소
Content Seen?콘텐츠 hash 비교, 중복이면 폐기웹 전체 페이지의 약 29%가 중복으로 버려진다
URL Seen?Bloom filter로 방문 이력 관리URL 변형이 많으면 같은 페이지가 여러 번 크롤된다
Freshness 정책update history 기반 재방문안 바뀌는 페이지는 재방문 주기가 늘어난다
robots.txtDisallow 경로 미수집잘 쓰면 버짓 절약, 잘못 쓰면 영구 미노출

여기서 중요한 통계 하나. 크롤러가 가져간 페이지의 약 29%는 중복 판정으로 폐기된다. 크롤은 됐는데 인덱스에 없다면, 내 페이지가 그 29%에 들어갔을 가능성부터 의심해야 한다.

버짓 관점에서 다시 쓰는 SEO 작업 목록

이제 SEO 작업들을 “크롤러의 어떤 메커니즘을 겨냥하는가”로 재분류할 수 있다.

1. 버짓 낭비 회수 — 중복 제거

같은 콘텐츠가 여러 URL로 fetch되면 버짓이 그만큼 증발한다. 그래서 비용 대비 효과가 가장 크다.

  • <link rel="canonical">로 정본 URL을 통일한다. mirror, print view, 파라미터 변형이 Content Seen? 단계에서 정확히 dedup된다.
  • utm_*, session_id 같은 tracking 파라미터를 canonical에서 제거한다. 세션 ID가 URL에 붙으면 URL Seen?의 Bloom filter가 무력화되어 같은 페이지가 반복 크롤된다.
  • trailing slash, http/https, www 표기를 일관되게 유지한다.
  • soft 404를 없앤다. 빈 페이지가 200으로 응답하면 서로 hash가 같은 중복 페이지를 양산하며 버짓을 태운다.

2. 발견 경로 확보 — Frontier에 제대로 진입시키기

크롤러는 기본적으로 BFS로 링크를 따라온다. 발견되지 않는 페이지는 존재하지 않는 페이지다.

  • sitemap.xml을 제출하면 크롤러가 BFS를 거치지 않고 URL을 직접 큐에 넣는다. <lastmod>를 정확히 기재하면 freshness 판정 재료가 되어 재크롤 우선순위에 반영된다.
  • 홈에서 3클릭 이내에 모든 중요 페이지가 도달 가능하도록 구조를 얕게 유지한다. BFS에서 depth가 깊은 페이지는 늦게 발견된다.
  • 어디서도 링크되지 않는 orphan page를 없앤다. 내부 링크가 많이 향하는 페이지는 Link Extractor에 자주 발견되어 priority가 자연히 오른다.

3. 페이지당 비용 절감 — 같은 버짓에 더 많은 페이지

버짓이 min(N pages, T seconds, B bytes)라면, 페이지당 시간과 바이트를 줄이는 만큼 크롤되는 페이지 수가 늘어난다.

  • TTFB를 줄인다(CDN, edge cache). 응답이 빠를수록 같은 T 안에 더 많은 fetch가 이뤄진다.
  • HTML을 압축하고 크기를 줄인다. 평균 500KB 페이지를 200KB로 줄이면 같은 B 상한에서 2.5배의 페이지가 수집된다.
  • Last-Modified/ETag를 제공해 변경 없는 페이지는 304로 응답한다. 크롤러의 대역폭을 아껴주는 사이트는 우호적으로 취급된다.
  • robots.txt로 admin·검색결과·필터 조합 페이지를 Disallow해서 N 낭비를 막는다. 특히 faceted navigation이 만드는 무한 URL 조합은 spider trap으로 분류되기 전에 직접 차단하는 편이 낫다.

4. 신선도 신호 — adaptive_multiplier 1.5 받기

  • 실질적인 콘텐츠 업데이트 주기를 유지한다. 직전 방문에서 신규 콘텐츠 비율이 50%를 넘으면 다음 버짓이 1.5배가 된다.
  • 반대로 timestamp만 갱신하고 본문이 동일하면 역효과다. hash가 같으므로 Content Seen?에서 폐기되고, 신규 콘텐츠 비율은 오히려 떨어져 multiplier가 깎인다.

5. 렌더링 — 크롤러가 읽을 수 있는 HTML

  • 초기 HTML에 콘텐츠와 링크가 다 들어 있어야 한다. CSR로만 그리는 사이트는 Link Extractor가 빈 페이지를 받아 발견 자체가 실패한다. SSR/SSG가 답이고, 불가하면 봇에게만 렌더 결과를 주는 dynamic rendering이 차선이다.
  • lazy-load 이미지는 SSR로 src를 주입하거나 <noscript> fallback을 둔다.

6. 서버 신뢰도 — 재방문 주기를 지키는 조건

크롤러는 느리거나 응답 없는 서버에 짧은 timeout을 걸고 재방문을 미룬다.

  • 5xx 에러율을 1% 아래로 유지한다.
  • redirect chain은 1 hop으로 끝낸다. 리다이렉트 하나하나가 fetch 비용, 즉 버짓이다. 영구 이전은 301로 명확히 한다.
  • HTTPS와 유효한 인증서, 안정적인 DNS 응답. DNS가 응답하지 않으면 크롤러 스레드가 블록되고, 그 host의 다음 방문 우선순위가 내려간다.

우선순위: ROI로 줄 세우기

메커니즘을 알면 우선순위 논쟁이 짧아진다. 효과가 즉시 나타나는지, 비용이 얼마인지로 정렬하면 이렇다.

순위작업왜 이 순서인가비용
1canonical + 파라미터 정규화중복으로 새던 버짓이 즉시 회수됨낮음
2sitemap.xml + lastmod발견과 freshness 신호를 동시에 해결낮음
3TTFB·페이지 크기 최적화같은 버짓에 수집량이 비례 증가중간
4SSR 도입CSR-only라면 발견 자체가 안 되므로 필수높음
5내부 링크 구조 개선priority가 자연 상승중간
6robots.txt trap 차단버짓 낭비 방지낮음
7실질 콘텐츠 갱신 주기화multiplier 유지, 단 지속 비용높음
8백링크 확보버짓 총량을 키우는 유일한 수단이나 log 스케일매우 높음

거꾸로, 피해야 할 것들도 메커니즘으로 설명된다. 무한 스크롤로만 콘텐츠를 노출하면 Link Extractor가 다음 페이지를 못 본다(pagination URL 병행 필수). JS로만 동작하는 navigation은 링크 추출에 실패한다. 봇과 사용자에게 다른 콘텐츠를 주는 cloaking은 적발 시 PageRank penalty로 이어지는데, 위 산식에서 봤듯 PageRank 하락은 버짓 총량의 영구 손실이다.

이 블로그에 실제로 적용한 것들

이 기준을 세운 직후에 이 블로그를 Astro로 새로 만들었다. 이론을 수용 기준 삼아 구현하면서 내린 결정들이다.

sitemap을 직접 생성했다. @astrojs/sitemap 패키지를 설치해놓고도 쓰지 않았는데, 기본 출력에 <lastmod>가 없기 때문이다. lastmod는 위에서 본 freshness 판정의 핵심 재료라 포기할 수 없었다. 직접 만들면서 한 가지 원칙을 지켰다. lastmod에는 빌드 시각이 아니라 콘텐츠의 수정일(updatedDate ?? pubDate)을 넣는다. 빌드 시각을 넣으면 배포할 때마다 전체 사이트가 갱신된 것처럼 보이는데, 이는 “timestamp만 갱신하고 본문은 동일”한 상황의 사이트 전체 버전이다. 크롤러가 와서 확인해보면 매번 hash가 같으니, lastmod 신호 자체를 불신하게 된다.

빈 시리즈 페이지를 생성하지 않도록 고쳤다. 구현 중에 글이 0편인 시리즈 페이지가 만들어지고 목록에서 링크까지 되는데 sitemap에는 빠지는 불일치를 발견했다. 내용 없는 페이지가 200으로 응답하는 것은 전형적인 thin content이자 soft 404 후보다. 위 1번 항목 그대로, 중복 hash를 양산하며 버짓을 태우는 페이지다. 아예 생성하지 않도록 수정해 빌드 산출물이 9페이지에서 6페이지로 줄었다. 페이지 수가 줄었는데 SEO에는 이득인, 버짓 관점이 아니면 반직관적인 결정이다.

canonical은 정규화해서 출력한다. 쿼리스트링·해시·trailing slash를 제거한 URL을 canonical로 내보낸다. 글 URL 자체도 평면 /posts/{slug} 구조로 고정해, 글이 시리즈를 옮겨도 URL이 변하지 않게 했다. 리다이렉트가 생길 일 자체를 없애는 쪽이 redirect chain을 관리하는 것보다 싸다.

페이지 무게는 프레임워크 선택으로 해결했다. 정적 빌드(SSG)라 렌더링 관문은 통과가 자명하고, 기본 JS 0KB에 웹폰트를 쓰지 않아 폰트 로딩도 0이다. 다이어그램도 클라이언트 라이브러리 대신 빌드 타임에 인라인 SVG로 렌더링한다. 뒤집어 말하면, 나중에 웹폰트나 클라이언트 JS를 도입하는 시점이 곧 성능 예산을 깎아 쓰기 시작하는 시점이라는 것도 명확해졌다.

정리

SEO를 “검색엔진 마음에 들기”로 접근하면 체크리스트는 끝이 없고 우선순위는 유행을 탄다. 크롤러를 자원이 유한한 분산 시스템으로 놓고 보면 문제가 구체화된다. 한정된 host 버짓 안에서 (1) 중요한 페이지를 먼저 크롤시키고, (2) 재방문을 자주 받고, (3) 가져간 페이지가 인덱스까지 살아남게 하는 것. 모든 SEO 작업은 이 세 목표 중 하나에 복무하고, 어디에도 복무하지 않는 작업은 걸러도 된다.

체크리스트는 잊어도 모델은 남는다. 다음에 SEO 관련 결정을 내릴 일이 생기면 “크롤러의 어느 컴포넌트가 이걸 보는가”부터 물으면 된다.

학습 교재: System Design Interview - An Insider’s Guide (Alex Xu)
본문에 포함된 도표는 위 교재에서 인용한 것입니다.