도메인 만료로 블로그가 사라졌다 — 새 도메인 이전 SEO·애드센스 체크리스트

블로그 분석을 하다가 이상한 점을 발견했다. curl로 홈페이지를 요청하니 TLS 인증서 오류가 났고, 인증서 검사를 끄고 다시 요청하니 “만료된 도메인 · Expired Domain” 이라는 안내 페이지가 돌아왔다. 그 페이지에는 <meta name="robots" content="noindex, nofollow">까지 붙어 있었다. 검색엔진에는 “이 사이트는 색인하지 마라”는 신호를, 방문자에게는 보안 경고를, 애드센스에는 광고를 낼 곳이 없다는 결과를 동시에 주고 있었던 셈이다.

이 글은 도메인 만료를 진단하고, 복구 대신 새 도메인(bottlehs.dev)으로 이전하면서 검색 노출과 애드센스를 최대한 빨리 되살리기 위해 한 일을 순서대로 정리한 기록이다. 같은 일을 겪는 사람이 그대로 따라 할 수 있도록 명령어와 코드를 함께 남긴다.

증상: “내 브라우저에서는 멀쩡한데?”

가장 헷갈렸던 부분은 운영자 본인의 브라우저에서는 사이트가 정상으로 보였다는 점이다. 이 블로그는 Gatsby의 gatsby-plugin-offline을 쓰고 있어서 서비스 워커가 페이지를 캐시한다. 한 번 방문한 브라우저는 네트워크가 실패해도 캐시된 앱 셸과 페이지를 보여 줄 수 있다. 그래서 “사이트가 죽었다”는 사실을 운영자가 가장 늦게 알게 되는 구조가 된다.

도메인 상태를 확인할 때는 다음 중 하나로 봐야 한다.

  • 시크릿 창(서비스 워커와 캐시가 없는 상태)
  • 휴대폰 LTE 등 다른 네트워크
  • 터미널의 curl, dig, whois

진단: dig와 whois로 원인 좁히기

1. 네임서버부터 확인한다

dig +short NS bottlehs.com
# expired-domain-1.ns.eliv-dns.kr.
# expired-domain-2.ns.eliv-dns.kr.

네임서버 이름에 expired-domain이 들어 있다. 등록기관이 만료된 도메인의 네임서버를 자기 주차 서버로 바꿔 놓은 것이다. 로컬 DNS 캐시 문제가 아닌지 확인하려면 공용 DNS와 레지스트리 원본까지 물어본다.

# 공용 DNS 여러 곳
dig @8.8.8.8 +short NS bottlehs.com
dig @1.1.1.1 +short NS bottlehs.com

# .com 레지스트리(권한 서버)에 직접 질의 — 여기서도 같으면 캐시 문제가 아니다
dig @a.gtld-servers.net NS bottlehs.com +norec

2. whois로 만료일을 확인한다

whois bottlehs.com | grep -iE 'expir|name server|updated'

이때 레지스트리 만료일과 등록기관 만료일이 다르게 나올 수 있다. 레지스트리는 만료 직후 자동으로 1년을 연장해 두고(Auto-Renew Grace), 등록기관이 유예 기간 안에 그 연장을 취소하거나 확정하는 방식으로 운영되는 경우가 많기 때문이다. 레지스트리 만료일이 미래로 나온다고 해서 “아직 내 도메인”이라고 판단하면 안 된다. 등록기관의 만료일과 네임서버 상태를 기준으로 본다.

3. 호스팅 쪽에서도 확인한다

Vercel에 연결된 도메인이라면 CLI로 상태를 볼 수 있다.

vercel domains inspect bottlehs.com

“This Domain is not configured properly”와 함께 현재 네임서버에 ✘ 표시가 나오면, 배포는 정상이고 도메인이 Vercel을 가리키지 않는 것이 문제라는 뜻이다.

복구할까, 새로 살까

도메인이 만료된 뒤에는 대략 다음 단계를 거친다. 기간과 비용은 TLD와 등록기관마다 다르므로 반드시 사용 중인 등록기관의 정책을 확인해야 한다.

단계 대략적인 기간 되살리는 방법 비용
갱신 유예(Grace) 만료 후 수십 일 이내 일반 갱신 보통 갱신비 수준
복구 유예(Redemption) 이후 약 30일 복구 신청 갱신비 + 복구 수수료(수배 이상일 수 있음)
삭제 대기(Pending Delete) 약 5일 불가 —
재등록 가능 이후 신규 등록(선점 경쟁) 신규 등록비

이번에는 이미 복구 유예 단계여서 복구 비용이 신규 도메인보다 훨씬 비쌌다. 판단 기준은 다음과 같이 정리했다.

기준 복구가 유리 새 도메인이 유리
외부 백링크 많고 가치가 크다 거의 없다
브랜드 인지도 도메인 자체가 브랜드다 이름만 유지하면 된다
복구 비용 감당 가능 신규 대비 과도함
이미 떨어진 색인 아직 대부분 남아 있음 이미 상당 부분 빠짐

결국 같은 이름에 TLD만 바꾼 bottlehs.dev를 새로 샀다. .dev는 HSTS preload 목록에 포함된 TLD라 HTTPS가 강제되어, 개발 블로그로서 보안 측면의 장점도 있다.

이전 1단계: 코드에서 도메인을 한 곳으로 모은다

도메인을 바꾸려고 코드를 검색해 보니 bottlehs.com이 여러 파일에 흩어져 있었다.

grep -rn "bottlehs\.com" --exclude-dir={node_modules,public,.cache,.git} .
  • gatsby-meta-config.js의 siteUrl
  • gatsby-config.js의 canonical 플러그인 설정, RSS 피드 link
  • vercel.json의 리다이렉트 규칙
  • 레이아웃 푸터 링크
  • 글 본문 4개에 박힌 절대 URL 내부 링크

설정은 siteUrl 하나만 바꾸면 되도록 모았다. 환경 변수로도 덮어쓸 수 있게 해 두면 프리뷰 환경에서도 유용하다.

// gatsby-meta-config.js
// 사이트 도메인은 이 값 하나로 관리한다 (canonical·sitemap·RSS·OG·JSON-LD 공통)
const siteUrl = (process.env.SITE_URL || `https://www.bottlehs.dev`).replace(/\/$/, ``)

module.exports = {
  siteUrl,
  // ...
}
// gatsby-config.js — 하드코딩 대신 metaConfig 참조
{
  resolve: `gatsby-plugin-canonical-urls`,
  options: { siteUrl: metaConfig.siteUrl },
},

본문의 내부 링크는 상대 경로로 바꿨다. 다음에 도메인이 또 바뀌어도 링크가 깨지지 않는다.

<!-- 나쁜 예: 도메인이 바뀌면 전부 죽은 링크가 된다 -->
[ES6 화살표 함수](https://www.bottlehs.com/javascript/es6-arrow-function/)

<!-- 좋은 예: 도메인과 무관하게 동작한다 -->
[ES6 화살표 함수](/javascript/es6-arrow-function/)

이전 2단계: 호스팅에 새 도메인 연결

Vercel 프로젝트에 bottlehs.dev와 www.bottlehs.dev를 추가하고, 루트 도메인은 www로 리다이렉트되게 설정했다. DNS는 등록기관(또는 Cloudflare 등 DNS 서비스)에서 Vercel이 안내하는 레코드를 넣으면 된다. 인증서는 Vercel이 자동으로 발급한다.

연결이 끝나면 반드시 실제 응답으로 확인한다.

curl -sI https://bottlehs.dev | grep -iE '^(HTTP|location)'
# HTTP/2 308
# location: https://www.bottlehs.dev/

curl -s -o /dev/null -w "%{http_code}\n" https://www.bottlehs.dev/
# 200

옛 도메인은 호스팅 프로젝트에서 제거했다. 더 이상 내 소유가 아닌 도메인을 붙들고 있을 이유가 없다.

이전 3단계: 검색엔진 — 301 없이 다시 색인받기

가장 아쉬운 부분이 여기다. 구글 서치콘솔에는 주소 변경(Change of Address) 도구가 있지만, 이 도구는 옛 사이트에서 새 사이트로 301 리다이렉트가 걸려 있어야 쓸 수 있다. 옛 도메인을 잃었으니 리다이렉트를 걸 수 없고, 따라서 기존 색인의 신호를 넘겨받을 수도 없다. 새 사이트로 처음부터 색인받는 것이 현실적인 선택이다.

그래서 할 수 있는 일은 “새 사이트를 최대한 빨리, 깔끔하게 크롤링되게 만드는 것”이다.

  1. 서치콘솔에 새 도메인 속성을 추가하고 DNS TXT 레코드로 소유권을 확인한다.
  2. 네이버 서치어드바이저에도 새로 등록한다. 옛 도메인의 인증 메타 태그는 새 도메인에서 쓸 수 없으므로 새로 발급받는다.
  3. sitemap.xml을 제출하고, robots.txt에 Sitemap: 줄을 명시한다.
  4. 사이트맵에서 얇은 페이지(글이 한두 개뿐인 태그 페이지 등)를 빼고, 글마다 lastmod를 넣는다.

4번은 새 도메인일수록 중요하다. 신규 사이트는 크롤러가 한 번에 가져가는 양이 많지 않은데, 이 블로그는 사이트맵 URL 927개 중 739개가 태그 페이지였다. 크롤러가 태그 페이지부터 돌다가 정작 글을 늦게 가져갈 수 있는 구조였다. 정리 과정은 Gatsby 블로그 GEO·AEO 적용기에 따로 정리했다.

이전 4단계: 애드센스 사이트 재등록

애드센스는 계정 단위와 사이트 단위를 나눠서 생각하면 정리가 쉽다.

항목 단위 도메인 변경 시
게시자 ID(ca-pub-…) 계정 그대로
광고 단위(슬롯 ID) 계정 그대로 재사용
ads.txt 내용 계정 같은 내용을 새 도메인 루트에 제공
사이트 승인 사이트 새로 추가하고 심사받아야 함

진행 순서는 다음과 같다.

  1. 새 도메인에서 https://새도메인/ads.txt가 200으로 열리는지 확인한다.
  2. 애드센스 → 사이트 → 사이트 추가에서 새 도메인을 등록한다.
  3. 소유권 확인은 <head>의 애드센스 코드 스니펫이나 google-adsense-account 메타 태그로 한다.
  4. 검토를 요청한다. 심사는 며칠에서 몇 주까지 걸릴 수 있다.
  5. 목록에서 옛 도메인을 삭제하고, 승인된 사이트에서만 광고가 게재되도록 설정을 켜 둔다. 옛 도메인은 이제 남의 것이 될 수 있기 때문이다.

심사 중 빈 광고 칸 숨기기

승인 전에는 수동 광고 단위가 빈 영역으로만 남는다. 이 블로그는 글마다 광고 영역이 세 개라 레이아웃이 어색해졌다. 그래서 스크립트와 소유권 메타 태그는 유지하고, 광고 단위만 환경 변수로 끄는 방식을 택했다.

// src/constants/adsense.js
// 심사 기간에는 배포 환경에서 GATSBY_ADSENSE_UNITS=off, 승인 후 변수를 지우면 다시 노출
export const ADSENSE_UNITS_ENABLED = process.env.GATSBY_ADSENSE_UNITS !== "off"
// src/components/adsense.js
if (!ADSENSE_UNITS_ENABLED || !adSlot) {
  return null
}

Gatsby에서 GATSBY_ 접두사가 붙은 환경 변수는 빌드 결과물(브라우저 코드)에 포함된다. 비밀값이 아니므로 호스팅 서비스에서도 “민감 정보”가 아닌 일반 변수로 등록해야 한다. 실제로 Vercel에서 민감 정보로 등록하려다 거부당했고, 일반 변수로 다시 등록했다.

놓치기 쉬운 나머지 연동

서비스 해야 할 일 메모
Disqus 댓글 Trusted Domains에 새 도메인 추가 댓글이 URL이 아니라 identifier(글 slug)로 연결되어 있으면 기존 댓글이 그대로 이어진다
GA4 웹 스트림 URL 수정 측정 ID는 그대로
RSS 구독자 피드 주소 변경 안내 옛 도메인에서 리다이렉트할 수 없으므로 공지 외에는 방법이 없다
소셜·프로필 링크 GitHub, 이력서, 명함 등 사람이 직접 바꿔야 하는 곳이 의외로 많다

재발 방지 체크리스트

이번 일의 근본 원인은 기술이 아니라 운영이다. 다음을 해 두었다.

  1. 자동 갱신 + 결제 수단 점검: 카드 유효기간이 지나면 자동 갱신도 실패한다.
  2. 여러 해 등록: 중요한 도메인은 한 번에 여러 해를 등록해 둔다.
  3. 등록기관 알림 메일 확인: 스팸함으로 빠지지 않도록 발신 주소를 허용 목록에 넣는다.
  4. 외부 모니터링: 서비스 워커가 가려 주는 문제는 사람이 알아채기 어렵다. 외부에서 주기적으로 확인한다.

모니터링은 거창할 필요 없다. 네임서버와 HTTP 상태만 확인하는 스크립트를 크론이나 CI 스케줄로 돌려도 충분하다.

#!/usr/bin/env bash
# domain-check.sh — 네임서버가 바뀌었거나 사이트가 200이 아니면 실패로 종료
DOMAIN="bottlehs.dev"
EXPECTED_NS="cloudflare.com"

NS=$(dig +short NS "$DOMAIN")
CODE=$(curl -s -o /dev/null -w '%{http_code}' "https://www.$DOMAIN/")

if ! echo "$NS" | grep -q "$EXPECTED_NS"; then
  echo "네임서버 이상: $NS"; exit 1
fi
if [ "$CODE" != "200" ]; then
  echo "HTTP 상태 이상: $CODE"; exit 1
fi
echo "정상"

정리: 도메인 이전 순서 한눈에 보기

  1. 시크릿 창과 dig·whois로 실제 상태를 확인한다.
  2. 복구 비용과 백링크 가치를 비교해 복구 또는 새 도메인을 결정한다.
  3. 코드의 도메인을 한 곳(siteUrl)으로 모으고, 본문 내부 링크는 상대 경로로 바꾼다.
  4. 호스팅에 새 도메인을 연결하고 실제 응답(308/200)을 확인한다.
  5. 서치콘솔·네이버에 새로 등록하고, 정리된 사이트맵을 제출한다.
  6. 애드센스에 사이트를 추가해 심사받고, 심사 중에는 빈 광고 칸을 숨긴다.
  7. 댓글·분석·구독 등 주변 연동을 옮긴다.
  8. 자동 갱신과 외부 모니터링으로 재발을 막는다.

FAQ

Q: 레지스트리 whois에는 만료일이 내년으로 나오는데, 왜 도메인이 만료된 건가요?
A: 레지스트리가 만료 시점에 자동으로 1년을 연장해 두고, 등록기관이 유예 기간 동안 그 연장을 확정하거나 취소하는 구조이기 때문이다. 등록기관 whois의 만료일과 네임서버가 실제 상태를 더 정확히 보여 준다.

Q: 옛 도메인에서 새 도메인으로 리다이렉트를 못 걸면 검색 순위는 어떻게 되나요?
A: 기존 색인 신호를 넘겨받을 수 없어서 새 사이트로 다시 평가받게 된다. 사이트맵 제출, 얇은 페이지 정리, 구조화 데이터 적용처럼 크롤링과 이해를 돕는 작업으로 회복 속도를 높이는 것이 현실적인 대응이다.

Q: 애드센스 광고 단위는 새로 만들어야 하나요?
A: 아니다. 광고 단위와 게시자 ID는 계정에 속하므로 그대로 쓸 수 있다. 새로 해야 하는 것은 사이트 추가와 심사뿐이다.

Q: 심사 중에 광고 코드를 아예 빼 두는 게 낫지 않나요?
A: 소유권 확인에 쓰이는 <head>의 애드센스 스크립트나 메타 태그는 남겨 두어야 한다. 빈 칸으로 보이는 수동 광고 단위만 숨기면 심사와 사용자 경험을 둘 다 챙길 수 있다.

Q: 서비스 워커 때문에 문제를 늦게 알았다면, 오프라인 플러그인을 빼야 하나요?
A: 꼭 그럴 필요는 없다. 오프라인 캐시는 방문자 경험에 도움이 된다. 다만 운영자는 캐시가 없는 환경에서 사이트를 확인해야 하고, 외부 모니터링을 두는 것이 더 근본적인 해결책이다.


Written by Jeon Byung Hun 개발을 즐기는 bottlehs - Engineer, MS, AI, FE, BE, OS, IOT, Blockchain, 설계, 테스트