← 전체 글

버튼 하나로 커스텀 도메인 붙이기 — 'DNS 전파는 못 줄인다'를 전제로 설계한 3단계 자동화

· 6 분 분량
블로그 관리 화면에서 커스텀 도메인을 구매·연결하는 확인 모달
구매 버튼 하나 뒤에 DNS 위임·인증서·검색엔진 등록이 자동으로 이어집니다.

NOTE

사용자가 “구매” 버튼을 누르면 도메인을 사서 블로그에 붙여주는 기능입니다. 버튼 한 번이면 될 줄 알았는데, 그 뒤엔 우리가 코드로 못 줄이는 대기 구간이 숨어 있었습니다.

회사에서 운영하는 블로그 플랫폼은 각 블로그를 myblog.example.com 같은 서브도메인으로 서빙합니다. 여기에 사용자가 myblog.shop 같은 자기 소유 커스텀 도메인을 붙일 수 있게 하는 기능을 만들었습니다. 도메인 구매(Dynadot)부터 DNS·인증서·검색엔진 등록까지 버튼 한 번에 자동으로 끝나는 게 목표였습니다.

문제는 이걸 테스트로 네 번 사보는 동안 네 번 다 다르게 실패했다는 겁니다. 24시간 동안 엉뚱한 페이지가 뜨기도 했고, 서버는 멀쩡한데 사용자만 못 들어가기도 했고, “완료”라는데 사이트가 안 열리기도 했습니다. 성공한 것도 있었지만 그건 재현이 안 되는, 그냥 운이었습니다.

원인은 결국 하나로 모였습니다. 우리가 통제할 수 없는 대기 구간을 무시하고, 즉시 처리하려 했던 것. 이 글은 그 대기를 전제로 받아들이고 파이프라인을 다시 세운 기록입니다.


1. 문제 — 왜 “즉시 연결”이 불가능한가

도메인을 붙이는 일은 이사와 비슷합니다. 짐만 옮긴다고 끝이 아니라, 주소 이전 신고를 하고, 집 열쇠(인증서)를 받고, 우체국·검색엔진에 새 주소를 알려야 편지가 제대로 옵니다.

이 중 주소 이전 신고(DNS 위임) 가 핵심입니다. “이 도메인은 이제 Cloudflare 네임서버를 보세요”라고 등록기관에 알리는 건데, 이게 전 세계 DNS에 퍼지는 데 시간이 걸립니다. 실측하면 이랬습니다.

도메인 A 위임 전파 2분
도메인 B 위임 전파 13분

같은 코드로 산 도메인인데 6배 넘게 차이 납니다. 등록기관·레지스트리·인터넷 사정이라 우리가 코드로 줄일 수 있는 값이 아닙니다.

그래서 진짜 질문은 “어떻게 빠르게 연결할까”가 아니라 “이 대기를 어떻게 감출까” 였습니다. 답은 단순합니다.

  • 몇 초 안에 끝나는 것 → 사용자가 기다리는 동안 즉시 처리 (동기)
  • 기다려야 하는 것 → 워커에게 넘기고, 사용자는 화면을 떠나도 되게 (비동기)

파이프라인이 동기 1단계 + 워커 2단계로 갈라진 이유가 이겁니다. 이제 각 단계를 봅니다.


2. 1단계 — 즉시 세팅 (동기, 몇 초)

사용자가 버튼을 누르고 응답을 기다리는 구간입니다. 그래서 여기서는 되돌릴 수 없거나 몇 초 안에 끝나는 것만 합니다.

  1. 도메인 실구매 — 가격이 검색 때와 다르면 멈추고 다시 물어본 뒤 결제(Dynadot)
  2. A 레코드 등록 — “이 이름 = 우리 서버”를 Cloudflare에 적음
  3. 위임(set_ns) — “이 도메인은 Cloudflare를 보세요”를 등록기관에 신고
  4. 검색엔진용 TXT 게시 — 구글이 나중에 소유 확인할 때 볼 쪽지만 붙여둠
  5. caddy·빌드 작업을 큐에 던지고 응답 반환

순서가 사고로 정해진 게 두 군데 있습니다.

A 레코드가 위임보다 먼저여야 합니다. 반대로 하면, 위임이 먼저 퍼진 그 틈에 Cloudflare가 “그런 주소 없음(NXDOMAIN)“을 권위 있게 답합니다. 그 응답이 사용자의 통신사 DNS에 캐시되면, 도메인이 살아난 뒤에도 그 사용자만 최대 1시간 접속하지 못합니다. 답(A 레코드)을 먼저 깔아두면 위임이 퍼지는 순간 이미 답이 있어 이 틈이 사라집니다.

set_ns는 “완료됐다”는 응답을 믿지 않습니다. 구매 직후 몇 초는 등록기관이 도메인을 완전히 준비하기 전이라, API가 success를 돌려주면서 실제로는 아무것도 안 바꾸는 경우가 있었습니다. 그래서 신고 후 실제 네임서버를 다시 읽어 기대값과 대조하고, 다르면 재시도합니다.

GSC는 TXT 쪽지만 붙이고, 소유 확인은 여기서 하지 않습니다. 이 시점엔 위임이 안 퍼져 구글이 쪽지를 볼 수 없어 반드시 실패하기 때문입니다. 왜 확인을 미루는지는 4장에서 다룹니다.

여기까지 몇 초. 사용자 화면엔 “연결 중”이 뜨고, 나머지는 워커에게 넘어갑니다.


3. 2단계 — 위임 대기와 문 열기 (워커)

여기가 핵심입니다. 워커는 위임이 퍼진 걸 직접 확인한 뒤에만 도메인을 엽니다.

caddy-reload 잡
├─ ① 위임 게이트 ── 공개 DNS(1.1.1.1/8.8.8.8)로 "이 도메인 네임서버가 Cloudflare인가?" 확인
│ 아직이면 → 잡을 실패시켜 재시도 (설정 안 씀)
│ 확인되면 → 계속
├─ ② Caddy 설정 기록 → 인증서 자동 발급 → HTTPS 열림
└─ ③ 뒷정리 (구 주소 301 · 엣지 캐시 purge · HTML 캐시 규칙)

①번 게이트가 이 설계의 심장입니다. Caddy는 설정을 받는 순간 그 도메인의 인증서를 발급하려 합니다. 그런데 위임이 안 퍼진 상태에서 발급을 시도하면, 인증서 발급기가 도메인의 상위 영역을 잘못 지목해 그 오판을 캐시합니다. 이 캐시는 재시작 전까지 안 지워져서, 한 번 이렇게 되면 도메인이 영구히 열리지 않습니다. 실제로 이 문제로 한 도메인이 재시작하기 전까지 계속 먹통이었습니다.

게이트는 이 사고를 원천 차단합니다. “확인되기 전엔 설정을 안 쓴다”는 규칙 하나로요. 그리고 부수 효과로 다운타임도 사라집니다. 설정 안에는 “구 주소 → 새 주소” 리다이렉트가 함께 들어 있는데, 예전엔 새 도메인이 죽어 있는 동안 구 주소까지 죽은 곳으로 리다이렉트됐습니다. 게이트가 막고 있으면 그동안 구 주소(myblog.example.com)가 계속 정상 서빙됩니다.

재시도 예산은 넉넉하게 잡았습니다. 1분 간격으로 최대 6시간. 위임 전파가 아무리 느려도 그 안에 끝나면 다음 시도에서 바로 통과하고, 대기 중인 잡은 비용이 없으니 예산을 크게 둬도 부담이 없습니다.


4. 3단계 — 검색엔진 등록을 워커로 (백필)

이 글에서 가장 손이 많이 간 부분입니다. 구글 검색등록(GSC)은 소유 확인 → 사이트맵 제출 순인데, 소유 확인이 까다롭습니다.

확인 방법은 DNS에 붙여둔 TXT 쪽지를 구글이 읽는 것입니다. 그런데 구글이 쪽지를 읽으려면 위임이 다 퍼져 있어야 합니다. 안 퍼졌으면 구글은 “그런 도메인 몰라요”라는 답만 받고 소유 확인을 거부합니다. 즉 구매 직후엔 GSC 등록이 무조건 실패합니다. 실제로 테스트한 도메인 다섯 건이 전부 구매 시점 확인에서 실패했습니다.

원래는 이걸 구매 직후에 잠깐(18초) 재시도했습니다. 그런데 위임 전파가 최소 2분이라 18초 안에 끝나는 경우는 없었고, 결국 사용자만 18초를 헛기다리는 코드였습니다. 그래서 이 시도를 없앴습니다. 대신 일을 이렇게 쪼갰습니다.

  • 1단계(구매 직후): TXT 쪽지만 붙인다. 확인·제출은 안 한다.
  • 워커(위임 확인 후): 그때 소유 확인 → 사이트맵 제출을 두 번에 나눠 백필한다.

두 번으로 나눈 이유가 있습니다.

시점역할
위임 확인 직후정상 경로. 인증서 발급 뒤 이어서 GSC까지 등록
30분마다(스케줄)안전망. 정상 경로가 놓친 도메인을 주기적으로 줍기

안전망이 왜 필요하냐면, 위임 확인 직후 등록도 실패할 수 있기 때문입니다. 워커가 그때 재시작 중이었거나, 구글 API가 잠깐 먹통이었거나. 실제로 한 도메인이 이런 이유로 몇 시간 방치된 적이 있었고, 그때는 제가 손으로 복구 스크립트를 돌려야 했습니다. 30분 스케줄이 있으면 사람 없이 자동으로 잡힙니다.

핵심은 “한 번 더”가 아니라 “다른 순간에” 입니다. 같은 시점에 여러 번 시도하면 다 똑같은 이유로 실패합니다. 위임 확인 직후 한 번, 그 뒤 주기적으로 한 번 — 각자 다른 실패를 담당합니다.


5. 화면은 어떻게 진행을 따라가나

사용자 화면은 “연결 중” 모달을 띄우고 5초마다 상태를 물어봅니다. 이때 상태를 DB에 저장해두고 읽는 게 아니라, 매번 DNS·HTTPS를 실제로 조회해 판정합니다. 저장값은 워커가 죽거나 전파가 흔들리면 현실과 어긋나기 때문입니다.

한 가지 더, 완료 전에는 새 도메인을 링크로 보여주지 않습니다. 브라우저는 화면의 링크를 미리 DNS 조회하는데(prefetch), 아직 위임 전인 새 주소를 링크로 걸면 그 조회가 “없음”을 통신사 DNS에 캐시시킵니다. 그러면 도메인이 살아난 뒤에도 그 사용자만 한동안 못 들어갑니다. 그래서 연결 중에는 아직 살아 있는 구 주소를 대표로 보여주고, 새 주소는 텍스트로만 노출합니다.


6. 결과 — 수동 3회에서 0회로

첫 도메인과 마지막 도메인을 비교하면 이렇게 바뀌었습니다.

항목첫 시도마지막 시도
소요38분11분
수동 개입3회0회
서버 재시작필요불필요
구 주소죽은 곳으로 리다이렉트계속 정상

가장 크게 바꾼 건 사실 코드가 아니었습니다. 등록기관 계정의 기본 네임서버를 Cloudflare로 지정한 설정 한 줄이었습니다.

첫 도메인이 24시간 동안 엉뚱한 파킹 페이지를 띄운 원인이 여기 있었습니다. 도메인이 처음엔 등록기관 기본 네임서버(파킹)로 태어났다가 나중에 Cloudflare로 바뀌는데, 그 짧은 파킹 상태가 통신사 DNS에 캐시되면 최대 24시간 남았습니다. 기본값을 처음부터 Cloudflare로 두니 파킹 상태 자체가 생기지 않았습니다.

지금까지의 코드 수정들이 “나쁜 상태가 생긴 뒤 노출을 줄이는” 것이었다면, 이 한 줄은 나쁜 상태를 애초에 만들지 않는 것이었습니다. 결이 달랐고, 효과도 가장 컸습니다.

물론 못 없애는 것도 남습니다. “등록 → 레지스트리 반영” 사이의 최대 1시간. 이 구간에 누가 그 도메인을 조회하면 “없음”이 잠시 캐시됩니다. DNS엔 캐시를 강제로 지우는 수단이 없어서, 이건 줄일 수 없습니다. 대신 그 구간에 아무도 조회하지 않게 하는 게 최선이고, 그래서 화면에서 새 주소를 미리 노출하지 않는 겁니다.


같은 코드로 네 번 사서 네 번 다 달랐던 이유는, 매번 위임 전파 시간이 달랐기 때문입니다. 그 대기를 없애려 한 게 실수였고, 전제로 받아들이자 설계가 정리됐습니다.

정리하면 원칙은 한 줄입니다. 확인되기 전엔 아무것도 하지 않고, 확인되면 자동으로 다 하고, 놓친 건 주기적으로 줍는다. 위임 게이트가 첫 문장을, 워커의 뒷정리가 둘째를, 30분 스케줄이 셋째를 담당합니다.