Cloudflare Pages는 훌륭한 정적 호스팅 서비스입니다. VORA가 운영되는 규모에서는 무료이며, GitHub 푸시에서 자동으로 배포되고, 내장된 글로벌 CDN 배포를 가집니다. 또한 모든 정적 호스팅 플랫폼처럼 특정한 규칙을 따릅니다 — 그리고 그 규칙을 배우는 것은 우리에게 실패한 배포 여러 번과 Google Search Console 인덱싱 사건보다 더 많은 비용을 들였습니다. 이 포스트는 배포 교훈을 기록합니다.
정적 사이트에 Cloudflare Pages를 선택한 이유
VORA의 아키텍처는 순수 정적 파일입니다: HTML, CSS, JavaScript. 빌드 단계 없음, 서버 없음, 데이터베이스 없음. 이것은 정적 호스팅을 위한 이상적인 후보입니다. 우리가 고려한 대안은 GitHub Pages, Netlify, Vercel입니다. 우리는 한 가지 특정한 이유로 Cloudflare Pages를 선택했습니다: 글로벌 CDN은 Cloudflare의 엣지 네트워크로 운영되며, 아시아 태평양에서 예외적으로 낮은 지연시간을 제공합니다. 주요 사용자가 한국과 일본인 제품의 경우, Seoul과 Tokyo에서 Cloudflare의 엣지 성능은 경쟁사보다 측정 가능하게 더 좋습니다.
설정은 간단합니다: GitHub 저장소를 연결하고, 루트 디렉토리를 설정하고, 빌드 출력 디렉토리를 설정합니다 (또는 빌드 단계가 없는 정적 사이트의 경우 비워둡니다). 메인에 푸시하면 자동으로 배포됩니다. 정상적인 경로에는 설정이 필요하지 않습니다.
Sitemap/Robots 사건
커밋 메시지 "chore: sitemap과 robots를 Cloudflare Pages 배포용 루트로 이동"은 실제 일어난 일을 축소합니다. 전체 이야기가 여기 있습니다.
우리는 sitemap.xml과 robots.txt를 저장소에 추가했습니다. 처음에는 파일 구성의 표준 관행을 따르면서 이들을 하위 디렉토리에 배치했습니다. Google Search Console은 sitemap을 검증하도록 구성되었습니다.
문제점: robots.txt는 정확한 경로 https://yourdomain.com/robots.txt에서 제공되어야 합니다 — 어떤 하위 디렉토리에서도 아닙니다. Googlebot은 명시적으로 루트만 봅니다. 마찬가지로 sitemap.xml은 관례상 https://yourdomain.com/sitemap.xml에서 예상됩니다 (실제 경로는 robots.txt에서 지정될 수 있음). 이 파일들이 하위 디렉토리에 있을 때, Google Search Console은 sitemap에 접근할 수 없다고 보고했고 robots.txt를 찾지 못했습니다. 인덱싱의 몇 주가 잠재적으로 영향을 받았습니다.
파일을 저장소 루트로 이동하면 (Cloudflare Pages 배포 루트가 됨) 즉시 문제가 해결되었습니다. Search Console 검증이 녹색으로 변했습니다. 하지만 sitemap을 인덱싱하는 데 걸린 지연은 일부 블로그 포스트가 게시 후 2-3주 동안 인덱싱되지 않았다는 것을 의미했습니다.
sitemap.xml → yourdomain.com/sitemap.xml에서 제공됩니다. 기본적으로 "공용" 디렉토리나 "dist" 디렉토리가 없습니다.
WASM 페이지를 위한 CORS 헤더 도전
Labs의 WASM 실험은 특정 HTTP 응답 헤더가 필요합니다: Cross-Origin-Opener-Policy: same-origin 그리고 Cross-Origin-Embedder-Policy: require-corp. 이것들은 SharedArrayBuffer에 필요하며, ONNX Runtime이 다중 스레드 WASM 실행을 위해 필요합니다.
일반적인 웹 서버에서는 nginx 또는 Apache 설정에서 이 헤더들을 추가합니다. Cloudflare Pages에서는 배포 루트에 배치된 _headers라는 특별한 파일을 통해 사용자 정의 헤더를 구성합니다. 형식:
# _headers file (Cloudflare Pages)
/vad-test.html
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
/hybrid-asr-test.html
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
/sherpa-onnx-test.html
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
이것은 작동했습니다 — 하지만 나열된 특정 페이지에만 해당합니다. 교차 출처 리소스를 로드하는 모든 페이지 (Google Fonts, Font Awesome CDN, Google AdSense) 그리고 COEP 헤더도 필요한 경우 근본적인 충돌에 직면합니다. COEP는 모든 하위 리소스도 Cross-Origin-Resource-Policy: cross-origin을 설정하도록 요구합니다. Google의 CDN 리소스는 이 헤더를 설정하지 않습니다. 결과: COEP를 비활성화하거나 (SharedArrayBuffer 손실) 외부 CDN 리소스를 손실합니다 (글꼴과 아이콘 손실).
우리의 해결책: WASM 집약적인 lab 페이지의 경우, 우리는 ONNX Runtime 라이브러리를 로컬로 제공했습니다 (저장소의 lib/ 디렉토리에 추가). 그 페이지들이 Google Fonts나 Font Awesome을 로드하지 않는 것을 받아들였습니다. 이것이 lab 실험 페이지가 메인 사이트와 약간 다른 타이포그래피를 가진 이유입니다. 우리는 시각적 일관성보다 기능을 선택했습니다.
실제로 프로덕션에서 사용되지 않는 server.py
저장소에는 server.py 파일이 포함되어 있습니다. 이것은 사람들을 혼동시킵니다. 이것은 CORS와 COIP 헤더가 주입된 Python HTTP 서버로, WASM lab 페이지의 로컬 개발용입니다. Sherpa-ONNX 또는 ONNX Runtime 페이지를 로컬에서 테스트하고 싶을 때, 파일을 직접 열거나 기본 HTTP 서버를 사용하는 대신 python3 server.py를 실행합니다 — WASM 스레딩이 단순 python3 -m http.server가 주입하지 않는 교차 출처 격리 헤더가 필요하기 때문입니다.
프로덕션에서 (Cloudflare Pages), _headers 파일이 이것을 처리합니다. server.py는 순수하게 개발자 경험 도구입니다. 우리는 혼동을 줄이기 위해 제거하는 것을 고려했지만 SETUP.md 파일은 "로컬 개발 방법" 워크플로우를 위해 명시적으로 참조합니다. 그것은 로컬 개발 전용임을 설명하는 주석과 함께 유지됩니다.
자동 병합 워크플로우 (그리고 왜 필요한가)
저장소에는 auto-merge.yml이라는 GitHub Actions 워크플로우가 있습니다. 커밋: "PR을 자동으로 병합하는 auto-merge.yml 워크플로우를 생성합니다." 이것은 우리가 사용하는 AI 지원 개발 워크플로우를 위한 인프라입니다.
봇 브랜치의 Pull 요청은 기본 검증을 통과할 때 자동으로 병합됩니다. 자동 병합이 없으면, 워크플로우는 다음과 같습니다: 봇이 PR을 푸시합니다 → 개발자가 수동으로 검토 및 병합해야 합니다 → 모든 변경에 대해 반복합니다. 자동 병합을 사용하면: 봇이 PR을 푸시합니다 → GitHub Actions가 검증합니다 → 검사를 통과하면 PR이 자동으로 병합됩니다. 이것은 메인 브랜치를 최신으로 유지하는 마찰을 줄입니다.
인간 검토 없이 자동으로 PR을 병합하는 것에 대한 명백한 우려가 있습니다. 우리의 완화 조치: 모든 자동 병합 PR은 사후에 검토됩니다; 자동 병합은 특정 브랜치 (봇 브랜치)의 PR에만 적용됩니다; 그리고 페이지는 서버 측 논리 없이 정적 HTML로, 실수의 폭발 반경을 줄입니다. 나쁜 변경이 자동 병합되어 배포된다면, 수정은 또 다른 커밋이고 또 다른 자동 배포 사이클입니다 — 일반적으로 2분 이내.
처음부터 다르게 구성할 것들
뒤를 돌아보면서, 몇 가지 Cloudflare Pages 설정을 우리가 나중에 발견하기보다 처음부터 설정했을 것입니다:
- 처음부터 사용자 정의 도메인:
vora.vibed-lab.comURL은 기능적이지만 영구적으로 이전 제품 이름을 표시합니다. 사용자 정의 도메인은 URL을 프로젝트 이름에서 분리했을 것입니다. - 처음부터 _headers 파일: WASM 실험이 필요할 때 반응적으로 CORS 헤더를 추가하기보다, 파일을 처음부터 배치하면 일부 페이지가 작동하고 다른 페이지가 그렇지 않은 혼동스러운 기간을 피할 수 있었을 것입니다.
- 처음부터 루트에 sitemap.xml과 robots.txt: 절대 다른 곳에 배치하지 마세요. 그것들을 다른 곳에 배치할 정당한 이유가 없습니다.
- 테스트를 위한 미리보기 배포: Cloudflare Pages는 비 메인 브랜치에 대해 미리보기 배포를 자동으로 생성합니다. 우리는 처음에 이것들을 사용하지 않았으며, 모든 변경에 대해 직접 메인으로 배포했습니다. 미리보기 배포를 사용하면 배포 특정 문제 (robots.txt 배치 같은)를 프로덕션에 영향을 미치지 않고 잡을 수 있었을 것입니다.
정적 호스팅은 복잡하지 않습니다. 복잡성은 세부사항에 있습니다: 특정 파일이 어디에 살아야 하는가, 보안 헤더가 CDN 리소스와 어떻게 상호 작용하는가, 실제로 빌드할 필요가 없는 빌드를 구성하는 방법. 이것들은 해결 가능한 문제로, 우리가 몇 주에 걸친 반응적 수정으로 늘린 하루의 문서 읽기입니다.