SEO 사이트맵 전면 재건과 홈 내부링크 강화
목차
오늘 오후 6시간이 어떻게 흘렀냐면, 처음엔 콘텐츠 사이트맵에 lastmod 하나 추가하는 작업으로 시작했다가 어느새 GSC 거부 대응, 502 크래시 핫픽스, 홈 내부링크 구조 재편까지 번져버렸다. 이런 흐름이 생기는 이유는 항상 비슷하다. 하나를 손대면 숨어있던 다른 문제가 드러나는 것. 오늘이 딱 그 케이스였다.
사이트맵이 GSC에서 조용히 거부당하고 있었다
발단은 콘텐츠 사이트맵이 Google Search Console에서 처리되지 않는다는 확인이었다. 원인은 파일 크기였다. sitemap-content.xml 하나에 URL을 다 때려넣다 보니 50MB 한도를 초과한 것이다. GSC는 이 경우 조용히 거부한다. 에러 메시지도 모호하고, 실제로 처리됐는지는 한참 지나야 알 수 있어서 눈치채는 게 늦어지는 구조라 더 짜증스럽다.
대응은 청크 분할이었다. sitemap-content-[page].xml.ts 동적 라우팅으로 페이지네이션을 걸고, 인덱스 역할의 sitemap-content.xml.ts가 각 청크를 가리키도록 바꿨다. 같이 손댄 파일들을 정리하면 이렇다.
sitemap-content-[page].xml.ts- 청크 동적 라우팅 신규 생성sitemap-content.xml.ts- 인덱스 역할로 재편sitemap.xml.ts- 콘텐츠 사이트맵 인덱스 포함sitemap-items.ts- 공통 아이템 분리blindbox-db.ts- blindbox 사이트 동일 패턴 적용robots.txt- sitemap과 sitemap-content 두 줄 명시
robots.txt에 두 줄 다 넣는 건 belt-and-suspenders 방식이다. 사이트맵 인덱스가 있으면 robots 한 줄로 충분하다는 의견도 있지만, GSC에서 직접 제출할 때 명시적으로 보이는 편이 운영하기 편하고 실수도 줄어든다. 그리고 blindbox 사이트가 형제 dex 사이트들 중에 이 패턴이 빠진 유일한 케이스였는데, 일관성 차원에서 이번에 통일했다. GSC 직접 제출도 병행해서 크롤 트리거를 걸었다.
lastmod 값 선택 - updated_at은 함정이다
사이트맵에 lastmod를 추가하는 건 단순해 보이지만, 어떤 필드를 쓰느냐가 핵심이다. 처음 시도는 updated_at이었는데, 이 컬럼이 매일 자동으로 갱신되는 값이었다. 콘텐츠가 실제로 바뀌지 않았는데 lastmod가 매일 업데이트되면 크롤러한테 거짓말을 하는 셈이다.
Google은 이걸 누적해서 신뢰도 점수로 반영한다. lastmod가 자꾸 바뀌는데 실제 페이지 내용이 그대로면, 결국 이 사이트맵의 힌트를 무시하기 시작한다. 크롤 예산 낭비라는 판단을 내리고 재크롤 빈도가 오히려 줄어드는 역효과가 난다.
결론으로 created_at을 선택했다. 콘텐츠가 생성된 시점을 lastmod로 고정하는 보수적인 접근인데, daily-bumped 되는 값보다 훨씬 정직하다.
// 피해야 할 패턴 - 매일 갱신되는 updated_at
lastmod: row.updated_at
// 선택한 패턴 - 실제 생성 시점으로 고정
lastmod: row.created_at
vtuber-db.ts와 blindbox-db.ts 양쪽 다 동일하게 적용했다. 그리고 이 결정의 근거를 CLAUDE.md에 남겼다. 나중에 "왜 updated_at 안 썼지?" 하면서 되돌리는 상황을 막기 위해서다. 배포 갭 교훈도 같이 기록했다. lastmod 로직을 바꾸고 빌드 없이 배포하면 아무 의미가 없다는 것. 정적 사이트는 빌드가 배포의 일부라는 당연한 사실인데, 급하게 핫픽스할 때 빼먹기 쉬운 단계다.
홈에서 리그 페이지로, 그리고 멤버 키워드까지
사이트맵 작업이 일단락된 뒤 다음으로 건드린 게 고립 페이지 문제였다. /league 페이지들이 홈에서 연결이 전혀 없는 상태였다. 사이트맵에는 있지만 내부링크가 없으면, 크롤러 입장에서는 사이트맵 힌트에만 의존해야 하고 PageRank 신호도 흐르지 않는다. server.mjs에 홈에서 리그 목록을 노출하는 코드를 추가해서 연결을 열었다.
작업 중에 언어 키가 빠진 것도 함께 잡았다. 홈의 새 블록에서 popular_leagues 키를 참조하는데 i18n 파일에 해당 항목이 없어서 영어 텍스트가 그대로 노출되는 English leak 상태였다. ui_i18n.json에 14개 지원 언어 전체에 키를 추가했다.
홈에 트렌딩 멤버 내부링크 섹션도 이어서 추가했다. 목적은 골든존, 즉 홈 화면 상단 가시 영역에 멤버 키워드 페이지로 가는 링크를 노출시켜 재크롤을 유도하고 검색 순위 신호를 강화하는 것이다. 멤버 상세 페이지들이 키워드 트래픽 잠재력이 있는데, 홈에서 연결이 약하면 크롤 우선순위가 밀린다. 내부링크를 통해 크롤 예산을 이 페이지들 쪽으로 자연스럽게 당기는 접근이다. 작업 후 진단도 기록했는데, kpopdex의 lastmod 패턴이 vtuber 사이트와 달리 정상 반영되고 있다는 게 확인됐다. 이 차이가 왜 생기는지는 추가 모니터링이 필요하다.
502 크래시, 예약어 함정
이 오후에서 가장 당황스러웠던 순간이 하나 있었다. server.mjs에 sitemap lastmod 구현 중 mod라는 SQL alias를 쓴 것이 문제였다. Node.js에서 이 alias가 코드로 그대로 올라오는 구조였는데, mod가 예약어라 파싱 단계에서 문법 오류가 났다. 서버가 크래시하면서 502를 뱉는 상황인데, 처음엔 원인이 바로 안 잡혔다. 어디서 터졌는지 로그를 뒤지다가 alias 이름이 문제라는 걸 파악하는 데 시간이 걸렸다.
-- 502 유발 - mod는 JS 예약어
SELECT created_at AS mod FROM table_name
-- 수정 - lm으로 변경
SELECT created_at AS lm FROM table_name
SQL alias가 JavaScript 변수명으로 그대로 올라오는 ORM 없는 구조에서는 alias 이름에 JS 예약어가 들어오는지 체크하는 습관이 필요하다. mod, class, delete, return 같은 것들. 이 케이스를 CLAUDE.md에 mod-reserved-word trap으로 박아뒀다.
서버 환경 변화와 배포 함정 기록
이날 서버 RAM이 1GB에서 2GB로 업그레이드됐다. Astro 빌드가 메모리를 꽤 먹는 편인데, 1GB 환경에서는 빌드 중 가끔 프로세스가 죽는 상황이 있었다. 이게 해소될 것으로 기대된다. 인프라 변경은 발생 즉시 docs에 기록하는 편인데, 나중에 "서버 스펙이 어떻게 됐더라"를 찾는 시간을 줄이기 위해서다.
배포 함정도 하나 발견해서 기록했다. .git 디렉토리 내 objects 파일이 root 소유로 잠겨있어서 배포 시 git 명령이 실패하는 케이스가 40번 서버에서 있었다. 이 상태에서 배포 스크립트를 돌리면 중간에 조용히 멈추거나 예상과 다르게 처리되는데, 원인을 모르면 한참 헤매게 된다. chown으로 소유권을 정리해서 해결했고, 이후 같은 삽질이 반복되지 않도록 문서에 남겼다.
이날 docs 커밋 수가 코드 커밋과 비슷한 수준인 이유가 여기 있다. 오후 내내 크고 작은 함정을 여러 개 밟았고, 각각을 해결한 직후 맥락이 살아있을 때 바로 기록하는 게 가장 정확하다. 나중에 기억으로 재구성하면 디테일이 빠진다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.