Cloudflare Workers에서 Gemini API를 서버 사이드로 호출할 때 다음 오류가 발생했습니다.
{
"error": {
"code": 400,
"message": "User location is not supported for the API use.",
"status": "FAILED_PRECONDITION"
}
}
처음에는 API 키 문제처럼 보였습니다. 하지만 로컬 PowerShell에서 같은 Gemini API 키로 generateContent 요청을 보내면 정상 응답이 왔고, Cloudflare Workers에서만 실패했습니다. 결국 원인은 키 자체가 아니라 Cloudflare Worker가 Gemini 무료 티어가 허용하지 않는 위치에서 Google AI API를 호출한 것에 가까웠습니다.
해결은 Cloudflare Workers의 Placement Hint를 사용해 admin Worker 실행 위치를 Google Cloud 서울 리전에 가깝게 유도하는 방식이었습니다.
"placement": {
"region": "gcp:asia-northeast3"
}
이 글은 이 문제를 진단하고, gcp:asia-northeast3 설정으로 실제 라이브 서버에서 AI 재작성 기능을 복구하기까지의 과정을 정리한 기록입니다.
문제 상황
Oaty 관리자 패널의 /admin/editor에는 두 개의 AI 보조 기능이 있습니다.
- SEO 최적화
- AI 재작성
브라우저에서 Gemini API를 직접 호출하지 않고, 관리자 패널의 프런트엔드는 내부 API인 /api/ai-assistant로 요청을 보냅니다. 이 서버 API는 Cloudflare Workers에서 실행되고, Worker 내부에서 @google/genai SDK를 사용해 Gemini API를 호출합니다.
구조를 단순화하면 다음과 같습니다.
브라우저
-> /admin/editor
-> /api/ai-assistant
-> Cloudflare Worker
-> Google Gemini API
이 구조 자체는 맞습니다. API 키가 브라우저에 노출되지 않고, CORS와 남용 위험도 서버에서 통제할 수 있기 때문입니다. 문제는 Cloudflare Worker가 어느 데이터센터에서 실행되느냐였습니다.
처음 보였던 오류
라이브 서버에서 AI 재작성 버튼을 누르면 관리자 패널 상단 콘솔에 다음 오류가 표시됐습니다.
AI 오류: {"error":{"code":400,"message":"User location is not supported for the API use.","status":"FAILED_PRECONDITION"}}
Google AI Studio 사용량 화면에서도 API 오류가 400 BadRequest로 잡혔습니다. Google의 Gemini API troubleshooting 문서에 따르면 FAILED_PRECONDITION은 Gemini API 무료 티어가 해당 요청 지역에서 제공되지 않을 때 발생할 수 있습니다. 공식 해결책은 Google AI Studio 프로젝트에 billing을 활성화하는 것입니다.
하지만 이 프로젝트는 무료 티어를 최대한 활용하는 범용 블로그 프로젝트입니다. billing account를 붙이는 방식은 기술적으로는 간단하지만, 운영 철학과 맞지 않았습니다. 따라서 먼저 요청이 실제로 어느 위치에서 나가는지 확인해야 했습니다.
Cloudflare 로그에서 확인한 핵심 값
Cloudflare Workers Logs에서 실패 요청을 보면 다음과 같은 값이 있었습니다.
{
"request": {
"cf": {
"country": "KR",
"city": "Daejeon",
"colo": "HKG"
}
},
"response": {
"status": 500
}
}
여기서 중요한 점은 country와 colo가 다른 의미라는 것입니다.
| 값 | 의미 |
|---|---|
country: KR |
요청한 사용자가 한국에서 접속했다는 뜻 |
city: Daejeon |
Cloudflare가 인식한 사용자 위치 |
colo: HKG |
Worker 요청을 처리한 Cloudflare 데이터센터 |
즉 사용자는 한국에 있었지만, Worker는 홍콩 쪽 Cloudflare 데이터센터에서 실행되고 있었습니다. 그리고 Gemini API 무료 티어는 이 요청을 지원되지 않는 위치에서 온 요청으로 판단했습니다.
왜 API 키 문제가 아니었나
같은 API 키를 로컬에서 직접 테스트하면 정상 응답이 왔습니다.
$key = Read-Host "Gemini API key"
Invoke-RestMethod `
-Method Post `
-Uri "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash:generateContent?key=$key" `
-ContentType "application/json" `
-Body '{"contents":[{"parts":[{"text":"ping"}]}]}'
정상 응답에서는 finishReason=STOP과 token count가 포함된 결과가 반환됐습니다. 이 테스트로 알 수 있는 것은 두 가지였습니다.
- API 키 자체는 살아 있다.
- 사용자의 로컬 환경에서는 Gemini API 무료 티어 호출이 가능하다.
따라서 남은 차이는 Cloudflare Worker의 실행 위치와 outbound 요청 경로였습니다.
Smart Placement 제거만으로는 부족했다
처음에는 Cloudflare Workers의 Smart Placement가 Worker를 예상과 다른 곳으로 보낸다고 보고, "mode": "smart" 설정을 제거하는 실험을 했습니다. 하지만 이 방식만으로는 문제가 안정적으로 해결되지 않았습니다.
Smart Placement는 Cloudflare가 트래픽 패턴을 분석해 Worker 실행 위치를 자동으로 최적화하는 기능입니다. 다만 Cloudflare 공식 문서에 따르면 Smart Placement는 이전에 Worker가 실행된 위치들을 기준으로 판단하며, 충분한 호출 데이터가 없으면 원하는 위치로 안정적으로 이동하지 않을 수 있습니다.
이 프로젝트의 경우 필요한 것은 자동 최적화가 아니라 Gemini API 무료 티어가 허용하는 경로로 Worker 실행 위치를 유도하는 것이었습니다.
해결: Placement Hint로 서울 리전 근처를 지정
최종적으로 admin Worker의 wrangler.jsonc에 다음 설정을 추가했습니다.
"placement": {
"region": "gcp:asia-northeast3"
}
asia-northeast3는 Google Cloud 기준으로 서울, 대한민국 리전입니다. Google Cloud 문서에서 asia-northeast3-a, asia-northeast3-b, asia-northeast3-c는 모두 Seoul, South Korea로 표시됩니다.
단, 이 설정의 의미를 정확히 이해해야 합니다. Cloudflare Worker가 Google Cloud 서울 리전 내부에서 실행되는 것은 아닙니다. Cloudflare Placement Hint는 지정한 클라우드 리전에 지연 시간이 낮은 Cloudflare 데이터센터에서 Worker를 실행하도록 유도하는 기능입니다.
즉 이 설정은 다음에 가깝습니다.
Cloudflare Worker를 GCP 서울 리전에 가까운 Cloudflare 위치에서 실행하도록 유도한다.
라이브 서버 검증 결과
배포 후 /admin/editor에서 AI 재작성 버튼을 다시 눌렀습니다. Cloudflare 로그는 다음처럼 바뀌었습니다.
{
"request": {
"cf": {
"country": "KR",
"city": "Daejeon",
"colo": "NRT"
}
},
"response": {
"status": 200
}
}
결과는 다음과 같습니다.
| 항목 | 변경 전 | 변경 후 |
|---|---|---|
| Worker colo | HKG |
NRT |
| Gemini API 결과 | 400 FAILED_PRECONDITION | 200 성공 |
| 관리자 패널 AI 재작성 | 실패 | 성공 |
여기서 NRT는 일본 도쿄/나리타 권역 Cloudflare 데이터센터를 의미합니다. 한국에서 실행된 것은 아니지만, gcp:asia-northeast3 힌트 덕분에 홍콩이 아니라 서울에 더 가까운 Cloudflare 위치로 이동했고, Gemini API 호출도 성공했습니다.
같은 버튼을 여러 번 눌러 5회 연속 성공도 확인했습니다. 이 정도면 단발성 우연이라기보다 placement 변경이 실제로 영향을 준 것으로 볼 수 있습니다.
왜 이 방식이 이 프로젝트에 맞았나
이 프로젝트는 공개 블로그와 관리자 패널이 분리되어 있습니다.
astro-blog-site -> 공개 블로그
astro-blog-admin -> /admin, /api, /admin-assets
이번 변경은 admin Worker에만 적용됐습니다. 따라서 공개 블로그의 SEO에는 직접적인 영향이 없습니다.
공개 블로그 SEO에서 중요한 것은 다음입니다.
- canonical URL
- sitemap
- robots meta
- 정적 HTML과 구조화 데이터
- Open Graph 이미지
- 페이지 속도와 안정적인 응답
이번 설정은 /admin과 /api/ai-assistant를 담당하는 Worker의 실행 위치를 바꾼 것이므로, 공개 글의 색인 신호나 검색엔진 크롤링 경로를 바꾸지 않습니다.
관리자 패널 성능 측면에서는 약간의 트레이드오프가 있습니다. Placement Hint는 admin Worker 전체에 적용되므로 /api/ai-assistant뿐 아니라 /admin 화면과 다른 /api/* 요청도 같은 placement 정책의 영향을 받을 수 있습니다.
하지만 이 프로젝트의 사용자는 한국 사용자이고, 실제 실행 위치가 NRT라면 한국에서 체감 지연이 크게 늘 가능성은 낮습니다. 오히려 기존 HKG보다 가까운 위치로 바뀌었고, AI 기능이 실패에서 성공으로 바뀌었기 때문에 현재 조건에서는 유지할 가치가 큽니다.
주의할 점
이 방식은 billing 없이 Gemini API 무료 티어를 계속 쓰기 위한 현실적인 우회책이지만, 절대적인 보장은 아닙니다.
주의할 점은 세 가지입니다.
- Cloudflare Placement Hint는 실행 위치를 지정 리전 내부로 고정하는 기능이 아니다.
- Gemini API 무료 티어의 지역 정책은 Google 쪽에서 바뀔 수 있다.
- admin Worker 전체에 적용되므로 AI가 아닌 관리자 API도 영향을 받을 수 있다.
따라서 운영 중에는 Cloudflare Logs에서 다음 값을 계속 확인하는 것이 좋습니다.
request.cf.colo
request.cf.country
request.headers["cf-placement"]
response.status
wallTimeMs
특히 colo가 다시 HKG 등 실패했던 위치로 돌아가거나, Gemini 응답이 다시 FAILED_PRECONDITION으로 바뀌면 placement 전략을 재검토해야 합니다.
장기적으로 더 깔끔한 구조
현재처럼 admin Worker 전체에 placement를 거는 방식은 빠르게 문제를 해결하기에는 좋습니다. 하지만 아키텍처만 놓고 보면 가장 깔끔한 구조는 아닙니다.
장기적으로는 AI 호출만 별도 Worker로 분리하는 편이 더 좋습니다.
admin Worker
-> 일반 관리자 화면과 인증, 저장 API 처리
ai-assistant Worker
-> Gemini API 호출 전담
-> placement region: gcp:asia-northeast3
Cloudflare 공식 문서도 풀스택 Workers 애플리케이션에서는 인증과 라우팅 같은 edge logic과, 데이터베이스나 외부 API 호출 같은 backend logic을 별도 Worker로 나누고 Service Binding으로 연결하는 방식을 안내합니다.
다만 지금 당장 이 구조로 재구성할 필요는 없습니다. 현재 문제는 gcp:asia-northeast3 placement만으로 해결됐고, 이 프로젝트의 사용자는 한국에 한정되어 있습니다. 따라서 지금은 현재 설정을 유지하고, 나중에 관리자 패널 성능이나 API 구조가 커질 때 AI 전용 Worker 분리를 고려하는 것이 합리적입니다.
최종 설정
최종적으로 유지한 설정은 다음과 같습니다.
{
"placement": {
"region": "gcp:asia-northeast3"
},
"observability": {
"logs": {
"enabled": true,
"invocation_logs": true
},
"traces": {
"enabled": false
}
}
}
observability.logs는 Cloudflare가 추천한 최신 로그 동기화 형태라 유지했습니다. 이 설정 덕분에 이후에도 실패 요청의 colo, country, cf-placement, 응답 상태를 더 쉽게 확인할 수 있습니다.
결론
이번 문제의 핵심은 Gemini API 키가 아니라 Cloudflare Worker의 실행 위치였습니다.
정리하면 다음 흐름입니다.
- Gemini API 키를 새로 발급해도 라이브 Worker에서는 400 오류가 발생했다.
- 로컬에서 같은 키로 직접 호출하면 정상 응답이 왔다.
- Cloudflare Logs에서 실패 요청의
colo가HKG임을 확인했다. - Google Gemini API 문서상
FAILED_PRECONDITION은 무료 티어가 지원되지 않는 요청 지역에서 발생할 수 있다. placement.region을gcp:asia-northeast3로 지정했다.- 배포 후 Cloudflare
colo가NRT로 바뀌었고, AI 요청이 5회 연속 성공했다.
무료 티어를 유지해야 하고, 사용자가 한국에 집중된 프로젝트라면 이 방식은 충분히 고려할 만합니다. 다만 장기적으로는 AI 호출만 별도 Worker로 분리해 placement 영향을 최소화하는 구조가 더 안정적입니다.