바이브코더를 위한 Cloudflare막히는 순간을
푸는 1시간
Byung Kim, Daheyo Works
손 들어 주세요
- AI로 뭔가 만들어서 내 컴퓨터에서 돌려본 분?
- 그걸 친구에게 링크로 보내 본 분?
- 그러다 API 요금이나 에러 때문에 멈춘 분?
오늘의 한 문장
AI는 코드는 잘 써 준다.
코드 바깥은 대신 해 주지 못한다.
배포, 비밀 키, 데이터, 로그인, 운영. 오늘은 이 다섯 칸을 차례로 채웁니다.
오늘 고칠 앱: AI 메모장
메모를 쓰면 AI가 요약해 주는 HTML 페이지 하나. AI가 만들어 준 그대로입니다.
| 지금 상태 | 무엇이 문제인가 | 고치는 곳 |
|---|---|---|
| localhost에서만 돈다 | 친구에게 링크를 보낼 수 없다 | 파트 1 |
| API 키가 JavaScript에 있다 | 누구나 키를 복사해 내 돈으로 AI를 쓴다 | 파트 2 |
| 새로고침하면 메모가 사라진다 | 데이터가 브라우저 안에만 있다 | 파트 3 |
| 로그인이 없다 | 주소만 알면 남의 메모를 본다 | 파트 4 |
| 고장 나면 알 길이 없다 | "안 돼요"라는 말만 듣는다 | 파트 5 |
파트 1. 배포의 벽
친구한테 보낸 링크가
localhost:3000이었다.
localhost는 내 컴퓨터 안에서만 통하는 주소입니다.
파트 1. 원리
배포 = 24시간 켜진
남의 컴퓨터에 올려 두기
- Cloudflare는 그 컴퓨터를 전 세계 수백 곳에 두고, 인터넷 경로상 가까운 곳에서 응답합니다.
- HTML, CSS, 이미지 요청은 무료이고 요청 수에도 세지 않습니다.
- 새 프로젝트는 Pages 대신 Workers로. 공식 문서의 권장입니다.
파트 1. 시연
{
"name": "ai-memo",
"compatibility_date": "2026-09-30",
"main": "./src/worker.ts",
"assets": {
"directory": "./dist",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api/*"]
},
"observability": { "enabled": true }
}npx wrangler login
npx wrangler deploy출력된 주소를 직접 열어 보는 것까지가 배포.
내 도메인은 네임서버를 Cloudflare로 옮긴 뒤 커스텀 도메인으로 붙입니다. DNS와 HTTPS는 Cloudflare가 만듭니다.
파트 1. 여기서 많이 막힙니다
| 증상 | 원인과 해결 |
|---|---|
| 빈 화면 | assets.directory가 빌드 결과 폴더(dist 등)가 아니다 |
| 새로고침하면 404 | SPA는 not_found_handling을 직접 적어야 한다 |
| 도메인이 안 붙음 | 네임서버가 Cloudflare에 있어야 한다. www는 따로 등록 |
| "성공"인데 안 바뀜 | 같은 이름의 Pages 프로젝트로 올라간 실제 사례. 주소를 직접 열어 보기 |
한국에서 느리다면 내주소/cdn-cgi/trace의 colo 값부터 확인. 자세한 내용은 자료 페이지 파트 1.
파트 2. 키 노출과 비용 폭탄
F12를 눌렀더니
내 API 키가 그대로 보였다.
방금 이 키를 전 세계에 공개했습니다. 노출된 키는 폐기하고 새로 만드세요.
파트 2. 원리
고치기 전
F12만 누르면 누구나 키를 복사합니다.
고친 뒤
② 시크릿 키 붙이기
키는 Worker 시크릿 안에만 있습니다.
파트 2. 시연
npx wrangler secret put AI_API_KEY.dev.vars*
.env*로컬 개발용 키는 .dev.vars에. 저장소에는 올리지 않습니다.
"ratelimits": [{
"name": "LIMITER",
"namespace_id": "1001",
"simple": { "limit": 10, "period": 60 }
}]const { success } = await env.LIMITER
.limit({ key: ip });
if (!success) return new Response('잠시 후에',
{ status: 429 });파트 2. 돈은 어디서 새는가
무료 플랜은 멈춘다
Workers 하루 10만 요청. 한국 시간 오전 9시 초기화. 넘으면 과금이 아니라 1027 오류.
유료 플랜은 안 멈춘다
월 $5부터. 자동으로 멈추는 상한은 공식 문서에 없음. Budget alert는 다음 날 오는 이메일.
진짜 폭탄은 AI API
OpenAI·Anthropic 콘솔에서 월 사용 한도를 꼭 걸 것.
레이트 리밋은 계량기가 아니라 폭주 차단기입니다. 데이터센터마다 따로 세고, 기간은 10초나 60초뿐입니다.
파트 2. AI가 자주 틀리는 것
- "환경 변수로 옮겼어요":
VITE_,NEXT_PUBLIC_이 붙은 변수는 브라우저 코드에 그대로 박힙니다. - 키를
vars에: 평문이고 저장소에 같이 올라갑니다. - 메모리 Map 카운터: Worker 인스턴스마다 따로 세서 제한이 안 먹습니다.
- KV 카운터: 같은 키는 1초에 한 번만 쓸 수 있고, 무료 쓰기는 하루 1,000회.
그래서 마지막에 F12와 빌드 결과 폴더를 직접 확인합니다.
파트 3. 데이터가 사라진다
메모를 세 개 썼는데
새로고침하니 다 사라졌다.
localStorage로 고쳐도 휴대폰에서 열면 없습니다. 브라우저 저장은 그 기기, 그 브라우저에만 남습니다.
파트 3. 데이터마다 서랍이 다르다
| 저장소 | 비유 | 이럴 때 | 무료 한도 |
|---|---|---|---|
| D1 | 엑셀 시트 | 목록, 검색, 사용자별 데이터 | 하루 쓴 행 10만, 읽은 행 500만 |
| KV | 이름표 메모지 | 설정, 캐시 | 하루 쓰기 1,000, 읽기 10만 |
| R2 | 창고 | 이미지, PDF, 업로드 파일 | 월 저장 10GB, 전송 무료 |
KV에 사용자 데이터를 넣지 마세요. 다른 지역에 보이기까지 60초 넘게 걸릴 수 있습니다. "저장이 됐다 안 됐다 해요"의 정체입니다.
파트 3. 시연
npx wrangler d1 create memo-db
npx wrangler d1 migrations create \
memo-db create_notes
npx wrangler d1 migrations apply \
memo-db --remoteawait env.DB
.prepare('INSERT INTO notes
(content, summary, created_at)
VALUES (?1, ?2, ?3)')
.bind(content, summary, now)
.run();이미 적용한 마이그레이션 파일은 고치지 않는다. 바꿀 게 생기면 새 파일을 만듭니다.
파트 3. 기억할 세 가지
--remote
빼면 로컬 DB에만 적용됩니다. 배포 후 "no such table"의 거의 전부.
${ 금지
SQL 문자열에 입력을 이어 붙이면 인젝션 구멍. AI 코드에서 찾아 .bind()로.
훑은 행
D1은 돌려준 행이 아니라 훑은 행을 셉니다. 자주 거르는 컬럼엔 인덱스.
파트 4. 로그인 붙이기
내 메모가 주소를 아는
모든 사람에게 보인다.
"로그인 만들어줘"의 결과물은 자주 위험합니다. 2026년 9월, 권장값대로 만든 비밀번호 저장 코드가 Workers 상한에 걸려 가입이 실패한 사례가 여러 건 나왔습니다.
파트 4. 원리: 문지기를 세운다
로그인 화면, 이메일 인증, 세션을 대시보드 설정만으로 얻습니다. Zero Trust 무료 플랜으로 소규모 팀까지.
파트 4. 시연
- Workers & Pages → 내 Worker → Access 탭 → Protect this Worker behind Access
- All traffic을 고른다. Previews only는 미리보기만 잠그고 실제 서비스는 열어 둡니다.
- 정책: Cloudflare account 또는 Email domain. Email domain에
gmail.com을 넣으면 전 세계 Gmail 사용자가 통과합니다. - 시크릿 창 → 이메일 → 메일로 온 코드 → 입장
- 메모 테이블에
user_email컬럼, 레이트 리밋 키도 이메일로
파트 4. 함정
정적 자산을 함께 서빙하는 Worker에는
ctx.access가 오지 않는다
공식 문서에 명시된 제약입니다. 대신 Cf-Access-Jwt-Assertion 헤더의 토큰을 검증합니다.
- 검증 없이 헤더 값을 믿지 않는다
- 공개키는 코드에 박지 않는다. 6주마다 바뀐다
iss(팀 도메인)와aud(AUD 태그)를 둘 다 확인한다
파트 4. 검증 코드 (jose)
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
export async function userEmail(request: Request, env: Env) {
const token = request.headers.get('cf-access-jwt-assertion');
if (!token) return null;
jwks ??= createRemoteJWKSet(new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`));
try {
const { payload } = await jwtVerify(token, jwks, {
issuer: env.TEAM_DOMAIN, audience: env.POLICY_AUD,
});
return typeof payload.email === 'string' ? payload.email.toLowerCase() : null;
} catch { return null; }
}파트 4. Access가 맞는 경우
| 상황 | 추천 |
|---|---|
| 나만, 팀만, 지인 베타 | Access |
| 공개 서비스의 관리자 페이지만 | 경로 단위 Access 앱 + workers.dev 닫기 |
| 누구나 가입하는 서비스 | 전용 인증 서비스 (Clerk, Supabase Auth, Auth0 등) |
어느 쪽이든 인증 코드는 직접 짜지 않습니다.
파트 5. 출시 후 운영
사용자는 "안 돼요"라고만 하고,
나는 뭐가 왜 안 되는지 모른다.
AI에게 "디자인 좀 바꿔줘"라고 했다가 저장 기능이 깨진 버전을 배포했습니다.
파트 5. 습관 세 가지
로그로 본다
설정 파일에 observability. 지금 요청은 wrangler tail, 지난 에러는 대시보드. 에러 한 줄을 AI에게.
되돌린다
wrangler rollback은 코드만 되돌린다. 데이터는 D1 Time Travel(무료 7일, 유료 30일).
커밋한다
잘 동작하면 커밋, 커밋하고 배포. "지금 상태 커밋해 줘"면 충분.
파트 5. 시간이 남으면
에러 묶음을 에이전트에게 (Issues)
같은 원인의 에러를 한 묶음으로 보여주고 Claude Code, Cursor 등에 넘깁니다. catch로 삼킨 에러는 console.error를 불러야 잡힙니다.
폼 스팸 막기 (Turnstile)
무료. 위젯만 붙이면 보호되지 않습니다. Worker에서 Siteverify로 토큰을 검증해야 합니다. 토큰은 300초, 한 번만.
한 시간 동안 바뀐 것
| 시작 | 끝 | |
|---|---|---|
| 주소 | localhost | 공개 주소, 내 도메인 |
| API 키 | 브라우저에 노출 | Worker 시크릿 |
| 남용 방지 | 없음 | 이메일 기준 레이트 리밋 |
| 메모 | 새로고침하면 사라짐 | D1에 저장 |
| 접근 | 누구나 | Access 로그인, 사용자별 메모 |
| 문제 대응 | "안 돼요" | 로그, 롤백, Git |
AI가 코드를 쓰게 하고,
나는 코드 바깥을 책임진다.
배포, 비밀, 데이터, 로그인, 운영. Cloudflare는 그 바깥을 한 곳에서 처리하게 해 줍니다.
과제
내 프로젝트 하나를
배포 전 체크리스트에 통과시키기
막히면 자료 페이지의 해당 파트 프롬프트를 그대로 AI에게 주세요. 명령어와 프롬프트에 복사 버튼이 있습니다.