AI로 장소 검색 서비스 만드는 중입니다 (기술 11) — 검색 유입 0에서 시작한 SEO: 잘못된 SEO 전략

기술 1~10편은 전부 검색을 잘 만드는 이야기였습니다. 이번 편은 정반대입니다 — 잘 만들어 놓고 아무도 안 오는 상태를 어떻게 진단했는지. 이 글의 초판에는 "아직 결과를 모른다" 고 적혀 있었습니다. 이제 압니다. 3주치 실측이 쌓였고, 그동안 전략을 다섯 번 틀렸습니다. 다섯 번 다 그럴듯했고 다섯 번 다 틀렸습니다. 그래서 결과를 붙여 다시 씁니다.
이 글은 두 번째 판입니다
초판은 8월 초에 썼습니다. 그때는 페이지를 만들고 사이트맵까지 조립한 상태였고, 마지막 절 제목이 「아직 결과는 모른다」였습니다.
그 뒤로 3주가 지났습니다. 등록했고, 색인이 시작됐고, 숫자가 나왔습니다. 그리고 그 숫자를 보고 나서 새로 틀린 게 넷 더 나왔습니다.
먼저 목록부터 보여드리겠습니다. 이 글은 이 다섯 개를 순서대로 따라갑니다.
| # | 그럴듯했던 전략 | 왜 틀렸나 | 실측 |
|---|---|---|---|
| ① | 노출이 없으니 사이트맵부터 내자 | 목록에 적힌 URL에 볼 게 없으면 제출은 성공하고 색인은 0 | 색인 가능 페이지가 루트 1개뿐 |
| ② | 장소가 1만이니 전부 열자 | 얇은 페이지 대량 색인은 도메인 전체 평가를 끌어내림 | 10,499 → 7,755 |
| ③ | 요약이 있으니 description에 그대로 쓰자 | AI 리뷰 요약이 스니펫에 부정 문구로 나감 | 한 페이지 121노출 0클릭 |
| ④ | 구글이 되면 네이버도 되겠지 | 같은 기간 Googlebot 6,460 vs Yeti 51 | 네이버 색인 1개 |
| ⑤ | 제목은 정확하기만 하면 된다 | 정확하지만 아무 유인이 없음 + 합계 숫자가 오염돼 있음 | 조회수 합계의 41%가 오매칭 |
그리고 이 다섯 개와 별도로, 3주 내내 측정을 안 하고 있었다는 것도 나중에 알게 됩니다. 그건 마지막에 적겠습니다.
출발점 — 검색은 좋아졌는데, 아무도 안 왔다
10편에 걸쳐 검색 품질을 올렸습니다. 콩국수도 잡고, 감자탕집이 한옥 카페로 나오는 것도 막고, 응답도 2~5초로 줄였습니다.
그리고 X·Threads·네이버 블로그에 글을 올렸습니다. 결과는 이랬습니다.
| 지표 | 값 |
|---|---|
| 게시물 노출 | 50~100 |
| GA 세션 | 6 |
| 실방문 (운영자 기기 제외) | 4명 |
| 검색 유입 | 0 |
먼저 확인한 건 CTR이었습니다. 노출 대비 클릭률이 4~8%. 정상 범위입니다.
이게 중요한 이유는 진단이 갈리기 때문입니다.
- CTR이 낮다 → 문구 문제. 제목·설명을 고쳐야 합니다.
- CTR은 정상인데 방문이 없다 → 노출 자체가 없다. 문구를 백 번 고쳐도 소용없습니다.
우리는 후자였습니다. 글이 나빠서 안 온 게 아니라 아무도 우리를 발견할 경로가 없었던 겁니다. SNS는 팔로워 수만큼만 도달하고, 검색은 0이었으니까요.
그래서 검색 엔진이 우리를 어떻게 보고 있는지부터 확인하기로 했습니다.
진단 — curl 네 번으로 끝났다
운영 서버에 직접 요청을 날려봤습니다. 그리고 5분 만에 원인을 찾았습니다.
$ curl https://youchuca.popit.kr/robots.txt
200 OK
<!doctype html><html>... ← index.html
robots.txt를 요청했는데 HTML이 돌아왔습니다.
$ curl https://youchuca.popit.kr/sitemap.xml
200 OK
<!doctype html><html>... ← 역시 index.html
사이트맵도 마찬가지였고요.
robots.txt와 sitemap.xml: 크롤러가 사이트에 오면 가장 먼저 찾는 두 파일입니다. robots.txt는 "여기는 봐도 되고 여기는 보지 마세요"라는 안내문이고, sitemap.xml은 "이 URL들을 봐 주세요"라는 목록입니다. 둘 다 정해진 형식의 텍스트여야 합니다.
원인은 nginx 설정 한 줄이었습니다.
location / { try_files $uri $uri/ /index.html; }
SPA(단일 페이지 앱)의 표준 설정입니다. "요청한 파일이 없으면 index.html을 주라" — 앱 안에서 /settings, /place/123 같은 경로를 라우터가 처리해야 하니 당연한 설정이죠.
문제는 이게 없는 파일 전부에 적용된다는 겁니다.
"파일이 없다"보다 "HTML을 반환한다"가 더 나쁩니다. 404가 오면 크롤러는 "안내문이 없구나, 기본값으로 가자"라고 판단합니다. 그런데 200 + HTML이 오면 파싱을 시도하다 실패합니다. 사이트가 고장 난 것처럼 보이는 거죠.
이걸 soft 404라고 부릅니다.
soft 404: 실제로는 없는 페이지인데 서버가 200 OK로 응답하는 것. 크롤러 입장에선 "정상 페이지"로 알고 크롤링 예산을 쓰는데 내용은 없거나 엉뚱합니다. 사이트 전체 평가에 해로워서 구글 서치 콘솔이 별도 오류 항목으로 잡습니다.
이어서 콘텐츠 쪽도 확인했습니다.
$ curl https://youchuca.popit.kr/share/place/{id}
200 OK
<meta property="og:title" content="...">
<meta http-equiv="refresh" content="0;url=/?place=...">
공유 링크는 OG 태그(카톡·트위터에 뜨는 미리보기)는 제대로 있는데, 본문이 리다이렉트 스텁이었습니다. 사람이 열면 지도로 즉시 넘어가니 잘 동작하는데, 크롤러에게는 내용이 0인 문서입니다. 구글은 리다이렉트로 처리해 색인에서 빼고, 네이버는 빈 문서로 봅니다.
그리고 결정타.
프론트 라우터에 /place/{id} 경로 — 없음
마케팅 자동화가 만들어 뿌리던 장소 링크가 애초에 존재하지 않는 경로였습니다. SPA 폴백 덕에 200이 오고 지도가 뜨긴 했지만, 그건 그 장소 페이지가 아니라 그냥 홈이었습니다.
원인을 한 줄로 줄이면 이렇습니다.
없는 경로가 전부 200 + HTML로 응답되고, 색인 가능한 콘텐츠 페이지가 루트 1개뿐이다.
틀린 전략 ① — "사이트맵부터 내자"
여기서 가장 먼저 떠오른 해법은 사이트맵 제출이었습니다. 검색 노출이 안 되면 사이트맵을 내라 — SEO의 상식이니까요.
틀렸습니다.
사이트맵은 "이 URL들을 봐 달라"는 목록일 뿐입니다. 목록에 적힌 URL이 열어봤을 때 볼 게 없으면, 제출은 성공하고 색인은 0입니다.
지금 상태로 사이트맵을 냈다면 크롤러가 받는 건 이랬을 겁니다.
| URL | 크롤러가 받는 것 | 결과 |
|---|---|---|
/ |
<div id="root"> + OG 태그 |
루트 1개, 내용 없음 |
/share/place/{id} |
리다이렉트 스텁 | 색인 제외 |
/robots.txt |
index.html | 파싱 실패 |
/sitemap.xml |
index.html | 파싱 실패 |
8천 개 URL을 제출하고 8천 개가 전부 "색인 안 됨"으로 잡히는 상황입니다. 그것도 사이트 품질 평가를 깎으면서요.
그래서 순서를 고정했습니다.
① 색인될 만한 페이지를 먼저 만든다
② 그 페이지 목록으로 사이트맵을 만든다
③ 그다음에 등록·제출한다
①을 건너뛰고 ②③만 하면 제출은 되지만 색인은 0으로 남습니다.
이게 첫 번째 잘못된 전략입니다. 실행하지는 않았지만, 아무 저항 없이 떠올랐다는 게 문제였습니다. "노출이 없다 → 사이트맵을 낸다"는 연결이 너무 자연스러워서, 그 사이에 "낼 게 있는가"라는 질문이 들어갈 자리가 없었습니다.
SPA는 크롤러에게 빈 div다
①을 하려면 근본 제약을 하나 넘어야 했습니다.
SPA(Single Page Application): 서버는 빈 껍데기 HTML 하나만 주고, 자바스크립트가 브라우저에서 화면을 그리는 방식. 유추카 지도 화면이 이 방식입니다. 앱처럼 부드럽게 동작하는 대신, 서버가 보내는 HTML에는 내용이 없습니다.
구글은 자바스크립트를 실행해서 보긴 합니다. 다만 나중에, 그리고 항상은 아닙니다. 네이버 크롤러는 훨씬 소극적입니다. 국내 서비스라 이쪽이 더 아팠고요. (이 문장은 나중에 부분적으로 정정하게 됩니다 — 전략 ④에서 다시 다룹니다.)
그래서 SSR 페이지를 따로 만들기로 했습니다.
SSR(Server-Side Rendering): 서버가 완성된 HTML을 만들어 보내는 방식. 크롤러가 받자마자 내용을 읽을 수 있습니다.
여기서 중요한 결정을 하나 했습니다. 기존 /share/* 같은 스텁을 만들지 않는다. 실제 콘텐츠를 렌더하고, 하단에 "지도에서 보기" 버튼으로 앱(SPA)으로 넘깁니다.
크롤러 전용 페이지를 따로 만들면 안 되기 때문입니다. 크롤러에게만 보여주는 내용과 사람에게 보여주는 내용이 다르면 클로킹으로 간주돼 제재 대상이 됩니다. "사람도 그대로 쓸 수 있는 페이지"여야 합니다.
무엇을 페이지로 열 것인가
세 종류를 만들었습니다. 각각 역할이 다릅니다.
| 페이지 | 개수 | 역할 |
|---|---|---|
/channel/{slug} 채널 |
20 | 내부 링크 허브 |
/list/{시도}/{시군구}/{카테고리} 지역×카테고리 |
259 | 유입 주력 |
/place/{id} 장소 |
7,755 | 볼륨 + 구조화 데이터 |
지역×카테고리가 유입 주력인 이유는 키워드 전략 때문입니다.
"서울 근교 계곡 카페" 같은 대형 키워드는 신생 도메인이 못 먹습니다. 이미 수년간 쌓인 블로그와 대형 매체가 다 차지하고 있죠. 반면 "성수동 카페 추천", "강릉 맛집 유튜버" 같은 롱테일은 개별 검색량이 작아도 259개가 합쳐지면 무시할 수 없습니다.
채널부터 만든 건 개수가 적어서였습니다. 20개뿐이라 SSR 렌더 패턴을 검증하기 좋고, 나머지 두 종의 내부 링크 허브가 됩니다.
이 예측은 나중에 절반만 맞습니다. 실제로 먼저 색인되고 노출이 붙은 건 유입 주력으로 설계한 지역 페이지가 아니라 볼륨용으로 만든 장소 페이지였습니다. 결과 절에서 다시 보겠습니다.
틀린 전략 ② — "장소가 1만이니 전부 열자"
여기가 이 작업에서 가장 중요한 판단이었습니다.
장소는 10,499곳입니다. 전부 페이지로 열면 URL 1만 개를 확보하는 셈이니 좋아 보입니다. 색인 자산이 많을수록 좋다는 것도 상식이고요.
역효과입니다.
이름과 주소만 있는 얇은 페이지를 수천 개 색인시키면, 그 페이지들이 색인 안 되는 데서 끝나지 않고 사이트 전체의 품질 평가를 끌어내립니다. 도메인 전체가 "내용 없는 사이트"로 분류되면 좋은 페이지까지 같이 묻힙니다.
그래서 게이트를 걸었습니다.
영상 언급 ≥ 1 AND 네이버 리뷰 ≥ 10
| 단계 | 남는 장소 |
|---|---|
| 전체 | 10,499 |
| 영상 언급 ≥ 1 | 10,199 |
| + 리뷰 ≥ 10 | 7,930 |
| + 비공개 채널 전용 장소 제외 | 7,755 |
2,700곳 이상을 일부러 버렸습니다.
그리고 남은 7,755곳이 정말 두꺼운 페이지가 되는지를 먼저 세어봤습니다.
| 재료 | 커버리지 |
|---|---|
| 네이버 상세 정보 보유 | 7,755 (100%) |
| 리뷰 발췌 1건 이상 | 7,755 (100%) |
| 리뷰 발췌 3건 이상 | 7,751 |
| 자체 생성 요약 | 7,750 (99.9%) |
| 사진 1장 이상 | 6,921 (89%) |
"얇지 않을 것 같다"가 아니라 "얇지 않다"를 확인하고 열었습니다. 기술 7편에서 배운 대로, 재보기 전에는 모르니까요.
지역 페이지에도 같은 원리로 게이트가 붙었습니다.
| 기준 | 조합 수 |
|---|---|
| 장소 10곳 이상 | 292 |
기타 카테고리 제외 |
262 |
| 비공개 채널 전용 장소 제외 | 261 |
| 지역 정보 미파싱 장소 제외 | 259 |
기타를 뺀 이유가 재밌습니다. "성동구 기타" 라는 페이지는 검색 수요가 0입니다. 아무도 그렇게 검색하지 않아요. 색인돼 봐야 얇은 페이지로만 남습니다.
게이트는 "404"가 아니라 "noindex"다
게이트를 만들고 나서 함정을 하나 발견했습니다.
처음엔 게이트를 못 넘은 장소는 404를 낼 생각이었습니다. 색인 대상이 아니니까요.
그런데 앱의 공유 링크가 모든 장소를 가리킵니다. 사용자가 리뷰 3개짜리 작은 카페를 친구에게 공유했는데 404가 뜨면? 공유 기능이 2,700곳에서 깨집니다.
그래서 셋으로 나눴습니다.
| 상태 | 응답 |
|---|---|
| 게이트 통과 | 200 — 색인 대상 |
| 게이트 미통과 | 200 + <meta name="robots" content="noindex,follow"> |
| 숨김 장소 · 없는 ID | 404 |
noindex, follow: "이 페이지는 색인하지 마라. 단, 여기 있는 링크는 따라가라." 페이지 자체는 검색에 안 나오지만 크롤러가 이 페이지를 거쳐 다른 페이지로 갈 수 있습니다.
"열려 있느냐"와 "색인하느냐"는 다른 질문이었습니다. 이 둘을 하나로 처리하려다 사용자 기능을 깨뜨릴 뻔했습니다.
사이트맵과 페이지가 어긋나면 안 된다
사이트맵을 만들 때 가장 조심한 건 이겁니다.
사이트맵에 있는데 404나 noindex면, 서치어드바이저가 오류로 잡습니다.
"이 URL 봐 주세요" 해놓고 열어보니 없는 거니까요. 신뢰를 깎는 행동입니다.
이게 왜 어렵냐면, 사이트맵 목록과 페이지 존재 조건이 다른 코드에 있으면 언젠가 반드시 어긋나기 때문입니다. 게이트 조건을 한쪽만 고치는 날이 오거든요.
그래서 사이트맵이 페이지와 같은 함수를 호출하게 했습니다.
| 사이트맵 | 목록 소스 |
|---|---|
| 채널 | ChannelPageService.visibleChannels() |
| 지역 | RegionCategoryPageService.combos() |
| 장소 | PlaceIndexGate + 숨김 장소 제외 |
새 쿼리를 쓰지 않았습니다. 페이지가 "이건 열려 있다"고 판단하는 그 코드가, 사이트맵 목록도 만듭니다. 조건이 갈릴 수 없는 구조입니다.
최종 구조는 이렇습니다.
/sitemap.xml ← 인덱스
├─ /sitemap-static.xml ← 2
├─ /sitemap-channels.xml ← 22
├─ /sitemap-regions.xml ← 264
├─ /sitemap-places-1.xml ← 5,000
└─ /sitemap-places-2.xml ← 3,960
총 9,248 URL. 앞의 설계 표(채널 20 · 지역 259 · 장소 7,755)보다 늘어난 건, 설계와 배포 사이 며칠 동안 채널과 장소가 추가로 수집됐기 때문입니다. 게이트 조건은 그대로고 통과한 개수만 바뀌었습니다.
사이트맵 하나에 5만 개까지 담을 수 있지만 5,000개로 나눴습니다. 하나가 커지면 재생성 비용이 커지고, 일부만 갱신하기도 어려워지니까요.
이 분할이 나중에 네이버 쪽에서 사고를 냅니다. 전략 ④에서 다룹니다.
페이지를 얇게 만들지 않으려면
게이트를 통과한 페이지도 내용을 어떻게 채우냐에 따라 얇아질 수 있습니다.
지역 페이지의 장소 카드가 이름 + 주소만 있다면, 그건 60줄짜리 표에 불과합니다. 구글이 "크롤링됨 — 현재 색인이 생성되지 않음" 으로 버리는 전형적인 형태고요.
그래서 카드마다 이걸 실었습니다.
- 사진 (지연 로딩)
- 네이버 카테고리·주소
- 방문자 리뷰 수·평점
- 리뷰 발췌 1건
장소 페이지는 더 두껍습니다 — 사진 5장, 자체 생성 요약, 리뷰 발췌 3건, 소개한 영상 10건(언급 문구 + 타임스탬프 링크), 소개한 채널, 같은 지역 목록 링크.
타임스탬프가 특히 마음에 듭니다. 영상 링크에 &t=617s가 붙어서, 그 장소가 나오는 지점부터 재생됩니다. 1편에서 "20분짜리 영상에서 30초를 못 찾아 포기했다"고 했던 그 문제를, 검색 결과 페이지가 직접 풀어주는 셈입니다.
색인 품질을 지키는 사소한 결정들
이 작업에서 재밌었던 건, 큰 설계보다 작은 결정들이 색인 품질을 좌우한다는 점이었습니다.
① URL을 함부로 만들지 않는다
채널 페이지에 영상 목록이 있는데, 정렬과 페이징이 필요했습니다. 보통 이렇게 하죠.
/channel/유리소리tv?page=3&sort=recent
안 했습니다. 20채널 × 12페이지 × 2정렬 = 약 480개의 얇은 목록 URL이 생깁니다. 전부 비슷한 내용이라 중복 판정을 받고, 크롤링 예산만 갉아먹습니다.
그렇다고 목록을 자바스크립트로 채우면 크롤러는 1페이지만 봅니다.
그래서 이렇게 했습니다. 목록 전체를 서버가 렌더하고, 자바스크립트는 숨기기만 합니다.
- 크롤러: 전체 목록을 다 봄
- 사람: 처음엔 일부만 보이고, 정렬·더보기를 누르면 나머지가 나타남
- JS가 꺼져 있어도 전체가 그대로 보임
들어가는 데이터가 늘어나 페이지가 156KB(압축 35KB)까지 커졌지만, URL을 480개 늘리는 것보다 낫다고 판단했습니다.
② 조회순만 담으면 최신 정렬에 최신이 없다
영상 목록에 조회순 상위 30개와 최신순 상위 30개의 합집합을 담았습니다.
조회순만 뽑아두면, 사용자가 "최신순"을 눌렀을 때 정작 최신 영상이 없는 목록이 됩니다. 실측해 보니 일상이여행 채널의 최신 영상은 조회수 500~8,000회대라 조회순 상위 30에 아예 못 듭니다.
관련해서 데이터 편차도 하나 배웠습니다. 유리소리TV는 장소를 언급한 영상 690개 중 29개만 조회수를 갖고 있습니다. 조회수 없는 영상을 목록에서 빼버리면 이 채널 페이지가 텅 비어요. 그래서 빼지 않고 조회순에서만 뒤로 보냅니다.
③ 우리 얇은 페이지로는 링크를 안 보낸다
내부 링크에 nofollow를 선택적으로 붙였습니다.
| 링크 대상 | 정책 |
|---|---|
/place/{id}, /channel/{slug} (SSR 페이지) |
dofollow |
/?place=, /?search= (SPA 딥링크) |
nofollow |
SPA 딥링크는 크롤러에게 빈 껍데기입니다. 거기로 링크를 보내면 우리가 우리 얇은 페이지를 추천하는 꼴이 됩니다. 사람에게는 그대로 동작하고요.
이게 단계적으로 바뀌었다는 점도 적어둘 만합니다. 채널 페이지를 만들 때는 장소 SSR 페이지가 아직 없어서 모든 장소 링크가 nofollow SPA 딥링크였습니다. 장소 페이지가 생긴 뒤 게이트 통과 여부에 따라 갈리게 바꿨고요. 실측하면 성동구 카페 목록 25곳 중 dofollow 18 / nofollow 7, 유리소리TV 채널 페이지는 dofollow 145 / nofollow 22입니다.
없는 경로로 미리 링크해두면 안 됩니다. nginx SPA 폴백이 200 + HTML을 주니까, 그게 전부 soft 404가 됩니다. 진단에서 본 그 문제를 스스로 만드는 셈이죠.
이 원칙은 마케팅 규칙까지 바꿨습니다. 그전까지 블로그·SNS 게시물은 전부?search=딥링크를 걸고 있었는데, 그건 크롤러에게 빈 페이지인 데다 nofollow라 링크 가치도 전달되지 않습니다. 그래서 네이버 블로그는/channel/*·/list/*같은 SSR 페이지로 링크하도록 규칙을 고쳤습니다. X·Threads는 SEO 목적이 아니라 클릭 유도 목적이라?search=를 유지했고요.
④ 남의 평점을 우리 평점으로 마크업하지 않는다
장소 페이지에 JSON-LD 구조화 데이터를 넣었습니다.
JSON-LD / 구조화 데이터: 페이지 내용을 기계가 읽을 수 있는 형식으로 따로 적어두는 것. 카페면 CafeOrCoffeeShop, 주소·좌표는 정해진 필드로. 이게 있어야 구글 지도·로컬 결과에 노출될 여지가 생깁니다.
넣은 것: 이름, URL, 사진, 주소, 좌표, 네이버 지도 링크, 자체 생성 설명.
안 넣은 것: aggregateRating(평점 집계).
리뷰 수와 평점은 우리가 갖고 있고, 넣으면 검색 결과에 ⭐ 별점이 뜹니다. 클릭률에 좋겠죠. 그런데 그 평점은 네이버가 집계한 것이지 우리가 모은 게 아닙니다.
남의 집계를 우리 사이트 평점으로 마크업하면 구글 리뷰 스니펫 정책 위반이고, 수동 조치 대상입니다. 별 하나 얻으려다 도메인 전체가 페널티를 받을 수 있습니다.
그래서 평점은 "네이버 방문자 리뷰"라고 출처를 밝힌 본문 텍스트로만 보여줍니다. 별 표시를 원한다면 유추카 자체 평점을 모으는 게 선행 조건입니다.
같은 이유로 네이버가 생성한 AI 요약도 안 씁니다. 대신 우리가 리뷰 원문으로 직접 만든 요약(기술 6편)을 싣습니다.
⑤ 사이트맵의 사소한 함정들
changefreq·priority는 안 넣었습니다. 구글이 무시한다고 공식적으로 밝힌 값입니다. 파일만 커집니다.lastmod은 "요청 시각"이 아니라 "스냅샷 생성 시각" 입니다. 요청할 때마다 현재 시각을 넣으면 크롤러는 매번 전부 바뀐 것으로 보고 다시 긁습니다.- 장소는
_id순으로 정렬합니다. 정렬이 없으면 요청마다 5,000개 분할 경계가 흔들려서, 크롤러가 같은 URL을 다른 파일에서 반복해 보게 됩니다. - 6시간 캐시. 8천 줄을 조립하는 데 숨김 장소 계산이 전체 언급을 훑어서 비쌉니다. 크롤러가 올 때마다 다시 할 일이 아닙니다.
/about — 크롤러에게 입구를 만들어주기
사이트맵을 다 만들고 보니 sitemap-static.xml이 / 한 줄이었습니다.
나머지 SPA 정적 경로를 넣으면 될 것 같은데, 실제로 확인해 보니 이랬습니다.
/settings/about 본문 텍스트 16자
/legal/terms 본문 텍스트 16자
/legal/privacy 본문 텍스트 16자
(title도 셋 다 동일)
전부 자바스크립트로 그려지는 페이지라 서버 HTML에는 아무것도 없었습니다. 게다가 /settings는 robots.txt로 막아둔 경로라, 사이트맵에 넣으면 "막아놓고 색인해 달라고 한다" 는 충돌로 잡힙니다.
그래서 소개 페이지를 SSR로 새로 만들었습니다 — /about.
역할이 두 가지입니다.
- 브랜드 검색 대응 — "유추카"로 검색했을 때 잡히는 대표 페이지
- 상위 허브 — 이게 없으면 크롤러가 채널·지역 페이지를 사이트맵으로만 발견합니다.
/about에서 채널 20개 + 상위 지역 조합 30개로 링크가 나가면 색인 깊이가 1 줄어듭니다.
두 번째가 실제 이유였습니다. 사이트맵은 "봐 달라"는 요청이고, 내부 링크는 "이게 중요하다"는 신호입니다. 둘 다 있는 편이 훨씬 낫습니다.
여기까지가 초판의 내용입니다. 이제부터가 새로 쓰는 부분입니다.
결과 — 3일 만에 색인이 시작됐다
8월 2일 오후, 운영에 배포하고 사이트맵을 냈습니다. 그리고 나흘 뒤 구글 서치 콘솔에 첫 리포트가 잡혔습니다.
| 날짜 | 클릭 | 노출 | CTR | 평균 게재순위 |
|---|---|---|---|---|
| 08-01 | 0 | 0 | — | — |
| 08-02 | 3 | 733 | 0.41% | 10.1 |
| 08-03 | 4 | 1,920 | 0.21% | 12.3 |
| 08-04 | 9 | 2,003 | 0.45% | 11.0 |
| 합계 | 16 | 4,656 | 0.34% | ~11 |
0에서 사흘 만에 일 노출 2,000까지 올라왔습니다. 설계 문서에서는 "구글이 먼저, 2~4주"로 잡았는데 그보다 훨씬 빨랐습니다.
색인된 URL은 전부 /place/{id} 형태였습니다. 유입 주력으로 설계한 지역 페이지가 아니라, 볼륨용으로 만든 장소 페이지가 먼저 잡힌 겁니다.
이유는 검색어를 보면 명확합니다.
클릭이 발생한 검색어 — 5개, 전부 상호명 직접 검색
| 검색어 | 클릭 | 노출 | 평균 순위 |
|---|---|---|---|
| 기호네곱창 | 1 | 4 | 7.75 |
| 우리돼지국밥 | 1 | 4 | 8.5 |
| 송하정스시 | 1 | 3 | 4.67 |
| 꽁당보리밥 본점 | 1 | 2 | 10 |
| 원차우 | 1 | 1 | 8 |
836개 검색어에 노출됐는데 거의 전부 상호명 또는 상호명+지역입니다. "서울 근교 계곡 카페" 같은 테마 키워드는 아직 하나도 안 잡혔습니다.
이건 나쁜 소식이면서 좋은 소식이었습니다. 테마 키워드는 못 먹었지만, "○○ 어디였지" 하고 상호를 검색하는 사람에게 유추카가 답으로 잡히기 시작한 것이니까요. 1편에서 적은 서비스의 출발점과 정확히 같은 자리입니다.
458배 차이는 착시였다
리포트를 열자마자 나온 질문이 이거였습니다. GA4 사용자수는 하루 10명 미만인데 GSC는 수천이다. 계측이 깨진 건가?
아닙니다. 비교 대상이 틀렸습니다.
| 8/2 | 8/3 | 8/4 | |
|---|---|---|---|
| GSC 클릭 | 3 | 4 | 9 |
| GA4 사용자 | 10명 미만 | 10명 미만 | 10명 미만 |
노출은 검색 결과에 링크가 표시된 횟수입니다. 아무도 클릭하지 않아도 순위권에 있으면 계속 쌓입니다. 비교해야 할 건 GSC 클릭 ↔ GA4 사용자이고, 그 둘은 사실상 일치합니다.
노출 4,656과 사용자 10명을 나란히 놓아서 458배로 보였을 뿐입니다. 지표의 정의가 다르면 비율은 아무 의미가 없습니다.
틀린 전략 ③ — "요약이 있으니 description에 그대로 쓰자"
노출은 붙는데 클릭이 안 붙었습니다. CTR 0.34%.
평균 게재순위 11위라는 것만으로도 설명은 됩니다. 1페이지 하단~2페이지 구간이고, 이 구간 CTR은 원래 1% 미만이니까요. 그런데 페이지별로 보니 그것만으로는 설명이 안 되는 게 하나 있었습니다.
/place/6a126c83b4fe865c7bc884db 빌라퍼즈 풀빌라 121노출 0클릭
전체에서 노출 1위인 페이지가 0클릭입니다. 121번 뜨는 동안 한 명도 안 눌렀습니다.
실제로 색인된 스니펫을 꺼내 봤습니다.
"가평 근처 독채 풀빌라로 프라이빗한 수영장과 깔끔한 시설이 강점이나, 방문 시기와 관리 상태에 따라 만족도 편차가 크다. … 와이파이 미흡·청소 불량·예약 변경·가격대 대비 서비스 미흡 등을 지적한 리뷰도 있어…"
검색 결과에 "청소 불량" 이 떠 있었습니다.
다른 페이지도 마찬가지였습니다.
송정희어머니순두부 — "… 매장 앞 주차가 편리하지만, 대중교통 이용 시 환승이 필요해 이동이 번거롭고, 저녁 7시 이후 방문 시 운영 종료 가능성이 있어 주의가 필요하다." 연일물회 — "… 매콤하면서도 달콤한 맛이 특징이다. 다만…" (문장이 잘림)
원인은 단순합니다. 기술 6편에서 만든 AI 리뷰 요약을 그대로 meta description에 넣고 있었습니다.
그 요약은 원래 상세 화면에서 읽으라고 만든 것입니다. 장단점을 균형 있게 적는 게 맞고, 실제로 잘 쓰고 있었습니다. 그런데 검색 스니펫은 155자만 잘려서, 맥락 없이, 클릭 전에 보입니다. 같은 텍스트인데 놓이는 자리가 다르면 완전히 다른 물건이 됩니다.
비교군도 리포트 안에 있었습니다. AI 요약이 없어서 템플릿으로 나간 페이지입니다.
짱나라24시해장국 (13노출 1클릭) — "서울특별시 도봉구의 한식 짱나라24시해장국 — 술맛여행놈 영상에 소개된 곳. 방문자 리뷰 971건."
지역·카테고리·소개한 유튜버·리뷰 수. 짧고, 부정 표현이 없고, 무엇보다 유추카에만 있는 정보가 들어 있습니다.
description 생성이 두 갈래(AI 요약이 있으면 그것, 없으면 템플릿)로 갈려 있었고, 없는 쪽이 더 나았던 겁니다.
덤으로 드러난 것 — 카테고리 라벨이 새고 있었다
같은 리포트에서 제목도 확인했는데, 이런 게 있었습니다.
| 실제 title | 문제 |
|---|---|
송정희어머니순두부 · 속초시 명소 추천 · 유추카 |
순두부집인데 "명소" |
빌라퍼즈 풀빌라 · 가평군 기타 추천 · 유추카 |
풀빌라인데 "기타" |
짱나라24시해장국 · 도봉구 맛집 추천 · 유추카 |
정상 |
내부 enum인 PlaceCategory.OTHER·SIGHT가 사용자 노출 문구로 그대로 새어 나가고 있었습니다. 관리 화면에서는 "기타"가 멀쩡한 라벨이라 두 달 동안 아무도 이상하게 안 봤는데, 그게 구글 검색 결과 제목에 박히니 바로 티가 났습니다.
둘 다 코드 한 곳씩이었습니다. description은 템플릿으로 통일하고, 카테고리는 네이버 업종을 반영하도록 고쳤습니다. 고친 뒤 같은 페이지는 이렇게 나옵니다.
빌라퍼즈 풀빌라 · 가평군 펜션 추천 · 유추카
여기서 배운 것. 화면용으로 만든 텍스트가 자동으로 검색용이 되지 않습니다. 그리고 내부 식별자는 언젠가 반드시 사용자 화면으로 샙니다. 이 프로젝트에서 그게 하필 구글 검색 결과였을 뿐입니다.
틀린 전략 ④ — "구글이 되면 네이버도 되겠지"
구글이 사흘 만에 2,000노출까지 올라온 걸 보고, 네이버도 시간문제라고 생각했습니다.
8월 6일, 네이버 서치어드바이저의 색인된 페이지 수는 1개였습니다.
원인 1 — 사이트맵 인덱스를 따라 내려가지 않았다
nginx 로그에서 네이버 봇(Yeti)의 요청 33건을 전부 경로별로 집계했습니다.
5 /sitemap.xml 200 ← 인덱스는 5번 읽어감
0 /sitemap-*.xml ← 하위 파일은 단 한 번도 요청 없음
5 / 200
4 /robots.txt 200
3 /place/{id} 200
3 /naver...html 200 ← 소유확인 파일
인덱스를 읽고 거기서 멈췄습니다.
앞에서 사이트맵을 5개로 쪼개고 /sitemap.xml을 인덱스로 뒀다고 적었죠. 구글은 인덱스를 따라 하위 파일까지 내려갑니다. 네이버는 안 내려갔습니다. 그래서 실제 URL 9,248개를 담은 하위 5개 파일을 한 번도 요청하지 않았습니다.
차단도 오류도 아니었습니다. 응답은 전부 200이고 robots.txt도 Allow: /입니다. 크롤할 URL 목록을 못 받은 것뿐이었습니다.
해결은 5개를 각각 제출하는 것이었습니다. 그리고 제출하자마자 이렇게 됐습니다.
21:27:05 /sitemap-static.xml 200
21:27:17 /sitemap-channels.xml 200
21:27:27 /sitemap-regions.xml 200
21:27:36 /sitemap-places-1.xml 200
21:27:44 /sitemap-places-2.xml 200
약 10초 간격으로 1분 안에 다섯 개를 전부 가져갔습니다. 같은 봇이 인덱스는 5번이나 읽으면서 하위 파일은 나흘간 한 번도 안 건드렸는데요.
원인 2 — 목록을 줬는데도 안 긁었다
목록을 받았다는 것과 실제로 크롤하는 것은 다릅니다. 닷새 뒤 다시 로그를 봤습니다.
9일간(8/2~8/11) Yeti 요청 51건 전부:
| 경로 | 요청 |
|---|---|
/ |
13 |
sitemap*.xml |
13 |
robots.txt |
6 |
| 소유확인 파일 | 3 |
/place/{id} |
3 |
| 그 외 | 13 |
실제 콘텐츠 페이지 요청이 3건입니다. 사이트맵에 8,000개 넘는 URL이 들어 있는데요.
대조군을 뽑았습니다. 같은 기간 Googlebot은 6,460건.
Googlebot 6,460
Yeti 51 ← 127분의 1
서버·robots.txt·사이트맵에 문제가 있었다면 구글도 못 긁었을 겁니다. 잘 긁고 있습니다. 그러니 기술적 차단이 아니라 네이버가 이 신규 도메인에 배정한 크롤 예산이 극도로 낮은 것입니다.
여기서 RSS 등록이라는 아이디어가 나왔는데, 접었습니다. RSS는 새로 올라온 소수의 최신 항목을 알리는 채널입니다. 사이트맵 8,000개 중 3개를 긁는 상태에서 RSS로 20개를 더 준다고 크롤 예산 자체가 늘지 않습니다. 게다가 유추카는 매일 새 글이 발행되는 구조가 아니라, 장소 페이지가 한 번에 대량으로 생기고 그 뒤로 정적입니다. RSS가 표현하기에 맞지 않는 형태입니다.
원인 3 — Yeti가 가장 많이 본 페이지가 빈 껍데기였다
수동 수집 요청용 URL 47개를 뽑으면서 전부 Yeti 사용자 에이전트로 실제 요청해 봤습니다. 그러다 위 표에서 1위인 항목이 걸렸습니다.
루트 /. Yeti 요청 51건 중 13건, 가장 많이 본 페이지입니다.
$ curl -A "Yeti/1.1" https://youchuca.popit.kr/
본문 텍스트: 15자 ("유추카 — 유튜브 추천 카페")
<a href> 링크: 0개
/about·/channel/*·/list/*·/place/*는 전부 SSR로 제대로 나옵니다. 루트만 SPA 껍데기였습니다. 진단 절에서 "색인 가능한 페이지가 루트 1개뿐"이라고 적었는데, 정확히 말하면 그 루트마저 비어 있었습니다.
구글은 자바스크립트를 실행해서 봅니다. 그래서 이 문제가 구글 쪽에서는 안 드러났습니다. 네이버 봇이 사이트에 올 때마다 본 첫인상이 계속 빈 페이지였던 겁니다. 크롤 예산이 안 늘어난 유력한 원인이었습니다.
/about이 이미 채널 20개 + 지역 30개 dofollow 허브 역할을 하고 있었으므로, 같은 성격의 진입 블록을 루트 index.html에 넣었습니다. 배포 후 실측입니다.
본문 15자 → 145자
링크 0개 → /about, /best
그리고 그 수정이 새 문제를 만들었다
루트에 콘텐츠를 넣자마자 걸리는 게 있었습니다. nginx가 try_files $uri $uri/ /index.html이라, SSR 경로가 아닌 모든 URL이 같은 index.html을 받습니다.
| 경로 | 응답 |
|---|---|
/legal/terms |
200 |
/settings |
200 |
/this-path-does-not-exist-12345 |
200 |
즉 이 진입 블록은 루트뿐 아니라 존재하지 않는 모든 URL에서도 크롤러에게 노출됩니다. 변경 전에도 200이긴 했지만 본문이 15자뿐이라 무시됐을 가능성이 큽니다. 이제 본문과 링크가 생겨서, 쓰레기 URL이 색인 후보로 잡힐 수 있게 된 겁니다.
진단 절에서 지적한 soft 404를, 고치는 과정에서 더 크게 만들 뻔했습니다.
그래서 index.html의 canonical을 루트로 고정했습니다. 어떤 경로로 들어와도 "이 문서의 정본은 /"라고 선언하는 겁니다. SSR 페이지들은 각자 자기 canonical을 내므로 충돌하지 않고요. 배포 후 /about이 자기 canonical을 유지하는지 따로 확인했습니다.
효과 — 3건에서 64건으로
루트 배포와 수동 수집 요청 47개를 같은 날 넣었습니다. 몇 시간 뒤 로그입니다.
| 경로 | 그날 아침 | 그날 밤 |
|---|---|---|
/channel/{slug} |
0 | 20 |
/place/{id} |
3 | 18 |
/best/{...} |
0 | 14 |
/list/{...} |
0 | 12 |
/ |
13 | 16 |
콘텐츠 페이지 요청 3건 → 64건.
제출이 실제로 먹혔다는 증거도 로그에 남았습니다. 20:51~20:52의 마지막 10건이 /channel/유리소리tv → /일상이여행 → /김사원세끼 순서인데, 제출한 채널 10개와 순서까지 일치합니다.
루트 배포도 응답 크기로 확인됐습니다.
08/10 09:15 788 bytes ← 기존 SPA 껍데기
08/11 20:47 1,863 bytes
08/11 21:17 4,220 bytes ← 배포 1분 뒤. 진입 콘텐츠 포함
프론트를 배포하고 1분 뒤에 Yeti가 들어와서 새 문서를 받아갔습니다. 웹마스터도구의 수집 수치도 5일 연속 0이다가 다음 날 23건으로 올라왔습니다.
그런데 아직 이릅니다
8월 17일 기준으로 다시 뽑았습니다.
| 날짜 | Yeti 요청 |
|---|---|
| 8/6~8/10 | 0~10 /일 |
| 8/11 | 72 ← 수동 제출 |
| 8/12~13 | 11 · 3 |
| 8/14~17 | 23 · 17 · 18 · 11 |
기준선(010)보다 꾸준히 높은 수준(1123/일) 을 유지합니다. 일회성으로 끝나지 않고 재방문이 이어지고 있습니다.
다만 자발적 확장은 아직입니다.
Yeti 가 긁은 고유 채널 slug : 10개 ← 제출한 10개와 동일
Yeti 가 긁은 고유 place id : 16개 ← 제출 15 + 1
제출한 URL을 반복 방문하고 있을 뿐, 사이트맵의 나머지 1만여 개로 넓히지는 않았습니다. 진짜 신호는 제출하지 않은 URL을 스스로 긁기 시작하는 것인데, 그건 아직입니다.
한 가지 정정도 해둡니다. 앞에서 "네이버는 자바스크립트를 안 본다"고 적었는데, 8월 12일 로그에 이런 게 있었습니다.
02:28:56 / 200 2,111
02:28:56 /assets/index-*.css 200 9,437
02:28:56 /assets/index-*.js 200 252,474
02:28:57 /api/categories 200 122
02:28:57 /api/channels 200 5,525
02:28:58 /api/places?bbox=... 200 15,047
번들과 CSS를 받고 API까지 호출했습니다. 최소한 이 요청에서는 SPA를 실제로 렌더링했습니다. "소극적"은 맞아도 "안 한다"는 틀렸습니다.
틀린 전략 ⑤ — "제목은 정확하기만 하면 된다"
8월 중순, 사용자가 구글 검색 결과 스크린샷을 붙이며 이렇게 물었습니다.
"'홍복' 구글 검색 시 유추카의 경우 너무 건조하고, 간략한 정보만 전달하는 타이틀이 나타남. 예시된 사례와 같이 클릭을 유도할 수 있는 제목으로 할 수는 없나요?"
당시 제목은 홍복 · 중구 중식당 추천 · 유추카였습니다. 틀린 게 하나도 없습니다. 상호, 지역, 업종, 브랜드. 그런데 옆에 뜬 블로그 제목에는 콜키지 가능·화상 중국집 같은 게 붙어 있었습니다.
정확한 것과 클릭할 이유가 있는 것은 다릅니다. 그리고 유추카에는 남들에게 없는 재료가 있었습니다 — 누가 소개했고, 그 영상이 얼마나 봤는지.
그런데 데이터를 세어보니 설계를 바꿔야 했다
먼저 "유튜버 N 채널 추천"을 넣기로 하고 분포를 봤습니다.
| 추천 채널 수 | 장소 | 비율 |
|---|---|---|
| 1개 | 9,829 | 85% |
| 2개 | 1,305 | 11% |
| 3개 이상 | 341 | 3% |
85%가 1개입니다. "유튜버 1 채널 추천"은 클릭 유인이 되기는커녕 오히려 빈약해 보입니다. 2개 이상일 때만 넣는 게 맞았습니다.
다음으로 조회수. 처음 생각은 그 장소를 언급한 영상들의 조회수 합계였습니다. 홍복으로 실측했습니다.
264,800 성시경의 먹을텐데 l 회현역 홍복 (with. 신동엽) ← 맞음
180,819 이마트 가면 이거 무조건 쟁여두세요 #shorts ← 홍복과 무관
─────────
445,619 합계
합계의 41%가 오매칭입니다. 합계를 쓰면 틀린 숫자가 검색 결과 제목에 박힙니다.
그래서 합계 대신 최다 조회 영상 1건을 쓰기로 했습니다. 오염 영향이 훨씬 작고, "26만뷰 영상에 소개"가 "44만뷰"보다 오히려 의미가 명확합니다.
여기서 사용자가 두 가지를 더 정했습니다 — "채널"이라는 말을 넣을 것, 그리고 조회수 임계값은 1만 이상. 그리고 하나를 더 지시했습니다.
"오매칭에 대한 점검해주세요"
제목에 숫자를 박기로 한 순간, 그 숫자의 오류율이 새로운 문제가 됩니다. 화면 안쪽에 있을 때는 넘어가던 오매칭이 검색 결과 제목에 나가면 그대로 드러나니까요.
결과
제목을 조건부 조립으로 바꿨습니다. 있는 재료만 쓰고, 빈약한 신호는 생략합니다.
| 조건 | 제목 |
|---|---|
| 채널 2개 이상 + 조회수 큼 | 환선굴 · 삼척시 동굴 — 2개 채널 추천 · 32만뷰 영상에 소개 |
| 채널 1개 + 조회수 큼 | 홍복 · 중구 중식당 — 26만뷰 영상에 소개 |
| 조회수 작음 | 홍복 · 중구 중식당 추천 · 유추카 (현행 유지) |
전략 ③에서 description을 덜 말하게 고쳤고, 여기서는 제목을 더 말하게 고쳤습니다. 모순처럼 보이지만 기준은 하나입니다 — 우리에게만 있는 정보는 늘리고, 남의 것을 옮긴 정보는 줄인다. 유튜버와 조회수는 우리 자산이고, 리뷰 요약은 남의 리뷰에서 나온 것입니다.
그리고 3주 동안 측정을 안 하고 있었습니다
8월 17일, 사용자가 이렇게 물었습니다.
"SEO를 위한 SSR 페이지에도 GA 집계가 되나요?"
확인했습니다.
| 페이지 | GA 태그 |
|---|---|
/about |
0건 |
/best |
0건 |
/place/{id} |
0건 |
/channel/{slug} |
0건 |
| SPA 번들 | 8건 ✅ |
안 되고 있었습니다.
원인은 구조 그 자체였습니다. GA는 프론트 코드에서 자바스크립트로 주입되는데, SSR 페이지는 백엔드가 HTML을 직접 만들고 SPA 번들을 아예 로드하지 않습니다. GA 스크립트가 실행될 일이 없습니다.
그래서 못 보고 있던 것:
- 검색으로
/place/*·/list/*·/channel/*에 직접 들어온 방문 — SEO 트래픽의 거의 전부 - 그 페이지에서 얼마나 머물렀는지, 어디로 갔는지
- SSR → SPA 전환("지도에서 보기" 클릭) 경로
3주 동안 SEO 작업을 하면서, 그 성과를 GA로는 한 번도 못 보고 있었습니다. GSC가 노출·클릭까지는 보여주지만 클릭 이후는 GA 몫인데 그게 통째로 비어 있었습니다.
규모를 재봤더니 이랬습니다.
사람 트래픽 6,166건 중 SSR 페이지 5,505건 (89%) 이 미집계
수정 자체는 작았습니다. 렌더러가 6종(place·channel·region·video·best·about)이지만 HTML 조립 함수가 공통이라 한 곳만 고치면 전부 적용됩니다.
다만 설정을 어디 둘지에서 한 번 갈렸습니다. 처음엔 서버의 기동 스크립트에 환경변수를 추가하는 방향을 봤는데, 그 파일은 저장소 템플릿과 서버 사본이 수동 동기화입니다. 새 변수를 넣으면 양쪽 다 손대야 하고, 하나를 빠뜨리면 SSR 집계만 조용히 꺼진 채로 돌아갑니다. 지금 이 작업이 필요했던 상황과 정확히 같은 실패 모양입니다.
GA4 측정 ID는 어차피 모든 페이지 HTML에 실려 나가는 공개 값이라 시크릿이 아닙니다. 그래서 운영 프로파일 설정 파일에 직접 넣었습니다. 파일 하나만 고치면 되고, 배포되는 jar 안에 들어가니 서버 스크립트를 다시 써도 안 사라집니다.
로컬·테스트에서는 빈 기본값이라 꺼져 있고, 형식이 맞지 않으면 스니펫을 통째로 안 냅니다. 그리고 스프링이 기본값 대신 실제 빈을 주입한다는 걸 컨텍스트 테스트로 고정했습니다 — 단위 테스트를 안 깨려고 넣은 기본값(집계 꺼짐)이 운영에 쓰이면 이 작업 전체가 무의미해지니까요.
부수 수확 셋
① SEO 작업이 검색 버그를 찾아냈다
장소 페이지의 "지도에서 보기"를 눌러 착지했더니, 지도에 엉뚱한 핀이 남아 있었습니다.
원인을 쫓다 보니 이랬습니다. 검색이 0건이면 지도가 카메라와 핀을 갱신하지 않아서, 직전 검색 결과가 화면에 그대로 남아 있던 겁니다. 그럼 왜 0건인가 — 여기서 검색 버그 두 개가 나왔습니다.
하나. 숙박업소를 제외하는 정규식에 스테이(팜스테이·게스트하우스)가 있었는데, 이게 스테이크의 부분 문자열이었습니다. 그래서 카테고리가 "스테이크,립"인 식당 12곳이 모든 검색에서 사라지고 있었습니다. 스테이(?!크)로 고쳤습니다. (기술 3편에서 "회"가 회식·회전초밥을 끌어오던 것과 정확히 반대 방향의 사고입니다. 한국어에서 짧은 단어는 어느 쪽으로든 사고를 냅니다.)
둘. 상호명에 요리 이름이 들어 있으면, 그 요리명이 하드게이트로 다시 걸려서 자기 자신이 걸러지고 있었습니다. "울프강 스테이크하우스"를 검색하면 '스테이크'가 요리 조건으로 잡히는데, 그 집 메뉴 토큰에는 "안심스테이크"로 들어 있어 정확히 안 맞아 0건. 상호명에 요리명이 든 411곳 중 356곳이 이 상태였습니다.
| 검색어 | 이전 → 이후 |
|---|---|
| 다사랑스테이크 | 0건 → 1건 |
| 울프강 스테이크하우스 | 0건 → 1건 |
| 성수동 스테이크 | 무관한 160건 → 3건 |
SEO 작업이 아니었으면 이 버그들을 못 찾았을 겁니다. 상호명으로 검색하는 경로를 평소엔 잘 안 쓰거든요. 새 진입점을 만들면 기존 경로의 결함이 드러난다 — 이것도 배운 것 중 하나입니다.
② 색인이 되니 다른 봇도 왔다
로그를 자주 보게 되면서 알게 된 건데, 크롤러가 Googlebot·Yeti만 있는 게 아니었습니다.
| 봇 | 2주 요청 | 성격 |
|---|---|---|
| ClaudeBot | 17,840 | AI 학습 |
| SemrushBot | 15,079 | SEO 분석 SaaS |
| Amazonbot | 14,474 | AI/검색 |
| Googlebot | 4,142 | 검색 |
| GPTBot | 1,226 | AI 학습 |
| Bytespider | 288 | 틱톡 |
SemrushBot이 Googlebot의 3.6배를 긁고 있었습니다. 그중 93%(14,041건)가 /place 페이지고, 하루 2,000~4,000건 페이스였습니다.
/place는 SSR이라 요청마다 DB 조회 + HTML 생성입니다. 2코어 4GB 서버에서 무시할 양이 아닙니다. 그리고 이건 검색 노출과 아무 관계가 없습니다 — Semrush 고객이 백링크를 조회할 때 쓰는 데이터가 될 뿐입니다.
그래서 SemrushBot과 Bytespider를 robots.txt로 차단했습니다. Semrush는 robots.txt를 191회 읽어 갔으므로 규칙을 지키는 봇이고, 이걸로 끊깁니다.
AI 크롤러는 남겼습니다. 요청 수는 더 많지만, 리퍼러에 이미 유입이 잡히고 있었거든요.
vertexaisearch 121건
chatgpt.com 2건
차단하면 부하는 줄지만 AI 검색 결과에 유추카가 인용될 가능성도 같이 사라집니다. 그 121건이 지금은 작아도, 방향이 반대인 선택이라 남겨뒀습니다.
③ 크롤러가 504를 받고 있었다
로그를 훑다가 이게 나왔습니다.
16/Aug 18:07 /channel/하이갱스-higaengs 504
16/Aug 19:23 /place/6a32eebc466c983ed989c934 504
16/Aug 19:29 /best/서울특별시/종로구 499
16/Aug 19:51 /channel/일상이여행-everyday-travel 504
16/Aug 20:18 /channel/먹보스-쭈엽이 504
두 시간에 몰려 있습니다. /channel/*은 SSR로 50~140KB를 만드는 무거운 페이지라, 서버에 부하가 걸리면 nginx 프록시 타임아웃(30초)을 넘길 수 있습니다.
크롤러에게 504는 나쁜 신호입니다. 반복되면 크롤 예산이 줄어듭니다. 어렵게 3건에서 64건으로 늘려놓은 걸 되돌리는 방향이고요. 같은 시간대에 Semrush가 2,230건을 긁고 있었으니 인과를 단정할 순 없어도 기여했을 가능성이 있습니다.
아직 안 끝난 것
정직하게 남겨둡니다.
- 네이버 자발적 크롤 확장 — 제출한 26개만 반복 방문 중입니다. 제출 안 한 URL을 스스로 긁기 시작해야 크롤 예산이 늘어난 겁니다.
- 네이버 색인 — 수집(crawl)과 색인(index)은 다릅니다. 수집은 확인됐고 색인은 아직입니다.
- CTR —
description·제목을 고쳤지만, 그게 CTR을 실제로 올렸는지는 다음 리포트를 봐야 압니다. 순위 11위라는 근본 제약은 그대로고요. - 장소 415곳의 지역 정보가 빈 문자열입니다. 주소는 멀쩡한데 시도·시군구 파싱이 누락됐어요. 어떤 지역 페이지에도 안 실리고, 지역 기반 검색에서도 빠질 가능성이 큽니다.
- GA 내부 트래픽 제외를 아직 안 걸었습니다. 측정 도구를 먼저 고쳐야 한다는 걸 기술 7편에서 배웠는데, SSR GA 누락까지 겹쳐서 두 번 미룬 셈이 됐습니다.
- 커버리지 오류 재분류 — 크롤 페이지 수는 늘고 있는데 일부가 다시 오류로 잡히고 있습니다. 원인 확인 중입니다.
이 글의 결론 — 다섯 번의 공통점
다섯 개를 다시 늘어놓으면 이렇습니다.
| # | 전략 | 무엇이 빠졌나 |
|---|---|---|
| ① | 사이트맵부터 내자 | 낼 게 있는지 안 봄 |
| ② | 전부 열자 | 열 것이 얇은지 안 봄 |
| ③ | 요약을 description에 | 그 텍스트가 어디에 놓이는지 안 봄 |
| ④ | 구글 되면 네이버도 | 네이버가 실제로 무엇을 요청했는지 안 봄 |
| ⑤ | 제목은 정확하면 됨 | 정확한 것이 클릭할 이유가 되는지 안 봄 |
전부 "확인하지 않고 상식으로 건너뛴 한 칸" 입니다. 그리고 다섯 번 다 로그나 데이터를 한 번 세어보는 것으로 잡혔습니다. curl 네 번, nginx 로그 집계 한 번, 분포 쿼리 한 번.
SEO가 특별히 어려운 분야라서 다섯 번 틀린 게 아닙니다. 검증 주기가 길어서입니다. 검색 품질은 러너를 돌리면 7초 만에 점수가 나오는데(기술 7편), 색인은 사흘에서 몇 주가 걸립니다. 그 사이에 확인하지 않은 가정 위에 다음 작업을 쌓게 되고, 틀리면 3주치를 되짚어야 합니다.
그래서 이 작업에서 제일 값을 한 습관은 "배포하고 기다리기" 대신 "로그를 열어 지금 무슨 요청이 오는지 세기" 였습니다.
- Yeti가 사이트맵 인덱스만 5번 읽고 하위를 0번 요청했다는 것
- 루트가 Yeti 요청 1위인데 본문이 15자라는 것
- 프론트 배포 1분 뒤에 4,220바이트를 받아갔다는 것
- 사람 트래픽 89%가 GA에 안 잡히고 있었다는 것
넷 다 서치 콘솔에는 안 나옵니다. 대시보드는 결과를 며칠 뒤에 요약해서 보여주고, 로그는 지금 일어나는 일을 그대로 보여줍니다. 검증 주기가 긴 작업일수록 주기가 짧은 관측 수단을 따로 확보해야 한다는 게, 이번 3주에서 가장 크게 남은 것입니다.
나머지 정리는 이렇습니다.
- CTR이 정상인데 방문이 없으면, 문구가 아니라 유통 문제입니다. 이 구분을 먼저 해야 엉뚱한 걸 고치지 않습니다.
- SPA의
try_files폴백은 없는 경로를 전부 200 + HTML로 만듭니다. robots.txt가 "없음"이 아니라 "HTML을 반환"하는 상태가 더 나쁩니다. - 순서가 전부입니다. 페이지 → 사이트맵 → 등록.
- "열려 있느냐"와 "색인하느냐"는 다른 질문입니다. 하나로 처리하려다 공유 기능을 2,700곳에서 깨뜨릴 뻔했습니다.
- 사이트맵은 페이지와 같은 코드에서 나와야 합니다. 다른 쿼리로 쓰면 언젠가 반드시 어긋납니다.
- 남의 집계를 우리 평점으로 마크업하지 않습니다. 별 하나 얻으려다 도메인 전체가 페널티를 받습니다.
- 화면용 텍스트가 자동으로 검색용이 되지 않습니다. 그리고 내부 식별자는 언젠가 사용자 화면으로 샙니다.
- 고치는 수정이 새 문제를 만듭니다. 루트에 콘텐츠를 넣자 soft 404 범위가 넓어졌고, canonical로 막아야 했습니다.
- 그리고 새 진입점을 만들면 기존 경로의 결함이 드러납니다. 덕분에 검색 버그 두 개를 찾았습니다.
기술편은 여기까지입니다. 네이버 색인과 CTR 결과는 몇 주 뒤에, 숫자가 나오면 또 적겠습니다.