구글 서치 콘솔에 「발견됨 – 현재 색인이 생성되지 않음」이 쌓이면 대개 이렇게 해석합니다. "크롤은 됐으니 기다리면 되겠지." 저희 사이트는 그 상태가 79건이었고, 4주 노출은 두 자릿수에 머물렀습니다. 기다려도 줄지 않았습니다.
정정 (2026-08-09). 이 글을 발행한 다음 날, 판독 스크립트가 "총 노출·총 클릭"을 검색어 차원 상위 20행의 합으로 계산하고 있었다는 것을 발견했습니다. 구글은 희소 검색어를 익명화해 그 차원에서 빼기 때문에 실제보다 적게 나옵니다. 차원 없이 다시 물으니 노출은 41이 아니라 52였고, 클릭도 0이 아니라 1이었습니다. 원래 문장("4주 노출 41에 클릭 0")을 위와 같이 고쳤습니다. 아래 9번 항목에 이 건을 추가했습니다.
2026년 8월 8일, 멀티에이전트 12기를 붙여 저희가 운영하는 사이트 5개를 전수 진단했습니다. 6개 축을 각각 진단하고, 그 결과를 다시 반증 전담 에이전트가 파일을 직접 열어 검증하는 구조였습니다. 발견 36건 중 23건이 확정됐고, 검증자가 진단자의 산술 오류 2건을 잡아냈습니다.
이 글은 그 결과입니다. 수치는 전부 실측이고, 저희에게 불리한 것도 그대로 적습니다.
가장 먼저 뒤집힌 것: 분량을 바이트로 재고 있었다
인계 문서에 이렇게 적혀 있었습니다 — "콘텐츠 112편, 얇지 않음(용어 평균 3.4KB·가이드 평균 7.1KB)". 그래서 "분량은 문제가 아니고 구조가 문제"라는 전제로 몇 주를 보냈습니다.
한국어는 UTF-8에서 글자당 3바이트입니다. 프론트매터·마크다운 기호·코드블록을 벗기고 실제 보이는 글자 수로 다시 재자 그림이 뒤집혔습니다.
| 종류 | 편수 | 바이트(옛 지표) | 가시 글자 수 중앙값 |
|---|---|---|---|
| 가이드 | 38 | 7,090 B | 2,551자 |
| 용어집 | 40 | 3,439 B | 1,497자 |
| 프롬프트 | 30 | 4,649 B | 669자 |
| 뉴스레터 | 4 | 3,765 B | 1,275자 |
1,200자 미만이 51편이었습니다. 게다가 프롬프트 30편 중 27편은 소제목 구조가 글자 하나까지 동일했습니다. 669자짜리가 27개, 똑같은 뼈대로. 구글이 색인을 미루는 전형적인 형태가 정확히 이것입니다.
교훈은 단순합니다. 한국어 사이트에서 콘텐츠 분량을 바이트로 재면 3배 부풀려 읽게 됩니다. 지표가 틀리면 그 위에 쌓은 모든 판단이 틀립니다.
링크를 뿌리는 장치가 실은 몰아주고 있었다
「관련 글」 위젯은 112페이지 전부에 붙는 유일한 자동 내부링크 장치였습니다. 정렬은 이랬습니다.
// 공유 태그 수 → 날짜 순. 문제가 보이시나요?
.sort((a, b) => b.sharedTagCount - a.sharedTagCount || b.date.localeCompare(a.date))
.slice(0, 4)
정렬 키에 "어느 글에서 출발했는가"가 없습니다. 그래서 같은 태그를 가진 글들은 전부 똑같은 상위 4개를 골랐습니다. 112편 전체를 시뮬레이션한 결과:
- 인바운드 링크 0개인 페이지 48편
- 한 페이지가 25개 독식
- 중앙값 2개
크롤러 입장에서 그 48개는 "사이트가 스스로 중요하다고 표시한 적이 없는 페이지"입니다. 사이트맵 등재는 발견 신호일 뿐 중요도 신호가 아닙니다. 읽는 사람에게도 손해입니다 — 어느 글을 눌러도 관련 글 칸에 같은 4개가 뜨니 두 번째 클릭에서 이미 본 걸 다시 만납니다.
고친 방법은 동점 구간만 출발지 슬러그 해시로 회전시키는 것이었습니다(우선순위는 그대로). 무작위가 아니라 해시인 이유는 빌드가 결정적이어야 해서입니다. 결과는 인바운드 0인 페이지 48편 → 4편, 최댓값 25 → 13, 중앙값 2 → 4.
화면만 봐서는 절대 안 보이는 결함입니다. 각 페이지에는 관련 글 4개가 멀쩡히 떠 있으니까요. "아무도 나를 가리키지 않는다"는 사실은 전체를 한 번에 세어야만 드러납니다. 그래서 고치면서 계측기를 같이 남겼습니다.
로딩 게이트 아래에 있는 것은 크롤러가 못 본다
노출의 대부분을 혼자 받던 페이지가 이렇게 생겼습니다.
if (!mounted || !data) return <Skeleton />; // 서버 렌더 HTML은 여기서 끝난다
mounted는 useEffect에서, data는 fetch 이후에 채워집니다. 즉 크롤러가 받는 HTML에는 스켈레톤뿐이었습니다. 그 아래에 있던 39개 지역 이름이 한 글자도 실리지 않았고, 롱테일 질의에 걸릴 표면이 통째로 없었습니다.
새 데이터를 만들 필요는 없었습니다. 같은 배열을 서버 컴포넌트에서 한 번 더 렌더하니 HTML이 25.6KB → 37.4KB가 됐습니다.
같은 이유로, 내부 링크를 클라이언트 컴포넌트 안에 두면 링크 그래프에 안 잡힙니다. 사람에게는 보이는데 크롤러에게는 없습니다. 형제 사이트에서도 같은 결함을 찾았습니다 — 메뉴가 use client 드롭다운이라 링크가 조건부 블록 안에만 있었고, 크롤러는 버튼을 누르지 않으므로 블로그 6개 URL이 인바운드 0이었습니다. 그 사이트에서 서버 렌더 HTML에 항상 실리는 크롤 경로는 사실상 푸터 하나였습니다.
프레임워크가 조용히 덮어쓰는 것
Next.js는 자식 세그먼트가 openGraph를 다시 선언하면 부모 것을 통째로 교체합니다. 병합이 아닙니다. 그래서 탭 레이아웃이 url·title·description만 다시 선언하는 순간, 파일 규약이 만들어 둔 og:image가 사라집니다.
라이브로 확인하니 원인이 정확히 갈렸습니다 — openGraph를 재선언하지 않은 페이지 2곳은 이미지가 살아 있었고, 재선언한 탭 12개 + 소개 페이지는 전부 빈 카드였습니다. 이미지 생성기는 멀쩡히 돌고 있었는데 아무도 그 이미지를 받지 못하고 있었습니다.
같은 계열의 결함이 하나 더 있었습니다. 루트 레이아웃에 openGraph.url: "/"이 박혀 있어서, 그것을 상속받은 31개 페이지의 og:url이 전부 홈을 가리켰습니다. 정작 각 페이지의 canonical은 자기 경로를 정확히 가리키고 있었으니, 같은 페이지가 두 주소를 주장하는 상태였습니다. 고치는 방법은 그 한 줄을 지우는 것이었습니다 — 틀린 값이 사라지면 소비자가 요청 URL로 폴백해 저절로 맞습니다.
목록이 두 벌이면 반드시 갈라진다
탭 목록이 내비게이션 컴포넌트에 13개, 추적 모듈에 9개로 따로 있었습니다. 탭이 늘 때 추적 목록은 따라오지 않았고, 홈을 포함한 8개 경로가 통계에서 통째로 빠졌습니다.
더 나쁜 건 실패 방식이었습니다.
- 브라우저는 모든 경로에서 측정 비콘을 쏘고 있었고,
- 서버는 화이트리스트 밖이라는 이유로 그걸 조용히 버렸고,
- 관리자 화면은 조회수 0인 행을 그리지 않으므로 빠진 경로는 '0'조차 보이지 않았습니다.
데이터가 없었던 게 아니라 버려지고 있었고, 화면만 봐서는 그 사실을 알 수 없었습니다. 목록을 세 번째로 베끼는 대신 한 배열에서 파생시키는 것으로 고쳤습니다.
분류 필드에 안전장치를 걸면 안 되는 이유
반려동물 정보 사이트에서 의료 면책 문구가 이렇게 걸려 있었습니다.
{post.data.category === 'health' && <MedicalNotice />}
카테고리가 health인 글에만 붙습니다. 그런데 실측해 보니 비health 글 10편 전부가 수의사·동물병원을 언급하고 있었고, 그중 하나는 고양이 요도 폐색을 "진짜 의학적 응급 상황", "완전 폐색 후 24~48시간 내 사망 가능"이라고 쓰면서 면책이 없었습니다. 카테고리가 '용품'이었기 때문입니다.
카테고리는 주제 분류지 위험도 분류가 아닙니다. 위험도를 글이 직접 선언하게 스키마에 필드를 넣고, 기본값을 true로 뒀습니다 — 빠뜨렸을 때 안 붙는 방향이 아니라 붙는 방향으로 실패하게. 면책 렌더가 4/17에서 17/17이 됐습니다.
우리에게 가장 불리한 발견
색인률이 세 가지로 존재하고 있었습니다.
| 계통 | 표본 | 색인률 |
|---|---|---|
| 공개 페이지에 게시 중 | 7월 16일 동결 81건 | 42.0% |
| 서치 콘솔 내려받기 | 전체 151건 | 39.1% |
| 추적기 상태 파일 | 8월 2일 동결 147건 | 11.6% |
세 방법론이 각각 타당합니다. 분모를 언제 동결했느냐가 다를 뿐입니다. 문제는 공개된 것이 가장 유리한 숫자라는 점이었습니다. 정직성이 유일한 자산인 페이지에서 이건 구조적 결함입니다.
후속 (2026-08-09). 셋을 나란히 공개한 다음 날 다시 재니 40.7% · 40.1% · 39.1%로 모였습니다. 11.6%였던 새 도메인 표본이 하루 만에 40.1%가 됐기 때문인데(147건 중 색인 17 → 59), 그 표본이 낮았던 이유가 콘텐츠 품질이 아니라 새 주소가 아직 안 읽힌 상태였음이 사후에 드러난 것입니다. 갈렸을 때 셋 다 공개해 두지 않았다면, 지금 "42%에서 40%로 떨어졌다"는 이야기만 남고 이 사실은 보이지 않았을 겁니다.
같은 페이지에서 조건표는 "커스텀 도메인 없음"이라고 적고, 바로 아래 회차 노트는 "도메인이 바뀌고"라고 적고 있었습니다. 한 페이지가 두 말을 하고 있었습니다. 검색 결과에 뜨는 설명문은 1회차 수치(33.3%)에 멈춰 있었고 본문은 2회차(42.0%)를 보여주고 있었습니다.
손으로 적어 둔 숫자는 반드시 낡습니다. 최신 회차에서 파생시키는 것으로 바꿨습니다.
정리 — 아홉 가지 결함의 공통점
- 한국어 분량을 바이트로 재면 3배 부풀려진다
- 정렬 키에 출발지가 없으면 내부 링크가 승자독식이 된다
- 로딩 게이트 아래 콘텐츠는 서버 HTML에 없다
- 클라이언트 컴포넌트 안의 링크는 링크 그래프에 안 잡힌다
openGraph재선언은 병합이 아니라 교체다- 레이아웃에 박은 값은 하위 전체로 샌다
- 같은 목록이 두 벌이면 반드시 갈라진다
- 분류 필드에 안전장치를 걸면 분류가 틀릴 때 안전장치가 사라진다
- 지표를 만드는 코드 자체가 틀릴 수 있다 — 상위 N행의 합을 "총계"라고 부르면, 잘린 만큼과 익명화된 만큼이 조용히 사라진다
아홉 번째는 이 글을 쓴 다음 날 스스로에게서 나왔습니다. "클릭 0"은 저희가 몇 주 동안 되뇌며 전략을 정한 판정이었는데, 총계를 제대로 물어보니 클릭은 1이었고 노출도 41이 아니라 52였습니다. 틀린 지표는 틀린 콘텐츠보다 고치기 어렵습니다 — 아무도 그것을 의심하지 않기 때문입니다.
이건 AI에게 일을 맡길 때의 일반적인 문제와 같은 모양입니다 — "검증 통과"라는 보고가 실제로는 아무것도 검사하지 않은 경우요. 그 함정과 걸러내는 절차는 AI의 "완료했습니다"를 검증하는 방법에 따로 정리했습니다.
그리고 같은 일이 한 번 더 있었습니다. 위 2번을 재려고 만든 계측기 자체가 눈이 멀어 있었습니다. 프론트매터에 태그를 적는 형식이 두 가지(여러 줄 목록과 인라인 배열)인데 파서가 인라인 배열을 못 읽었고, 프롬프트 30편이 전부 그 형식이었습니다. 그래서 계측기가 프롬프트의 태그 관계를 통째로 못 보고 결과를 부풀려 보고했습니다(실제 4편을 7편으로). 파서를 고치고 같은 자로 양쪽을 다시 재서 위 수치를 정정했습니다. 계측기가 눈이 먼 것은 그 계측기로는 알 수 없습니다 — 다른 검사를 붙이다가 우연히 드러났습니다.
아홉 가지 전부 화면을 봐서는 보이지 않는 결함입니다. 페이지는 멀쩡히 렌더되고, 관련 글도 뜨고, 통계 화면에도 숫자가 있습니다. 전체를 한 번에 세거나, 서버가 실제로 내보낸 HTML을 열어 보거나, 라이브 응답을 직접 받아 봐야만 드러납니다.
그래서 고칠 때마다 계측기를 같이 남겼습니다. 인바운드 0인 페이지 수를 세는 스크립트, 메뉴 링크가 푸터에 있는지 보는 단언, 레이아웃에 고정 og:url이 없는지 보는 검사. 고친 것보다 이쪽이 오래 갑니다.
숫자가 실제로 어떻게 움직이는지는 색인 실측 로그에 회차별로 계속 올립니다. 오른 회차도 멈춘 회차도 고치지 않습니다. AI 에이전트를 실제로 굴려 나온 비용 실측은 멀티에이전트 릴레이 1건의 실제 비용에 따로 정리해 두었습니다.