코드 자체는 거의 바꾸지 않았습니다. 바뀐 것은 코드를 둘러싼 연결입니다 — 도메인을 누가 관리하는지, 화면 파일을 어디서 내보내는지, 로그인이 어느 DB와 어느 구글 클라이언트를 거치는지, 어떤 브랜치가 어느 주소에 나가는지. 하나만 어긋나도 "사이트는 뜨는데 로그인이 안 된다", "테스트 코드가 운영 DB를 건드린다" 같은 일이 생깁니다. 그래서 작업 내내 던진 질문은 하나였습니다. 이 설정은 정식 쪽인가, 테스트 쪽인가?
01출발점 — 무엇이 어디에 있었나
코드 개인 사이트 저장소(trotbonbang)의 digiboardgame/ 폴더 — 사이트 코드와 게임 코드가 한 저장소 호스팅 Cloudflare Pages 프로젝트 하나 → board.trotbonbang.com (public/_redirects로 SPA 폴백) DB·로그인 Supabase 프로젝트 하나 + Google Cloud 프로젝트 하나 — 테스트와 운영의 구분 없음 연결 개인 사이트 상단 탭 '디지보드게임' → board.trotbonbang.com/lobby
테스트하던 곳이 곧 사람들이 쓰는 곳이었다
문제는 셋이었습니다. 게임을 고치는 커밋이 개인 사이트 저장소에 섞이고, 시험 삼아 바꾼 코드가 곧바로 사용자 앞에 나가며, 서비스 이름과 주소가 다른 사이트에 묶여 있었습니다.
02정한 것 — 이름, 주소, 서버 둘
- 이름과 저장소 — 서비스 이름은 영문
DOTROBOARD, 한글도트로보드. 저장소도 새로(FOTIBlabs/dotroboard) 만들어 코드를 옮겼습니다. 처음엔 같은 저장소 안의 다른 폴더로 복사했다가, "도메인 자체를 새로 연결한다"는 말을 듣고 바로 별도 저장소로 옮겼습니다. - 정식과 테스트 — 정식은
dotroboard.com. 테스트 서버 주소는 세 가지를 견줬습니다: 다른 개인 도메인의 서브도메인, 개인 사이트의 기존 서브도메인,test.dotroboard.com. 관리할 곳이 한 군데로 모이고 구글 로그인 설정이 단순해지는 test.dotroboard.com + 검색 제외로 정했습니다. - Pages 대신 Workers — 새 프로젝트를 만들 때 대시보드가 기본으로 Worker를 만들었고, 정적 파일만 내보내는 이 앱에서는 차이가 거의 없어 그대로 Workers로 통일했습니다. Cloudflare가 새 기능을 Workers 쪽에 붙이고 있다는 점도 이유였습니다.
- DB는 서울 하나 — 해외 서비스도 고려했지만, 턴제 게임이라 해외 이용자의 0.2초 지연은 체감이 적습니다. 접속 지역별로 서버를 나누는 안도 따져 봤는데, 한국 사람이 보낸 초대 링크로 미국 친구가 들어오면 다른 DB라 방이 없는 문제가 바로 생깁니다. 나누게 되면 접속 위치가 아니라 방 주소에 지역을 넣는 방식으로 하기로 하고 보류했습니다. Supabase 리전은 나중에 바꿀 수 없어서 이 결정을 프로젝트를 만들기 전에 했습니다.
03전체 구조 — 다섯 서비스의 연결
Namecheap 도메인 구매만. 네임서버를 Cloudflare 두 개로 바꾼 뒤로는 DNS·메일 설정을 하지 않는다 │ 네임서버 ▼ Cloudflare 영역 dotroboard.com ├─ DNS dotroboard.com → Worker(커스텀 도메인) · www → A 192.0.2.1(Proxied, 자리표시) ├─ Rules www → 루트 301 (Redirect) · test.* 응답에 X-Robots-Tag: noindex (Transform, 응답 헤더) ├─ Email Email Routing: contact@dotroboard.com → 개인 메일 (받기 전용) └─ Workers dotroboard (main) ─▶ dotroboard.com dotroboard-test (dev) ─▶ test.dotroboard.com ▲ Workers Builds: push하면 npm run build → npx wrangler deploy [--env test] GitHub FOTIBlabs/dotroboard ─┘ main = 정식, dev = 테스트, 새 작업은 dev에서 Supabase 정식 프로젝트(서울) ← 정식 Worker의 빌드 변수 VITE_SUPABASE_URL / _PUBLISHABLE_KEY 테스트 프로젝트(옛 데이터 유지) ← 테스트 Worker의 빌드 변수 각각: 마이그레이션 · Edge Function(game) · secrets(GAME_CRON_SECRET) · Vault 2개 · Auth URL 설정 Google Cloud 정식용 프로젝트 → OAuth 클라이언트 → 정식 Supabase의 Google 로그인 설정 테스트용 프로젝트 → OAuth 클라이언트 → 테스트 Supabase의 Google 로그인 설정 Search Console: 도메인 속성 dotroboard.com (DNS TXT로 소유 확인, test.*도 함께 인정) 검색 등록 Google Search Console · 네이버(index.html meta) · 다음(robots.txt 맨 아래 주석) · sitemap.xml
같은 코드, 다른 연결 — 정식과 테스트를 가르는 건 브랜치와 빌드 변수뿐
핵심은 코드에는 어느 서버인지가 들어 있지 않다는 점입니다. 같은 저장소의 두 브랜치를 두 Worker가 각자 빌드하고, Worker마다 넣어 둔 빌드 변수가 화면 코드에 어느 Supabase를 쓸지 굽습니다. 구글 로그인은 Supabase를 거치므로, Supabase 프로젝트마다 자기 구글 클라이언트를 갖습니다. 비밀값(서버 키, 크론 비밀값, OAuth 보안 비밀)은 저장소 어디에도 없습니다.
04이전 순서 — 정식 서버 (실제로 한 그대로)
- 코드 옮기기 — 원래 저장소에서 git에 올라간 파일만 새 저장소 맨 위로 꺼냈습니다(
git archive HEAD:digiboardgame | tar -x, 575개).node_modules·dist·.env는 따라가지 않습니다. 화면·개인정보처리방침의 서비스 이름과 주소,package.json이름, README의cd digiboardgame안내를 새 저장소 기준으로 고쳤습니다. - Worker 연결 — Workers & Pages → Create → Import a repository. 빌드 명령
npm run build, 배포 명령npx wrangler deploy, Root directory/. 저장소에wrangler.jsonc를 두어dist/를 올리고, 없는 주소는index.html을 돌려주게 했습니다(not_found_handling: "single-page-application"). - 도메인 — Cloudflare에 도메인 추가 → 알려 준 네임서버 두 개를 Namecheap의 Custom DNS에 입력 → Active 대기. Namecheap에서 딸려 온 주차 페이지 A 레코드와 메일 포워딩 MX·TXT는 지우고, Worker의 Domains에서 커스텀 도메인
dotroboard.com을 붙였습니다(Cloudflare가 레코드를 알아서 만듭니다). - www — Worker에 붙이지 않고, DNS에
A www 192.0.2.1(Proxied)만 둔 뒤 Redirect Rules 템플릿 "Redirect from WWW to root"(301, Preserve query string 체크)로 루트에 보냈습니다. 요청이 Cloudflare에서 먼저 처리되므로 자리표시 주소로 충분합니다. - 메일 — 네임서버를 옮기면 Namecheap 메일 포워딩은 관리할 수 없게 됩니다. Cloudflare Email Routing으로
contact@dotroboard.com→ 개인 메일을 만들었습니다. 받기 전용이라, 그 주소로 보내려면 별도 메일 서비스가 필요합니다. - Supabase 새 프로젝트 — 리전 서울. Data API는 켜고, "Automatically expose new tables"는 껐습니다(마이그레이션이 모든 테이블 권한을 회수한 뒤 필요한 것만 직접 주기 때문에 동작은 같고, 실수로 만든 테이블이 바로 공개되지 않음). Auth에서 익명 로그인(게스트)과 manual linking(게스트 → 구글 연결)을 켰고, 캡차는 코드 수정 없이 켜면 게스트 로그인이 막혀서 두었습니다. 이어서 CLI로
supabase link→db push(마이그레이션 31개) →secrets set GAME_CRON_SECRET→npm run functions:deploy, SQL Editor에서 Vault 비밀값 2개(함수 주소, 크론 비밀값), Realtime의 public access 끄기, URL Configuration(Site URL과 Redirect URLs)까지. - 빌드 변수 — Worker → Settings → Build의 Variables and secrets(런타임 변수 칸이 아님)에 Supabase 주소와 publishable key. 이 값은 빌드할 때 화면 코드에 들어가므로 바꾸면 반드시 다시 빌드합니다.
- 구글 로그인 — 정식용 Google Cloud 프로젝트를 새로 만들고, Search Console에서 도메인 속성
dotroboard.com을 DNS TXT로 소유 확인. Google Auth Platform에서 앱 이름 DOTROBOARD, 홈페이지·개인정보처리방침·약관 링크, 승인된 도메인을 넣고(로고는 넣지 않음 — 넣으면 브랜드 심사), 웹 클라이언트의 JavaScript 원본에 사이트 주소, 리디렉션 URI에https://<Supabase 프로젝트>.supabase.co/auth/v1/callback. 받은 ID·보안 비밀을 Supabase의 Google 로그인 설정에 넣고, 대상을 프로덕션으로 게시했습니다(테스트 상태면 등록한 사람만 로그인됩니다). - 확인 — 게스트로 방 만들기 → 다른 브라우저로 초대 링크 입장 → 한 판, 구글 로그인, 게스트에서 구글 연결, 창을 모두 닫아도 차례가 넘어가는지(크론·Vault 확인).
05테스트 서버 분리 — 브랜치 하나, Worker 하나
브랜치 dev ─ 새 작업은 dev에서 따고 dev로 → test.dotroboard.com에서 확인 → dev→main PR 머지 → 정식 설정 파일 wrangler.jsonc 의 env.test = { name: "dotroboard-test" } — 두 브랜치가 같은 파일, 머지해도 충돌 없음 Worker dotroboard-test · Branch control dev · 배포 명령 npx wrangler deploy --env test 빌드 변수 = 테스트 Supabase · 커스텀 도메인 test.dotroboard.com 검색 제외 Transform Rules(응답 헤더) Hostname = test.dotroboard.com → X-Robots-Tag: noindex, nofollow robots.txt로 막지 않는다 — 크롤러가 못 들어오면 noindex도 못 읽고 주소만 검색될 수 있다 새는 길 막기 정식 Worker: Previews Base 탭의 "Builds for Preview branches" 끄기 — 켜 두면 dev를 push할 때 정식 Worker가 dev 코드를 정식 DB 키로 미리보기 빌드한다 두 Worker 모두 workers.dev 주소·Preview URL 끄기 — 커스텀 도메인 하나로만 들어오게 DB 변경 마이그레이션·Edge Function은 테스트 Supabase에 먼저 → 머지 후 정식 Supabase에 같은 명령
테스트 서버는 예전 테스트 DB와 구글 클라이언트를 그대로 이어 받았다
테스트 서버는 새로 만들지 않고, 예전 board 서버가 쓰던 Supabase 프로젝트와 구글 클라이언트를 그대로 물려받았습니다. 바꾼 것은 그쪽 Supabase의 Site URL·Redirect URLs, 구글 클라이언트의 JavaScript 원본, 동의 화면의 앱 이름(DOTROBOARD TEST)과 승인된 도메인뿐입니다. 덕분에 테스트하던 계정과 기록이 끊기지 않았습니다.
06옛 서버 정리
- 탭 먼저 — 개인 사이트 상단의 '디지보드게임' 탭을 먼저 지웠습니다. 정적 페이지를 생성하는 스크립트(
build-pages.js)와index.html두 곳에 있어서, 둘 다 고치고 스크립트를 다시 돌려 상세 페이지 11개를 새로 만들었습니다. 주소를 끊기 전에 링크부터 없애야 깨진 탭이 남지 않습니다. - Pages 프로젝트 삭제 — 배포 기록이 100개를 넘어 대시보드에서 지워지지 않았습니다. Cloudflare 안내대로
wrangler pages deployment delete로 기록을 지운 뒤 삭제했습니다(아래 문제 해결 기록). - 남은 연결 끊기 — 개인 사이트 영역의
boardDNS 레코드, 테스트 Supabase Redirect URLs와 구글 JavaScript 원본의 board 주소를 지웠고, 개인 사이트 저장소의digiboardgame/폴더도 지웠습니다(기록은 git에 남음).
07공개 준비 — 보이는 것과 찾히는 것
- 로고와 아이콘 — 받은 픽셀 로고는 주사위 눈이 윗면 4·양옆 2·2로, 실제 주사위로는 나올 수 없는 조합이었습니다. 눈을 지우고 다시 찍어 1·2·3으로 고쳤습니다. 이 로고로 공유 미리보기 이미지(1200×630)를 만들고, 16px에서 알아볼 수 없는 로고 대신 로고 속 빨간 D를 16×16 픽셀로 다시 그려 파비콘·홈 화면 아이콘으로 썼습니다. 상단 헤더는 로고 이미지 + "도트로보드".
- 문서 — 이용약관(
/terms)을 새로 쓰고, 개인정보처리방침의 서비스명·주소·문의 메일을 바꿨습니다. 로그인 버튼 아래와 홈 하단에 두 문서를 링크했습니다. - 검색 — OG·트위터 태그,
robots.txt(방·게임·개인 화면 제외),sitemap.xml. Search Console·네이버 서치어드바이저·다음 웹마스터도구에 등록하고, 각자의 소유 확인 코드는 지우면 안 되는 위치로 문서에 적어 두었습니다. - 다른 작업 세션을 위해 — README 맨 위에 "운영 현황 한눈에"(서버·브랜치·Worker·Supabase·구글 프로젝트 대응표, 비밀값 위치)와 "작업 규칙"(dev에서 작업, 테스트 DB 먼저,
_redirects금지)을 두고, 아키텍처 문서에 구성도와 이번 결정들을 남겼습니다.
08문제 해결 기록
- 첫 배포 실패 —
_redirects무한 루프 — Pages에서 쓰던/* /index.html 200을 Workers는 "무한 루프"(code 100324)로 보고 배포를 거부했습니다. 파일을 지우고wrangler.jsonc의 SPA 설정으로 바꿨습니다. Pages는404.html이 없으면 같은 폴백을 하므로 두 쪽 모두에서 동작합니다. - "이미 레코드가 있다" — www 리디렉션 규칙이 "프록시된 레코드가 없다"고 경고해 레코드를 만들려 하니, 이번엔 이미 있다고 했습니다. 도메인을 추가할 때 Namecheap 레코드를 회색 구름(프록시 꺼짐)으로 가져온 것이었습니다. 정리 뒤 자리표시 레코드를 Proxied로 두었습니다.
- 정식에 반영 안 된 커밋 — 첫 커밋을 올린 작업 브랜치가 빈 저장소의 기본 브랜치가 되어 버렸고, 정식 Worker는 main을 빌드했습니다. 기본 브랜치를 main으로 바꾸고 PR로 맞췄습니다. 이후로는 "main에 무엇이 빠졌나"를
git log main..dev로 먼저 봅니다. - 테스트 서버만 "서버 미연결" — 빌드 변수는 맞게 들어 있었습니다. Retry build는 그 빌드의 브랜치·커밋을 다시 빌드하는데, 테스트 Worker를 만들 때의 첫 빌드가 main이었습니다. Branch control을 dev로 바꾼 뒤에는 dev에 새 push가 있어야 dev로 빌드됩니다.
- 탭과 선택지 — Build 화면의 Production / Previews Base는 둘 중 하나를 고르는 스위치가 아니라 설정 화면 두 장입니다. Rules의 Request Header와 Response Header Transform도 다른 것이라, 검색 제외는 Response 쪽에 걸어야 합니다.
- 지워지지 않는 Pages 프로젝트 — 배포 기록 100개 초과. 문서의 삭제 스크립트는 bash와
jq용이라 PowerShell로 옮겼는데,wrangler login은 성공해도 출력을 다른 명령으로 넘기면 "로그인 정보 없음"이 나오고, 목록이 비어 바로 "Done"이 찍혔습니다(지운 것은 없음). Pages: Edit 권한의 API 토큰을 환경변수로 주고, 목록을 못 받으면 멈추도록 고친 스크립트로 지운 뒤 프로젝트를 삭제하고 토큰도 지웠습니다.
09다음 이전 때 그대로 쓸 체크리스트
0 결정 이름 · 정식/테스트 주소 · DB 리전(나중에 못 바꿈) · Pages/Workers 1 코드 git archive로 추적 파일만 옮기기 → 이름·주소·문서 정리 → 빌드·테스트 2 호스팅 Worker 연결(build / deploy / root) · wrangler.jsonc(SPA) · _redirects 금지 3 도메인 네임서버 → Cloudflare · 가져온 레코드 정리 · 커스텀 도메인 · www 301 · 메일 라우팅 4 DB 새 Supabase(리전) · 익명/manual linking · link → db push → secrets → functions:deploy Vault 2개 · Realtime public 끄기 · Auth URL(Site/Redirect) 5 빌드변수 Build 칸에 VITE_SUPABASE_URL / _PUBLISHABLE_KEY → 다시 빌드 6 구글 새 GCP 프로젝트 · Search Console 소유 확인 · 브랜딩(로고 없이) · 클라이언트(원본 / 콜백) → Supabase Google 설정 → 프로덕션 게시 7 테스트 dev 브랜치 · env.test · 두 번째 Worker(--env test) · noindex 응답 헤더 정식 미리보기 빌드 끄기 · workers.dev·Preview URL 끄기 · 첫 dev push로 빌드 확인 8 확인 게스트 방·초대 링크·한 판 · 구글 로그인/연결 · 창 닫고 차례 넘어감 · 관리자 화면 9 정리 옛 링크 먼저 제거 → 옛 도메인 떼기 → 옛 프로젝트 삭제 → Auth/구글의 옛 주소 제거 10 공개 약관·개인정보 · OG 이미지·아이콘 · robots·sitemap · 검색 등록 3곳 · README 대응표
순서가 중요한 곳: DB 리전은 처음에, 옛 링크는 주소를 끊기 전에
10지금 이 자리
도트로보드는 dotroboard.com과 test.dotroboard.com 두 서버로 나뉘어, 새 기능은 테스트에서 확인한 뒤 머지 한 번으로 정식에 나가는 흐름이 자리 잡았습니다. 몇 차례의 반복 끝에 얻은 잠깐의 안정, 균형(Balance)의 단계입니다.
남은 원칙은 세 가지입니다. 코드에는 서버를 적지 않고 연결로 가른다. 정식으로 새는 길(미리보기 빌드, 기본 주소, 같은 DB 키)은 하나씩 막는다. 바꾼 연결은 다른 세션이 읽을 수 있는 문서에 남긴다. 다음 흐름은 공유 미리보기와 검색 노출이 실제로 어떻게 잡히는지 관찰(Observe)하고, 사용자가 늘면 DB 요금제와 게스트 가입 보호(캡차)를 붙이는 일입니다.