bobob.app
운영 기록2026-06-1810분 읽기

Vercel, sitemap, canonical을 다시 정리한 기록

bobob.app을 다시 손보면서 apex redirect, canonical host, sitemap 제출 목록, feed, Search Console URL 검사를 한 번에 분리해 정리한 운영 기록입니다.

퇴근 전 일더미 줄이기 바로 하기

도메인 문제는 코드만 봐서는 끝나지 않는다

앱 코드에 308 redirect를 넣었는데도 실제 apex 도메인은 307을 반환할 수 있다. 이건 좀 억울했다. 코드가 맞는데 왜 응답이 다르지? 이유는 플랫폼의 도메인 redirect가 앱보다 먼저 응답하기 때문이다. 이럴 때는 Next.js 설정만 보는 것으로는 부족하고 Vercel 프로젝트 도메인 설정을 직접 확인해야 한다.

bobob.app은 apex에서 www로 가는 redirect를 308로 고정했다. 그 다음 sitemap을 줄였다. 이전에는 여러 locale sitemap이 제출되어 있었지만 실제 Blog/Play 콘텐츠는 아직 unprefixed 경로가 중심이다. 그래서 Search Console 제출 목록에서 오래된 child sitemap을 제거하고 `/sitemap.xml`만 남겼다.

정리 순서

순서바꾼 표면사람이 다시 확인한 이유
1apex `bobob.app`을 `www.bobob.app`으로 308 redirect앱 라우터보다 플랫폼 도메인 응답이 먼저 나올 수 있어서
2`/sitemap.xml`은 sitemap index로 유지정적 파일로 굳혀 두면 실제 앱 라우트와 금방 어긋나서
3`/sitemaps/en`에 검증된 핵심 URL만 포함아직 source-locale Blog/Play가 제출 표면의 중심이라서
4오래된 다국어 child sitemap 제출 내역 제거실제 콘텐츠가 없는 locale URL을 넓게 보이게 하지 않으려고
5핵심 URL을 Search Console에서 개별 확인제출, 발견, 색인이 서로 다른 단계라서

이 작업은 화려하지 않지만 중요하다. 검색엔진이 볼 URL과 사용자가 보는 URL이 다르면 작은 사이트는 금방 신뢰를 잃는다. 콘텐츠를 늘리기 전에 canonical과 sitemap을 작게 맞추는 이유가 여기에 있다. 재미는 없지만, 안 하면 계속 발목 잡힌다!

이 순서를 적어 두지 않으면 나중에 같은 실패를 다시 본다. 예를 들어 `/sitemap.xml`이 200이어도 그 안의 child sitemap이 너무 넓으면 제출 표면은 여전히 흐리다. `/sitemaps/en`이 의도한 숫자를 내도 production 배포가 실패하면 live crawler는 이전 숫자를 본다. Search Console에서 "제출 성공"이 떠도 URL 검사에서 canonical이 다른 host를 가리키면 아직 끝난 것이 아니다. 숫자 하나가 맞는지보다 어느 계층의 숫자인지를 먼저 본다.

헷갈렸던 계층

이번에 가장 자주 헷갈린 것은 같은 URL 문제처럼 보여도 실제 계층이 다르다는 점이었다. 그래서 화면을 보기 전에 아래처럼 나눠 적었다.

계층내가 확인한 것틀렸을 때 생기는 증상
도메인 설정apex가 www로 영구 redirect 되는지앱 코드가 맞아도 307이나 다른 host가 먼저 응답함
앱 route`/sitemap.xml`, `/sitemaps/en`, `/blog`가 final 200인지로컬에서는 열리는데 production sitemap에서 빠짐
콘텐츠 정책대표 글과 archive/noindex 후보가 나뉘었는지짧은 메모가 sitemap/feed/search에 대량으로 섞임
외부 도구Search Console과 Bing이 최신 sitemap 숫자를 보는지제출은 됐지만 예전 넓은 URL 수가 계속 남음

이 표를 만들고 나니 “왜 아직 안 잡히지?”라는 질문이 조금 줄었다. 하나의 문제처럼 보였던 것이 사실은 도메인, 앱, 콘텐츠, 외부 관찰 네 가지였기 때문이다. 작은 사이트에서는 이 계층을 섞으면 하루 종일 같은 버튼만 다시 누르게 된다.

숫자는 세 줄로 나눠 적는다

sitemap을 줄일 때 가장 위험한 착각은 source 숫자와 live 숫자와 외부 도구 숫자를 한 줄에 적는 것이다. 같은 `/sitemaps/en`을 이야기하는 것 같지만 실제로는 서로 다른 시점이다.

숫자 줄어디에서 보는가내가 적는 말아직 증명하지 못한 것
source target로컬 빌드, 콘텐츠 registry, 하네스이번 커밋이 의도한 URL 수와 feed item 수production에 올라갔는지
live target`https://www.bobob.app/sitemaps/en`, feed, robots, OpenSearch현재 방문자와 crawler가 실제로 받는 수Search Console이 다시 읽었는지
external observationSearch Console URL 검사, sitemap report, Bing/Naver 도구외부 도구가 마지막으로 본 수와 상태최종 색인 반영 여부

이 세 줄을 섞으면 좋은 변경도 잘못된 근거가 된다. source에서는 대표 URL이 76개로 줄었는데 live가 아직 74개를 내보내면, 나는 아직 "공개됐다"고 쓰면 안 된다. 반대로 live가 맞아도 Search Console이 며칠 전 넓은 sitemap을 기억하고 있으면, 나는 "외부 도구가 따라왔다"고 쓰면 안 된다. 작은 운영 기록에서 이 구분은 글의 신뢰도와도 연결된다. 독자가 보는 페이지가 운영자의 실제 판단을 담고 있어야 하고, 운영자가 못 본 단계는 못 봤다고 적어야 한다.

이번에는 sitemap도 다시 줄였다

표면유지할 것뺄 것
`/sitemaps/en`대표 Blog, category hub, Play, search, tools archive짧은 제작 메모 대량
feed대표 Blog와 Playnoindex 후보 글
Blog 목록pillar와 제작 로그 중심비슷한 짧은 변경 메모
개별 URL접근 가능하게 유지적극 제출하지 않음

이렇게 나누면 Search Console에서 보는 숫자도 더 읽기 쉬워진다. live sitemap URL 수와 discovered pages가 맞는지 보고, 대표 글 몇 개를 URL 검사 대상으로 잡는다. 짧은 메모까지 전부 제출하면 발견 수가 늘어도 어떤 페이지가 중요한지 흐려진다.

줄인 sitemap은 작아 보이지만, 운영자에게는 오히려 더 많은 정보를 준다. 150개가 넘는 URL이 섞여 있을 때는 어디가 문제인지 찾기 어렵다. 70여 개의 대표 URL로 줄이면 발견 수, feed item 수, noindex 후보, category hub, Play 결과 링크를 서로 비교할 수 있다. 숫자가 작아져야 변화가 보인다.

줄이는 과정에서 중요한 것은 약한 글을 없던 일로 만드는 것이 아니다. 짧은 제작 메모는 개별 URL로 남겨도 된다. 다만 `/blog`, category hub, search, feed, sitemap에서 먼저 보이지 않게 해야 한다. 사용자는 archive 후보 글을 우연히 직접 열 수는 있지만, 첫 화면에서 그것들을 대표 콘텐츠처럼 보게 되면 사이트의 중심이 흐려진다. 그래서 운영 판단은 "삭제"보다 "어디에 보일 것인가"에 가깝다.

배포 상태는 콘텐츠 품질과 따로 본다

Vercel 배포가 막힌 날에는 특히 조심해야 한다. 로컬에서 build와 rendered-content가 모두 통과해도, 배포 상태가 failure이면 public sitemap은 그대로다. 이럴 때 새 글을 썼다는 사실과 방문자가 볼 수 있다는 사실을 분리해야 한다.

상태적어도 되는 말적으면 안 되는 말
source check 통과로컬 소스와 빌드는 의도한 표면을 만든다production도 같은 표면을 제공한다
deployment failure배포 상태가 막혀 live 반영이 보류됐다Search Console이 새 숫자를 볼 준비가 됐다
live follow-up 통과현재 live discovery surface는 응답한다live가 최신 source target과 일치한다
external tool 대기며칠 뒤 URL 검사와 discovered pages를 다시 본다제출 직후 색인이 끝났다

이 표는 나중에 나를 막아 세우는 장치다. 급하면 "로컬에서 되니까 된 것"처럼 말하고 싶어진다. 하지만 사이트 운영에서는 그 말이 가장 위험하다. crawler가 보는 것은 로컬 파일이 아니라 배포된 URL이고, 외부 도구가 보는 것은 배포 직후의 의도도 아니라 자기들이 마지막으로 읽은 상태다.

배포 설정 파일은 DevTools보다 먼저 본다

DevTools Network 탭은 브라우저가 받은 결과를 잘 보여준다. 하지만 왜 그 결과가 나왔는지는 다른 파일에 숨어 있을 때가 많다. production에서 환경변수가 빠졌는지, Docker Compose 포트가 엉뚱한 서비스로 열렸는지, YAML 들여쓰기가 한 단계 밀렸는지는 DevTools 화면만 보고 끝내기 어렵다. 배포 설정은 브라우저 응답보다 먼저 깨지는 표면이다.

그래서 배포 전에는 `deployment config checker`, `docker compose validator`, `env parser validator` 같은 질문을 하나의 흐름으로 묶어 본다. Bobob에서는 새 얇은 페이지를 만들지 않고 YAML Validator, ENV Parser Validator, JSON Formatter를 이어서 쓴다. 한 파일을 예쁘게 보는 것보다 "배포 후 어떤 URL과 설정을 다시 볼 것인가"까지 남기는 쪽이 더 오래 간다.

설정 조각DevTools에서 바로 안 보이는 부분먼저 확인할 도구
Docker Composeservice, image, build path, port, volume, dependency가 서로 맞는지YAML Validator의 Compose diagnostics
`.env`중복 key, 빈 값, quote 오류, hosting 대시보드로 옮기며 빠진 값ENV Parser Validator의 duplicate/malformed/empty warning
YAML config들여쓰기 때문에 실제 object shape가 달라졌는지YAML Validator의 parsed preview와 formatted YAML
JSON config fragmentruntime 설정 조각이 복사 전에 깨진 JSON인지JSON Formatter의 parse error 위치와 structure summary
public URL설정 수정 뒤 canonical host가 final 200으로 응답하는지HTTP Status Checker와 DNS Lookup

이 표가 필요한 이유는 단순하다. 배포 설정이 틀리면 DevTools에는 "요청 실패", "404", "redirect 이상함", "환경값 없음" 같은 결과만 남는다. 원인은 `.env`, YAML, 플랫폼 설정, DNS, response header 중 하나일 수 있다. 그래서 확인 순서를 파일 점검, 공개 URL 점검, Search Console 관찰로 나눠야 한다. 같은 실패를 계속 새로고침하지 않으려면, 브라우저 화면과 배포 설정 파일을 같은 증거로 취급하면 안 된다.

5xx 에러는 public 응답과 로그를 분리한다

production에서 `500 Internal Server Error`, `502 Bad Gateway`, `503 Service Unavailable`, `504 Gateway Timeout`이 뜨면 화면은 단순하다. 그냥 서버가 터진 것처럼 보인다. 하지만 운영 기록으로 남길 때는 먼저 public URL이 어떤 응답을 실제로 내는지, CDN이나 edge가 만든 응답인지, 앱 서버까지 도달한 뒤 실패한 것인지 나눠야 한다.

그래서 `http 500 checker`, `502 bad gateway checker`, `504 gateway timeout checker`, `cloudflare 522 checker`, `vercel 500 error` 같은 검색도 HTTP Status Checker와 배포 설정 검증 흐름으로 넣었다. HTTP Status Checker는 public status, final URL, redirect chain, content-type, cache-control, response header, elapsed time을 남기는 쪽이다. 원인 로그를 대신 보지는 않는다. 그 다음 ENV Parser Validator, YAML Validator, JSON Formatter로 환경값, Compose/YAML shape, 설정 JSON을 확인하고, 플랫폼 로그나 upstream 상태를 따로 본다.

화면에 보인 증상먼저 남길 public 증거다음에 확인할 계층
500 Internal Server Errorfinal URL, status, content-type, trace 가능 headerapp log, missing env, runtime exception
502 Bad Gatewayedge/CDN header, upstream hint, redirect 이후 hostreverse proxy, function region, upstream health
503 Service Unavailableretry-after, cache-control, platform maintenance hintdeploy 상태, quota, service readiness
504 Gateway Timeoutelapsed time, gateway header, final hostfunction timeout, upstream latency, external API
Cloudflare 522/524Cloudflare status/header, origin hostorigin reachability, firewall, long-running request

이 구분을 안 하면 "사이트가 죽었다"만 남고 다음 사람이 어디를 봐야 할지 모른다. public checker는 장애의 바깥 표면을 고정하고, 배포 설정 검증은 파일과 환경값을 고정하고, 플랫폼 로그는 실제 예외를 고정한다. 세 개가 같은 방향을 가리킬 때만 원인을 좁힐 수 있다.

확인할 명령과 화면

로컬에서는 sitemap XML과 feed item 수를 본다. 라이브에서는 canonical host가 최종 200을 주는지, apex가 www로 영구 redirect 되는지, 제출 URL이 noindex 없이 열리는지 본다. Search Console에서는 제출 성공과 색인 상태를 분리한다. 이 네 가지를 섞지 않는 것만으로도 운영 판단이 훨씬 차분해진다.

내가 실제로 남기는 확인 묶음은 보통 이렇다.

  • - 로컬 build에서 Blog/Play 정적 경로가 빠지지 않는지 확인한다.
  • - `/sitemaps/en`의 URL 수와 feed item 수가 의도한 대표 표면과 맞는지 본다.
  • - archive/noindex 후보가 Blog 목록, category hub, search, feed에 섞이지 않는지 본다.
  • - canonical URL이 `https://www.bobob.app` 기준으로 맞는지 확인한다.
  • - Search Console의 제출, 발견, 색인 문구를 같은 말로 바꾸지 않는다.

이 체크는 멋있는 기능이 아니다. 하지만 지금 bobob.app 같은 작은 Blog + Play 사이트에서는 이 체크가 콘텐츠 품질의 일부다. 좋은 글을 써도 제출 표면이 흐트러지면 처음 온 사람과 crawler 모두 사이트의 중심을 읽기 어렵다.

확인 결과를 문서에 남길 때도 같은 원칙을 쓴다. 명령이 통과했으면 명령 이름과 범위를 적고, 브라우저로 본 것은 URL과 페이지 수를 적고, 못 본 것은 못 본 이유를 적는다. "아마 됐을 것"이라는 문장은 다음 작업자에게 아무 도움이 되지 않는다. 운영 기록은 멋진 회고보다 증거가 더 중요하다.

바로 해보기

정시 퇴근까지 버티기는 이 운영 작업과 닮아 있다. 하루의 모든 요청을 다 받는 대신 지금 처리할 것과 넘길 것을 구분해야 퇴근할 수 있다. 사이트 운영도 마찬가지다.

줄인 뒤에 더 봐야 하는 숫자

sitemap을 줄였다고 바로 문제가 해결되는 것은 아니다. 줄인 뒤에는 Google이 새 제출 표면을 어떻게 읽는지 봐야 한다. `/sitemaps/en`의 URL 수가 로컬과 라이브에서 같은지, Search Console의 discovered pages가 예전 넓은 숫자에 머물러 있는지, 대표 글 URL 검사 결과가 canonical과 indexability를 제대로 보여주는지 확인해야 한다. 제출은 신호이고 색인은 결과다.

그래서 운영 기록에는 항상 “서비스가 있다”, “이 환경에서 접근된다”, “Search Console이 읽었다”를 나눠 적는다. 세 가지가 섞이면 같은 실패를 계속 반복한다. live 페이지가 200이어도 Search Console이 오래된 sitemap을 보고 있을 수 있고, sitemap이 맞아도 대표 글이 `noindex`면 제출 의미가 없다.

다음 공개 점검 전 체크

다음 공개 점검에서는 기술 상태보다 표면 상태를 먼저 본다. Blog 목록에서 짧은 메모가 대량으로 보이지 않는지, category hub가 빈 목록처럼 보이지 않는지, 대표 글이 충분한 문장과 판단 근거를 갖고 있는지 확인한다. 홈 첫 화면에서 세 개의 pillar가 보이는지, `/blog`가 긴 archive처럼 시작하지 않는지, Play 결과에서 관련 글로 자연스럽게 돌아오는지도 같이 본다. sitemap 숫자는 그 다음이다.

마지막에는 문서에도 같은 단어를 남긴다. “제출됨”은 제출됨이고, “발견됨”은 발견됨이고, “색인됨”은 색인됨이다. 이 단어를 정확하게 남기면 다음 작업자가 와도 어디서 기다려야 하는지 안다. 운영 기록은 거창한 회고가 아니라 다음 사람이 같은 실수를 덜 하게 만드는 표지판에 가깝다.

내가 다음에 이 글을 다시 열면 먼저 세 군데를 볼 것이다. 첫째, `/blog`와 category hub에서 이 운영 글이 짧은 archive 글보다 먼저 보이는가. 둘째, `/sitemaps/en`과 feed가 대표 글 중심으로 같은 방향을 가리키는가. 셋째, live deployment 상태와 외부 도구 관찰이 소스 목표를 따라왔는가. 셋 중 하나라도 비어 있으면 아직 "정리 완료"라고 쓰지 않는다. 그 정도의 조심성이 작은 사이트의 운영 품질을 만든다.

읽고 나서 해볼 것

Vercel, sitemap, canonical을 다시 정리한 기록 - bobob.app | bobob.app