사이트맵 오류 해결부터 동 단위 분리까지, 주차장 사이트 SEO 실전기
사이트맵 URL 267개가 전부 거부당하고, 고친 사이트맵 대신 예전 404가 캐시에 남아 있었어요. 전국 주차장 요금 사이트를 올리며 겪은 사이트맵 오류 해결 과정과 행정구역 정리, 동 단위 분리, 제목 수정까지 정리했습니다.
서치콘솔에 들어갔다가 “어머나” 소리가 나왔어요. 주차장 사이트의 사이트맵에 “URL이 허용되지 않음” 오류가 267개 찍혀 있었거든요. 페이지 전부였어요. 이 글은 그 사이트맵 오류 해결 과정부터, 사이트를 직접 써보고 불편해서 구조를 뜯어고친 과정까지의 기록이에요.
사이트 공장 시리즈 4편이에요. 1~3편에서 공장을 짓고 서버를 세우고 API를 연결했다면, 이번엔 그 공장의 첫 결과물 이야기예요. 공공데이터로 전국 주차장 요금을 보여주는 사이트인데, 만드는 것보다 고치는 데 훨씬 많은 시간이 들었어요. 저처럼 데이터 기반 사이트를 대량 페이지로 만들어보시는 분이라면, 같은 함정 몇 개는 피해 가실 수 있을 거예요.
버림값 노출 0의 범인, 존재하지 않던 sitemap.xml
사실 사이트맵 문제는 주차장 사이트 전에 버림값에서 먼저 터졌어요. 대형폐기물 수수료 사이트인 버림값은 만들어둔 지 꽤 됐는데도 서치콘솔 노출이 0이었어요. 사이트맵 제출에 문제가 있었다는 건 알았는데, 원인을 제대로 파본 건 공장 작업을 시작하면서였어요.
원인은 허무했어요. 서치콘솔에 제출한 주소는 /sitemap.xml인데, 그 파일이 서버에 아예 없었어요. 버림값은 Astro로 만들었는데, Astro 공식 사이트맵 통합 문서를 보면 이 통합이 만드는 건 sitemap-index.xml이고, 이 파일이 번호가 붙은 사이트맵 파일들을 가리키는 구조예요. 저는 관성적으로 sitemap.xml을 제출했고, nginx는 없는 파일 대신 404 HTML을 돌려줬던 거예요. 구글 입장에서는 사이트맵을 달라는데 웹페이지가 온 셈이죠.
/sitemap.xml 경로로도 사이트맵이 나오게 고쳐서 올렸는데, 서버에서 직접 요청하면 정상인데 바깥에서 열면 여전히 실패했어요. 이번엔 Cloudflare였어요. 고치기 전의 404 응답을 캐시에 들고 있었던 거예요. 응답 헤더를 보니 캐시 상태가 HIT로 찍혀 있었고요. 대시보드에서 sitemap.xml, sitemap-index.xml, robots.txt 세 개를 캐시 퍼지하고 다시 제출하니 그제야 “성공”이 떴어요. 발견된 페이지 183개, 실제 페이지 수와 정확히 같았어요.

이때 배운 게 두 가지예요. 사이트맵은 제출한 주소에 실제로 파일이 있는지부터 확인해야 해요. 그리고 CDN을 쓰고 있다면, 고친 다음에 캐시를 비우지 않으면 고친 게 아니에요. 그래서 공장 배포 과정에는 배포 직후 사이트맵 관련 캐시를 퍼지하고 공개 주소로 다시 확인하는 단계를 넣었어요.
혹시 지금 사이트맵 때문에 막혀 있다면, 사이트맵 오류 해결의 첫 단계로 터미널에서 이 정도는 직접 확인해보는 걸 추천해요. 제가 버림값을 고칠 때 실제로 본 항목들이에요.
# 1. 제출한 주소가 200이고 XML로 오는지
curl -I https://내도메인/sitemap.xml
# 2. 사이트맵 안 URL 개수가 실제 페이지 수와 맞는지
curl -s https://내도메인/sitemap-0.xml | grep -c "<loc>"
# 3. robots.txt에 사이트맵 경로가 들어 있는지
curl -s https://내도메인/robots.txt
첫 번째에서 응답 형식이 HTML로 나오면 파일이 없는 거고, 응답 헤더에 캐시 HIT가 보이는데 내용이 옛날 거면 CDN 캐시 문제예요. 저는 이 세 줄로 원인을 둘 다 잡았어요.
사이트맵 오류 해결의 핵심, 로컬 주소가 박힌 URL 267개
버림값을 고치고 안심했는데, 주차장 사이트에서 더 큰 게 터졌어요. 서치콘솔에 들어가 보니 사이트맵은 읽었지만 오류가 있다고 떴고, 오류 예시를 펼치니 이런 주소가 나왔어요. http://127.0.0.1:4321/, http://127.0.0.1:4321/search/. 로컬 개발 서버 주소예요. 사이트맵 안의 URL 267개가 전부 제 컴퓨터를 가리키고 있었으니, 구글이 “이 위치의 사이트맵에 사용할 수 없는 URL”이라며 전부 거부한 거죠.
원인을 파보니 코드가 조용히 거짓말을 하고 있었어요. 사이트 주소를 담는 환경변수가 비어 있으면, 설정 파일과 사이트맵, robots.txt, canonical 태그를 만드는 코드가 전부 알아서 localhost로 채워 넣고 있었어요. 거기에 예시 설정 파일에는 127.0.0.1 주소가 그대로 들어 있었고요. 맥에서 빌드할 때 그 값이 결과물에 박힌 거예요. 타임라인도 확인했어요. 구글이 사이트맵을 받아간 게 15시 20분, 정상 주소로 다시 배포한 게 16시 9분이었어요. 고치기 전 파일을 읽어간 거죠.
여기서 고친 방향이 이 사이트맵 오류 해결의 핵심이에요. 값이 없을 때 조용히 대체하지 말고, 빌드를 실패시킨다. 이제 사이트 주소가 비어 있거나, https가 아니거나, localhost나 127.x면 빌드 자체가 멈춰요. 그리고 검수 단계에서 사이트맵 안의 모든 주소, robots.txt의 Sitemap 줄, 모든 페이지의 canonical과 og:url이 실제 도메인으로 시작하는지 전수 검사해요. 하나라도 다르면 배포가 안 돼요. 매일 아침 상태 점검에도 같은 검사를 넣었고, 서치콘솔 API로 사이트맵 오류 수와 구글이 마지막으로 받아간 시각을 조회해서 오류가 있으면 텔레그램으로 알리게 했어요. 오늘처럼 제가 직접 들어가서 발견하기 전에 공장이 먼저 알려주게요. 재제출해도 서치콘솔 화면의 오류 267은 바로 안 사라져요. 구글이 새 사이트맵을 다시 받아갈 때까지 옛 파일 기준 결과가 그대로 보이거든요. 그래서 점검 알림도 구글이 받아간 시각이 마지막 배포보다 이전이면 “구 파일 기준 오류, 재수집 대기 중”이라고 구분해서 오게 했어요. 그렇게 기다린 끝에 구글이 새 사이트맵을 받아가자 오류가 사라졌어요. 사이트맵 오류 해결은 고치는 순간이 아니라 구글이 다시 읽어가는 순간에 끝나는 셈이에요.
써보니 불편했다, 표만 있는 페이지
사이트맵을 고치고 나서 제가 직접 사이트를 써봤어요. 광주 동구 페이지를 열었더니 주차장 이름과 기본요금이 표로 쭉 나오더라고요. 그런데 이렇게만 나오니까 뭔가 불편했어요. 그래서 이 주차장이 어디 있는데? 주차장 요금을 찾는 사람은 결국 위치와 가는 길이 궁금할 텐데, 표에서 멈춰버리는 거예요.
그래서 주차장마다 눌러서 펼치면 위치가 보이게 하고 싶었어요. 다만 지도 API를 붙이는 대신 임베드 같은 가벼운 방식으로요. 결과적으로 행을 펼치면 주소, 운영시간, 요금 상세가 나오고, 카카오맵에서 보기와 길찾기 버튼, 네이버지도 링크가 붙게 했어요. 카카오맵은 링크 형식만 맞추면 키 없이 해당 위치로 바로 열려서, 폰에서 누르면 앱으로 넘어가요. 지도 미리보기는 키가 필요 없는 오픈스트리트맵 임베드를 쓰되, 펼칠 때만 불러오게 했어요. 주차장이 200개 넘는 페이지에서 지도를 미리 다 띄우면 페이지가 엄청 무거워지거든요.

펼친 내용의 텍스트는 처음부터 HTML에 넣어두고 접어만 뒀어요. 그래야 검색엔진이 주소나 운영시간 같은 내용을 읽을 수 있으니까요. 자동 검수는 페이지가 열리는지만 보지, 쓸 만한지는 못 봐요. 이 불편함은 사람이 써봐야만 보이는 거였어요.
행정구역이 바뀌어 있었다, 전남광주통합특별시
다음은 페이지 품질 검사였어요. 지역만 바뀌고 내용이 거의 같은 페이지가 많으면 구글이 대량 생산된 저품질 페이지로 볼 수 있어서, 본문 유사도를 전수로 비교해봤어요. 267페이지에서 90% 이상 비슷한 쌍은 0건이었어요. 지역마다 주차장 이름과 요금이 달라서 겹치지 않았던 거죠. 대신 행이 3개 미만인 얇은 페이지가 17개 나왔고, 예상 못 한 문제가 하나 같이 걸려 나왔어요.
같은 지역이 여러 페이지로 쪼개져 있었어요. 데이터에 전남광주통합특별시 566행, 전라남도 229행, 광주광역시 214행이 섞여 있었거든요. 동구는 광주광역시 동구 214행짜리 페이지와 전남광주통합특별시 동구 1행짜리 페이지로 나뉘어 있었고요. “전븍특별자치도” 같은 오타도 있었어요. 확인해보니 광주광역시와 전라남도가 합쳐진 전남광주통합특별시가 세계일보 보도에 나온 일정대로 2026년 7월 1일 공식 출범한 상태였어요. 기관마다 데이터를 갱신한 시점이 달라서 옛 표기와 새 표기가 섞여 있었던 거예요.
표기는 전남광주통합특별시로 통일했어요. 다만 사람들은 한동안 “광주 동구 주차장”으로 검색할 테니, 제목과 본문에 “전남광주통합특별시 동구(구 광주광역시 동구)“처럼 옛 이름을 같이 넣었어요. 이미 있던 옛 주소 13개는 301 리디렉션으로 새 주소에 연결했고요. 띄어쓰기가 빠진 주소 때문에 엉뚱한 지역으로 분류된 행도 바로잡았어요. 표기를 정리하고 나니 얇은 페이지 17개 중 5개가 다른 페이지에 합쳐져 사라졌고, 남은 12개만 검색에서 빼되 상위 지역 페이지의 링크는 유지했어요. 아예 지우지 않은 건 그 지역을 찾는 사람이 있을 수 있어서예요. 대신 사이트맵에서는 빼고, 검색 결과에는 안 나오게 했어요. 얇은 페이지는 지우기 전에 데이터 오류부터 의심해야 해요. 이번에도 얇은 페이지의 상당수가 표기 문제로 쪼개진 것이었으니까요.
검사하다 문장 버그도 하나 나왔어요. 요약문이 “기본요금은 400원–2,000원다”로 끝나고 있었어요. “원이다”가 맞죠. 받침에 따라 조사를 바꾸는 처리가 없어서 생긴 거라, 생성되는 모든 문구가 조사 처리 함수를 거치게 고쳤어요. 소개 페이지의 “주차요금 조회은”도 같이 고쳐졌고요.
무한스크롤 대신 동 단위로 쪼갰다
다음 문제는 속도였어요. 공장 서버에서 성능을 쟀더니 제주시 페이지가 79점으로 기준에 못 미쳤어요. 주차장 1,544곳을 한 페이지에 다 그리고 있었거든요. 이걸 보고 제가 물었어요. 결과가 많으면 페이징이나 무한스크롤을 넣었어야 하는 거 아냐?
방향은 맞았는데, 방법은 다른 게 나았어요. 무한스크롤은 검색엔진 입장에서 함정이에요. 구글 공식 문서에 따르면 구글 크롤러는 버튼을 따라가지 않고, 현재 페이지 내용을 바꾸는 자바스크립트도 실행하지 않아요. 스크롤해야 나오는 주차장은 색인에서 빠질 수 있다는 뜻이죠. 페이징은 색인은 되지만 제목이 같은 페이지가 이어질 뿐이라 검색으로 들어올 이유가 약하고요.
그래서 주차장이 200곳 넘는 시군구 10곳을 읍면동 페이지로 한 단계 더 나눴어요. 제주시 페이지는 허브가 되고, 그 아래에 연동, 노형동, 이도이동 같은 페이지가 생기는 구조예요. 사람들은 “제주시 주차장”보다 “노형동 주차장”처럼 동네 이름으로도 많이 찾으니까, 페이지가 늘어나는 게 아니라 검색 수요에 맞는 페이지가 생기는 거예요. 허브에는 1,544곳 목록 대신 요약과 저렴한 곳, 읍면동별 링크를 넣었어요. 가장 큰 하위 페이지도 99점이 나왔어요. 다만 처음 쪼갰을 때는 주소에 동 이름이 없는 주차장 477곳이 “제주시 기타”라는 페이지에 몰려 있었어요. 기타 주차장을 검색하는 사람은 없으니 의미 없는 묶음이었죠. 이 주차장들은 좌표로 어느 동에 있는지 판정해서 제자리를 찾아줬고, 좌표조차 없는 9곳만 기타에 남았어요. 이 과정은 3편에서 다룬 카카오 좌표 변환 이야기예요.
| 단계 | HTML 페이지 | 사이트맵 URL | 검색 제외 |
|---|---|---|---|
| 처음 | 267 | 267 | 0 |
| 표기 통일, 얇은 페이지 정리 | 260 | 248 | 12 |
| 읍면동 분리 | 487 | 426 | 61 |

허브를 만들다 보니 하나가 더 보였어요. 제주시 주차장 1,544곳 중 1,428곳이 무료였어요. 그러니 “저렴한 곳 10”을 뽑으면 전부 0원이라 의미가 없었죠. 그래서 무료주차장을 따로 섹션으로 묶고, 유료 중에 저렴한 곳을 보여주게 바꿨어요. “제주시 무료주차장”은 그 자체로 사람들이 많이 찾는 검색어이기도 하고요. 그래서 한때 제목도 “제주시 무료주차장 1,428곳”으로 시작하게 바꿨다가, 바로 아래에서 말할 이유로 한 번 더 고쳤어요. 주차장이 전부 무료인 지역은 요약문에서 “모두 무료주차장”이라고 한 줄로 정리했고요.
사람들은 “주차요금”이 아니라 “주차장”을 검색했다
마지막으로 바꾼 건 제목이에요. 공장 리서처로 지역 이름에 단어를 붙인 키워드의 검색량을 직접 조회해봤더니, 결과가 분명했어요. “○○구 주차장” 형태는 샘플 중 32%에서 검색량이 잡혔는데, “○○구 주차요금”은 0%였어요. 사이트 이름에 “주차요금”이 들어가 있어서 제목도 그쪽에 맞춰져 있었는데, 정작 사람들이 제일 많이 치는 단어를 정면으로 안 잡고 있었던 거예요.
그래서 지역 페이지 제목을 “광산구 주차장 · 무료주차장 36곳 · 공영 요금 정리 · 전남광주통합특별시”처럼 바꿨어요. 사람들이 실제로 치는 단어를 제목 맨 앞에 둔다. 구글이 긴 제목 뒤를 잘라내도 중요한 부분은 남아요. 동구나 중구처럼 같은 이름이 여러 시도에 있어서 끝에 시도를 붙였고, 데이터에 있는 정보만 쓰게 했어요. 민영 주차장이 없는 지역은 “공영 요금”, 전부 무료면 “요금 정리”는 빼는 식이에요.
이제 이 사이트는 한동안 손대지 않을 거예요. 제목을 바꾸면 구글이 다시 읽고 평가하는 데 시간이 걸리는데, 그 사이에 또 바꾸면 무엇이 효과였는지 알 수 없거든요. 참고로 광고가 붙은 프로덕션의 성능 점수는 광고 없는 미리보기보다 한참 낮게 나와요. 그래도 광고 로딩은 건드리지 않았어요. 구글 순위에 쓰이는 건 실험실 점수가 아니라 실제 방문자 데이터라, 방문자가 쌓인 뒤에 판단하기로 했어요. 성능 점수 자체도 측정할 때마다 크게 흔들렸어요. 같은 페이지가 한 번은 64점, 다음엔 99점이 나오기도 했으니까요. 그래서 검수 기준을 한 번 측정이 아니라 세 번 측정한 중앙값으로 바꿨어요. 점수가 동전 던지기처럼 나오는데 그걸로 배포를 막거나 통과시키면 검수가 의미가 없거든요.
이번 사이트맵 오류 해결 과정을 돌아보면, 큰 기술보다 확인 습관의 문제였어요. 제출한 주소에 파일이 있는지, 캐시를 비웠는지, 사이트맵 안의 주소가 진짜 도메인인지. 다음 5편은 이 모든 과정에서 AI한테 개발을 맡기며 배운 것들이에요. AI가 완료했다고 보고한 단계가 실제로는 비어 있었던 이야기도 거기서 풀게요.