Daheyo Works 강의 자료

바이브코더를 위한 Cloudflare막히는 순간을 푸는 1시간

AI는 코드를 잘 써 줍니다. 하지만 코드 바깥, 그러니까 배포하고, 키를 숨기고, 데이터를 남기고, 로그인을 붙이고, 고장 난 걸 고치는 일은 대신 해 주지 못합니다. 이 자료는 그 다섯 순간을 Cloudflare 하나로 넘는 순서를 담았습니다.

강의 Byung Kim내용 기준일 2026년 10월 2일읽는 데 약 25분

파트 1부터 읽기 배포 전 체크리스트

가운데는 AI가 써 주는 코드, 둘레는 내가 책임질 코드 바깥 다섯 가지: 배포, 비밀 키, 데이터, 로그인, 운영 배포파트 1 비밀 키파트 2 데이터파트 3 로그인파트 4 운영파트 5 코드AI가 써 준다
둘레의 다섯 칸이 코드 바깥입니다. 눌러서 해당 파트로 갑니다.

시작하기

오늘 고칠 앱: AI 메모장

메모를 쓰면 AI가 요약해 주는 HTML 페이지 하나입니다. AI가 만들어 준 그대로라서, 바이브코딩으로 만든 앱이 흔히 가진 문제를 다 갖고 있습니다.

지금 상태무엇이 문제인가고치는 곳
localhost에서만 돈다친구에게 링크를 보낼 수 없다파트 1
API 키가 JavaScript에 있다방문자 누구나 키를 복사해 내 돈으로 AI를 쓴다파트 2
새로고침하면 메모가 사라진다데이터가 브라우저 안에만 있다파트 3
로그인이 없다주소만 알면 남의 메모를 본다파트 4
고장 나면 알 길이 없다"안 돼요"라는 말만 듣는다파트 5

준비물: Cloudflare 계정(무료 플랜), GitHub 계정, 터미널에서 node -v가 동작하는 Node.js LTS, AI 코딩 도구 하나(Claude, Cursor 등). 내 도메인이 있으면 파트 1에서 연결해 볼 수 있습니다.

이 자료를 쓰는 법: 명령어와 AI에게 줄 프롬프트에는 복사 버튼이 있습니다. 각 파트 끝의 "막히면"은 실제로 많이 걸리는 지점을 모은 것이니, 막혔을 때 그 부분만 펼쳐 보세요. 요금과 한도는 기준일(2026년 10월 2일) 값이고 바뀔 수 있으니, 숫자가 중요한 결정 전에는 참고 문서의 공식 페이지를 다시 확인하세요.

파트 1

배포의 벽

친구한테 보여주려고 링크를 보냈는데, 그게 localhost:3000이었다.

localhost는 "내 컴퓨터 안에서만 통하는 주소"입니다. 다른 사람에게 보여주려면 앱을 인터넷에 올려야 합니다.

새 프로젝트는 Workers(정적 자산 포함)로 올립니다. 그리고 출력된 주소를 직접 열어 보는 것까지가 배포입니다.

원리

배포는 내 파일을 24시간 켜져 있는 남의 컴퓨터에 올려 두는 일입니다. Cloudflare는 그 컴퓨터를 전 세계 수백 곳에 두고, 방문자와 인터넷 경로상 가까운 곳에서 응답합니다.

HTML, CSS, 이미지 같은 정적 파일 요청은 무료이고 요청 수에도 세지 않습니다. Worker 코드가 실행되는 요청(API 등)만 요금과 한도에 들어갑니다.

왜 Pages가 아니라 Workers일까요? Cloudflare 공식 문서(Workers Best Practices)가 새 프로젝트는 Pages 대신 Workers로 시작하라고 안내합니다. Pages는 계속 동작하지만 새 기능은 Workers에 들어갑니다. 프론트와 API를 한 번에 올릴 수 있어서 이후 파트(시크릿, 데이터베이스, 로그)와도 그대로 이어집니다.

따라 하기

  1. 설정 파일을 만듭니다. 프로젝트 폴더에서 npm create cloudflare@latest로 만들거나, 아래 파일을 직접 만들어도 됩니다. 꼭 있어야 하는 칸은 name과 compatibility_date 둘뿐입니다.

    wrangler.jsonc
    {
      "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 }
    }

    directory는 빌드 결과 폴더(dist, build, out 등)입니다. not_found_handling은 화면 이동을 브라우저가 처리하는 앱(SPA)에서 새로고침 404를 막고, run_worker_first는 /api/* 요청을 항상 Worker가 받게 합니다. observability는 파트 5에서 볼 로그를 미리 켜 둡니다. API가 없는 정적 사이트라면 main과 run_worker_first를 뺍니다.

  2. Cloudflare에 로그인합니다. 브라우저 창이 열리면 승인합니다.

    터미널
    npx wrangler login
  3. 배포합니다. 끝나면 https://ai-memo.<내 계정>.workers.dev 같은 주소가 출력됩니다. 휴대폰으로 열어 보세요.

    터미널
    npx wrangler deploy

    "성공" 메시지만 믿지 말고 출력된 주소를 직접 열어 보세요. 같은 이름의 Pages 프로젝트가 있어서 성공이라고 떴는데 아무도 안 보는 주소로 올라간 사례가 있습니다.

  4. (도메인이 있다면) 내 도메인을 붙입니다. 도메인의 네임서버가 Cloudflare로 옮겨져 있어야 합니다. 설정 파일에 아래 칸을 더하고 다시 배포하면, DNS 레코드와 HTTPS 인증서를 Cloudflare가 알아서 만듭니다. 대시보드의 Worker 설정에서 도메인을 추가해도 됩니다.

    wrangler.jsonc에 추가
    "routes": [{ "pattern": "memo.example.com", "custom_domain": true }]

    지금 보고 계신 이 페이지도 같은 방법으로 learn.daheyo.com에 올라가 있습니다.

막히면

배포했는데 빈 화면만 나온다

대부분 assets.directory가 빌드 결과 폴더를 가리키지 않는 경우입니다. React, Vue, Next.js 같은 앱은 빌드를 먼저 하고 그 결과 폴더를 지정하세요.

첫 화면은 뜨는데 다른 주소에서 새로고침하면 404가 난다

화면 이동을 브라우저가 처리하는 앱(SPA)은 "not_found_handling": "single-page-application"을 직접 적어야 합니다. Pages처럼 404.html 유무로 알아서 판단하지 않습니다.

반대로 이 설정을 켜면 주소창에 /api/...를 직접 쳤을 때 Worker 대신 HTML이 나옵니다. API는 fetch로 부르거나 run_worker_first에 경로를 넣으세요.

올리면 안 되는 파일까지 올라간다

Workers는 Pages처럼 파일을 자동으로 빼 주지 않습니다. 정적 파일 폴더에 .assetsignore를 만들고 .DS_Store, 초안 파일 같은 이름을 적으세요.

내 도메인이 붙지 않는다

Workers 커스텀 도메인은 네임서버가 Cloudflare에 있어야만 붙습니다. 다른 곳의 DNS에 CNAME만 추가하는 방식은 안 됩니다.

호스트 이름도 정확히 같아야 합니다. example.com과 www.example.com은 따로 등록하고, 와일드카드(*.example.com)는 안 됩니다. 네임서버 변경은 반영에 시간이 걸릴 수 있습니다.

wrangler login 브라우저 창이 회사 네트워크에서 막힌다

대시보드에서 API 토큰을 만들어 CLOUDFLARE_API_TOKEN 환경 변수로 넣으면 로그인 없이 배포할 수 있습니다. 토큰은 비밀번호처럼 다루고 저장소에 올리지 마세요.

assets.bucket is a required field 오류가 난다

오래된 Wrangler의 문제입니다. 프로젝트의 Wrangler를 최신으로 올리면 사라집니다.

빌드는 되는데 배포 후에 런타임 오류가 난다

쓰는 라이브러리가 Workers에 없는 Node.js 전용 기능에 기대는 경우가 많습니다. 오류 메시지 전체를 AI에게 주고 Workers에서 도는 대안을 물어보세요. 배포 전에 의존성을 정적으로 검사해 주는 커뮤니티 도구(npx edgefit check)도 있습니다.

한국에서 접속하면 느린 것 같다

https://내주소/cdn-cgi/trace를 열고 colo= 값을 보세요. 요청을 처리한 Cloudflare 데이터센터 코드입니다. ICN이면 서울, LAX·SJC·SEA면 미국 서부입니다.

2026년 5~9월 Cloudflare 커뮤니티에 무료 플랜 커스텀 도메인으로 들어온 한국 방문자가 미국으로 간다는 보고가 여러 건 있었습니다. 한 사례에서는 첫 응답이 LAX 경유 466ms, 같은 네트워크의 ICN 경유 사이트가 35ms였습니다. 공식 원인 설명은 없고, 유료 플랜으로 올려도 바뀌지 않았다는 보고도 있습니다. 값은 시간과 통신사에 따라 바뀌니 여러 번 재 보세요.

AI에게 맡기기

프롬프트

이 프로젝트를 Cloudflare Workers의 static assets 기능으로 배포하고 싶어. Pages가 아니라 Workers로. wrangler.jsonc 파일을 만들어 주고, 빌드 결과물 폴더가 어디인지 확인해서 assets.directory를 맞춰 줘. 클라이언트 라우팅을 쓰는 앱이면 not_found_handling을 single-page-application으로 하고, /api/* 경로는 run_worker_first로 Worker가 먼저 받게 해. 배포에서 빠져야 할 파일은 .assetsignore에 넣어 줘. 내가 실행해야 할 명령어를 순서대로 알려주고, 배포 뒤 출력된 URL로 실제 접속해 확인하는 방법도 알려줘.

파트 2

키 노출과 비용 폭탄

배포한 사이트에서 F12를 눌렀더니, 내 AI API 키가 그대로 보였다.

브라우저로 내려간 JavaScript는 누구나 열어 볼 수 있습니다. 키가 그 안에 있으면 전 세계에 공개한 것과 같습니다. 이미 노출된 키는 숨기는 것으로 끝나지 않으니, AI 제공사 콘솔에서 폐기하고 새로 만드세요.

비밀 키는 브라우저에 두지 않습니다. Worker를 사이에 세워 대신 호출하게 하고, 그 앞에서 너무 잦은 요청을 거릅니다.

원리

고치기 전

브라우저JavaScript 안에 API 키
직접 호출
AI APIOpenAI, Anthropic 등

F12만 누르면 누구나 키를 복사해 내 돈으로 AI를 씁니다.

고친 뒤

브라우저/api/summarize만 부른다
요청
내 Worker① 너무 잦은 요청을 거른다
② 시크릿 키를 붙인다
대신 호출
AI API

키는 Worker의 시크릿 안에만 있고, 브라우저 코드에는 없습니다.

따라 하기

  1. 프론트에서 키를 지웁니다. AI API를 직접 부르던 코드를 fetch('/api/summarize', ...)로 바꿉니다. AI에게 시키면 30초면 됩니다.

  2. Worker에 대신 부르는 경로를 만듭니다. 키는 env에서만 읽습니다.

    src/worker.ts (뼈대)
    export default {
      async fetch(request: Request, env: Env): Promise<Response> {
        const url = new URL(request.url);
        if (url.pathname === '/api/summarize' && request.method === 'POST') {
          const { text } = await request.json<{ text: string }>();
          // callAi는 쓰는 AI 제공사에 맞게 AI에게 만들게 한다. 키는 env에서만 꺼낸다
          const summary = await callAi(text, env.AI_API_KEY);
          return Response.json({ summary });
        }
        return new Response('Not found', { status: 404 });
      },
    };
  3. 키를 시크릿으로 등록합니다. 실행하면 값을 묻고, 새 버전을 만들어 바로 배포까지 합니다. 등록한 값은 Wrangler에서도 대시보드에서도 다시 볼 수 없습니다.

    터미널
    npx wrangler secret put AI_API_KEY

    대시보드에서 넣을 때는 Worker의 Settings → Variables and Secrets에서 유형을 Secret으로 고르고, Deploy를 눌러야 반영됩니다.

  4. 로컬 개발용 키는 .dev.vars에 두고, 저장소에 안 올라가게 합니다. GitHub에서 키가 새는 경우 대부분이 여기서 생깁니다.

    .dev.vars (내 컴퓨터에만)
    AI_API_KEY=여기에-개발용-키
    .gitignore에 추가
    .dev.vars*
    .env*

    .dev.vars와 .env는 둘 중 하나만 쓰세요. .dev.vars가 있으면 .env의 값은 Worker에 들어가지 않습니다.

  5. 레이트 리밋을 겁니다. 같은 사람이 너무 자주 부르면 429를 돌려줍니다. 기간(period)은 10초나 60초만 됩니다.

    wrangler.jsonc에 추가
    "ratelimits": [
      {
        "name": "LIMITER",
        "namespace_id": "1001",
        "simple": { "limit": 10, "period": 60 }
      }
    ]
    src/worker.ts, AI를 부르기 전에
    const ip = request.headers.get('cf-connecting-ip') ?? 'unknown';
    const { success } = await env.LIMITER.limit({ key: ip });
    if (!success) return new Response('잠시 후 다시 시도해 주세요', { status: 429 });

    namespace_id가 같으면 다른 Worker와 카운터를 같이 씁니다. 앱마다 다른 숫자를 쓰세요. Wrangler 4.36.0 이상이 필요하고, 이 바인딩은 대시보드에 나타나지 않습니다(429는 로그에서 확인).

  6. 다시 배포하고 F12로 확인합니다. Sources 탭과 빌드 결과 폴더(dist) 어디에도 키가 없어야 합니다.

돈은 어디서 새는가

  • Cloudflare 무료 플랜은 넘으면 멈춥니다. Workers는 하루 10만 요청까지이고 매일 한국 시간 오전 9시(00:00 UTC)에 초기화됩니다. 넘으면 과금이 아니라 1027 오류로 요청이 멈춥니다. D1·KV도 무료 한도를 넘으면 해당 작업이 오류로 실패합니다.
  • 유료 플랜은 멈추지 않습니다. 월 $5부터이고 쓴 만큼 과금됩니다. 사용을 자동으로 멈추는 지출 상한은 공식 문서에서 찾지 못했습니다. Billing → Billable Usage의 Budget alert는 하루 한 번 집계해 다음 날 이메일을 보내는 알림이고, 사용을 막지는 않습니다.
  • 진짜 비용 폭탄은 AI API 쪽입니다. OpenAI, Anthropic 등 AI 제공사 콘솔에서 월 사용 한도를 꼭 걸어 두세요.
  • 레이트 리밋은 정밀한 계량기가 아니라 폭주 차단기입니다. 카운터를 데이터센터마다 따로 세고 느슨하게 맞춥니다. "하루 10회" 같은 상한은 이걸로 못 만듭니다. 하루 상한이 필요하면 AI 호출을 D1에 한 줄씩 기록하고, 호출 전에 오늘 건수를 세서 넘으면 거절하세요. 사용자별 상한과 앱 전체 상한을 둘 다 두면, 봇 하나가 무료 할당을 다 써서 정상 사용자까지 막히는 일을 피할 수 있습니다.

막히면

로컬에서 "키가 없다"는 오류가 난다

wrangler secret put은 배포된 Worker에 적용됩니다. 로컬(wrangler dev)에서는 .dev.vars의 값을 씁니다. 파일이 설정 파일과 같은 폴더에 있는지 확인하세요.

AI가 "환경 변수로 옮겼어요"라고 했는데 F12에 키가 또 보인다

빌드 도구의 공개 접두사가 붙은 변수(VITE_, NEXT_PUBLIC_ 등)는 빌드할 때 브라우저 코드에 그대로 박힙니다. Vite 공식 문서도 VITE_ 변수에 민감한 값을 넣지 말라고 경고합니다. 비밀 키는 Worker의 시크릿에만 두세요.

AI가 키를 wrangler.jsonc의 vars에 넣었다

vars는 암호화되지 않은 평문이고, 설정 파일과 함께 저장소에 올라갑니다. 비밀 값은 wrangler secret put으로 옮기고, 이미 커밋했다면 키를 폐기하세요.

대시보드에서 바꾼 값이 다음 배포 뒤 원래대로 돌아갔다

대시보드에서 바꾼 vars는 다음 wrangler deploy 때 설정 파일 값으로 덮어써질 수 있습니다. 설정 파일을 고치거나 --keep-vars를 쓰세요. 시크릿은 배포로 지워지지 않습니다.

레이트 리밋이 정확히 10번째에서 걸리지 않는다

정상입니다. 카운터가 데이터센터별로 따로 돌고 느슨하게 갱신됩니다. 공식 문서도 정확한 계량 용도가 아니라고 밝힙니다.

AI가 짠 레이트 리밋이 전혀 안 먹는다

흔한 오답 두 가지입니다.

  • Worker 메모리의 Map에 횟수를 세는 코드: Worker 인스턴스마다 따로 세서 "분당 5회"가 "인스턴스당 5회"가 됩니다.
  • KV에 횟수를 세는 코드: 같은 키는 1초에 한 번만 쓸 수 있고, 무료 플랜 쓰기는 하루 1,000회뿐이며, 다른 지역에 늦게 반영돼 동시 요청이 통과합니다.

위의 Rate Limiting 바인딩을 쓰세요.

IP 말고 무엇을 키로 써야 하나

Cloudflare 문서는 IP를 키로 쓰는 것을 권하지 않습니다. 같은 IP를 여러 사람이 함께 쓰기 때문입니다. 로그인이 없는 앱에서는 현실적인 타협이고, 파트 4에서 로그인을 붙이면 이메일로 바꿉니다. IP는 cf-connecting-ip 헤더에서 읽으세요. X-Forwarded-For의 첫 값은 사용자가 바꿔 보낼 수 있습니다.

Pages에서 시크릿을 넣었는데 프로덕션이 못 본다

Pages는 시크릿을 넣은 뒤 다시 배포해야 반영됩니다. Workers의 wrangler secret put은 바로 배포됩니다.

AI에게 맡기기

프롬프트

지금 프론트엔드 코드에 AI API 키가 하드코딩돼 있어. 이걸 Cloudflare Worker 프록시로 옮겨줘. 브라우저는 /api/summarize만 호출하고, Worker가 env.AI_API_KEY 시크릿으로 실제 API를 호출하게 해. 키는 wrangler.jsonc의 vars나 VITE_·NEXT_PUBLIC_ 같은 공개 접두사 변수에 넣지 마. 로컬 개발용 .dev.vars 파일을 만들고 .gitignore에 .dev.vars*와 .env*를 넣어 줘. Workers Rate Limiting 바인딩(ratelimits, period 60, limit 10)으로 제한을 걸고 key는 cf-connecting-ip 헤더로 해. 메모리 Map이나 KV로 횟수를 세는 방식은 쓰지 마. 넘으면 429를 돌려줘. 마지막에 빌드 결과물(dist 폴더)에 키 문자열이 남아 있지 않은지 검사해 줘.

파트 3

데이터가 사라진다

메모를 세 개 썼는데, 새로고침하니 다 사라졌다.

localStorage로 고쳐도 휴대폰에서 열면 없습니다. 브라우저 저장은 "그 기기, 그 브라우저"에만 남습니다.

목록으로 쌓이고 검색되는 데이터는 D1에 넣습니다. KV와 R2는 언제 쓰는지만 알면 충분합니다.

원리: 데이터마다 서랍이 다르다

저장소비유이럴 때 쓴다무료 한도
D1엑셀 시트(표)목록, 검색, 정렬, 사용자별 데이터. 예: 메모 목록하루 읽은 행 500만, 쓴 행 10만. 저장 5GB(DB 하나 500MB)
KV이름표 붙은 메모지자주 읽고 드물게 바뀌는 설정, 캐시. 예: 오늘의 안내 문구, 기능 켜고 끄기하루 읽기 10만, 쓰기 1,000
R2창고(파일)이미지, PDF, 업로드 파일. 예: 메모에 첨부한 사진월 저장 10GB, 쓰기 작업 100만, 읽기 작업 1,000만. 내려받기 전송은 무료

하루 한도는 매일 한국 시간 오전 9시(00:00 UTC)에 초기화되고, R2는 월 단위입니다.

KV에 사용자 데이터를 넣지 마세요. KV에 쓴 값이 다른 지역에 보이기까지 60초 넘게 걸릴 수 있고, "그 키가 없다"는 결과도 잠시 기억됩니다. 같은 키는 1초에 한 번만 쓸 수 있습니다. 방금 쓴 메모를 바로 다시 읽어야 하는 앱에서 "저장이 됐다 안 됐다 한다"는 증상이 이렇게 생깁니다. 공식 문서도 설정과 캐시는 KV에, 사용자 프로필이나 주문 같은 데이터는 D1에 두라고 권합니다.

따라 하기

  1. 데이터베이스를 만듭니다. 출력된 바인딩 설정(d1_databases)을 wrangler.jsonc에 붙여 넣습니다.

    터미널
    npx wrangler d1 create memo-db
  2. 테이블을 마이그레이션 파일로 만듭니다. 나중에 컬럼을 바꿀 때도 "새 파일 하나"로 끝납니다.

    터미널
    npx wrangler d1 migrations create memo-db create_notes
    migrations/0001_create_notes.sql
    CREATE TABLE notes (
      id         INTEGER PRIMARY KEY AUTOINCREMENT,
      content    TEXT NOT NULL,
      summary    TEXT NOT NULL DEFAULT '',
      created_at TEXT NOT NULL
    );
    터미널: 운영 DB에 적용
    npx wrangler d1 migrations apply memo-db --remote

    규칙은 하나입니다. 이미 적용한 파일은 고치지 않고, 바꿀 게 생기면 새 파일을 만듭니다.

  3. Worker에 목록과 저장 경로를 만듭니다. SQL은 AI에게 맡기고, 구조만 확인하세요. 사용자 입력은 반드시 .bind()로 넘깁니다.

    src/worker.ts 중에서
    // GET /api/notes: 목록
    const { results } = await env.DB
      .prepare('SELECT id, content, summary, created_at FROM notes ORDER BY created_at DESC LIMIT 50')
      .all();
    
    // POST /api/notes: 저장. 입력은 문자열로 이어 붙이지 않고 bind()로 넘긴다
    await env.DB
      .prepare('INSERT INTO notes (content, summary, created_at) VALUES (?1, ?2, ?3)')
      .bind(content, summary, new Date().toISOString())
      .run();
  4. 배포하고 확인합니다. 메모 작성 → 새로고침 → 휴대폰에서 열기. 둘 다 남아 있어야 합니다.

  5. 대시보드의 D1 화면에서 실제 행을 봅니다. 내 데이터가 어디 있는지 눈으로 확인해 두면 다음에 덜 불안합니다. 같은 화면의 Metrics에서 읽은 행과 쓴 행 수도 볼 수 있습니다.

꼭 알아 둘 두 가지

보안: SQL에 사용자 입력을 문자열로 이어 붙이면 SQL 인젝션 구멍이 됩니다. AI가 만든 코드에서 SQL 문자열 안에 ${가 보이면 .bind()로 바꿔 달라고 하세요.

비용: D1은 "돌려준 행"이 아니라 "훑은 행"을 셉니다. 결과가 3줄이어도 인덱스 없이 테이블 전체를 훑으면 전체 행 수가 읽은 행으로 잡힙니다. 자주 거르는 컬럼에는 인덱스를 거세요. 대시보드나 Wrangler로 실행한 쿼리도 사용량에 들어갑니다.

막히면

배포했는데 "no such table" 오류가 난다

--remote를 빼고 실행해서 로컬 개발용 DB에만 테이블이 생긴 경우가 거의 전부입니다. d1 migrations apply, d1 execute, kv key put은 모두 기본값이 로컬입니다.

로컬에서 쓴 메모가 배포한 사이트에는 없다

wrangler dev는 프로젝트의 .wrangler/ 폴더 안에 있는 로컬 DB를 씁니다. 로컬과 운영은 서로 다른 데이터베이스입니다.

마이그레이션이 중간에 실패했다

실패한 파일만 되돌려지고, 앞서 성공한 파일은 적용된 채로 남습니다. npx wrangler d1 migrations list memo-db --remote로 어디까지 적용됐는지 보고, 고친 내용은 새 파일로 이어 가세요.

사용자가 주로 한국에 있다

D1은 따로 정하지 않으면 만드는 요청을 보낸 곳 근처에 생깁니다. 위치 힌트는 apac 같은 권역 단위로만 줄 수 있고(서울 단위는 없음), 힌트는 보장이 아닙니다.

터미널
npx wrangler d1 create memo-db --location apac
무료 한도를 넘으면 어떻게 되나

D1 무료 플랜은 하루 한도를 넘으면 쿼리가 오류로 실패하고, 한국 시간 오전 9시에 풀립니다. 유료 플랜으로 올리면 보통 몇 분 안에 풀립니다. 공개 페이지라면 검색 엔진과 크롤러도 DB를 부른다는 점을 기억하세요.

사진이나 파일을 저장하고 싶다 (R2)

R2 버킷은 기본이 비공개입니다. r2.dev 공개 주소는 개발·테스트용이라 속도 제한이 있고 캐시나 접근 제어를 쓸 수 없으니, 서비스용 주소는 처음부터 내 도메인으로 연결하세요. 큰 파일은 Worker를 거치면 요청 크기 제한에 걸리므로, 서명된 업로드 주소(presigned URL)로 브라우저에서 R2에 바로 올립니다.

AI에게 맡기기

프롬프트

메모를 Cloudflare D1에 저장하고 싶어. wrangler d1 migrations create로 notes 테이블 마이그레이션 파일을 만들어 주고, Worker에 GET /api/notes, POST /api/notes 경로를 추가해 줘. SQL에는 반드시 prepare().bind()를 써서 사용자 입력을 넘겨. 목록 조회에 쓰는 컬럼에는 인덱스를 걸어 줘. 원격 DB에 마이그레이션을 적용하는 wrangler 명령어(--remote 포함)도 알려줘.

파트 4

로그인 붙이기

내가 쓴 메모가, 주소를 아는 사람 모두에게 보인다.

AI에게 "로그인 만들어줘"라고 하면 비밀번호를 그대로 저장하거나 세션 처리를 빠뜨린 코드가 자주 나옵니다. 제대로 만들려고 해도 함정이 많습니다. 2026년 9월에는 보안 권장값(PBKDF2 60만 회)대로 만든 비밀번호 저장 코드가 Workers의 반복 횟수 상한(10만 회)에 걸려, 운영에서 가입과 로그인이 실패한 수정 사례가 여러 프로젝트에 올라왔습니다.

로그인을 직접 만들지 않습니다. Cloudflare Access를 앱 앞에 문지기로 세우면, 로그인 화면과 이메일 인증과 세션을 대시보드 설정만으로 얻습니다.

원리

방문자앱 주소로 접속
먼저 검사
Cloudflare Access허용된 사람인가?
아니오
로그인 화면이메일로 받은 일회용 코드 입력
예
내 Worker토큰을 검증해 이메일 확인 → 그 이메일의 메모만

로그인하지 않은 방문자는 Worker에 닿지 못합니다. Worker는 "누가 왔는지"만 받아서 씁니다.

기본 로그인 방식은 이메일로 받는 일회용 코드(10분 뒤 만료)이고, Google이나 GitHub 로그인도 켤 수 있습니다. Zero Trust 무료 플랜으로 소규모 팀(50명 규모)까지 쓸 수 있습니다. 정확한 기준은 Zero Trust 요금 페이지에서 확인하세요.

따라 하기

  1. Zero Trust를 켭니다. 계정에서 처음 쓰면 팀 이름을 정하는 활성화 절차가 먼저 나옵니다.

  2. Worker에 Access를 겁니다. 대시보드 Workers & Pages → 내 Worker → Access 탭 → Protect this Worker behind Access → All traffic.

    Previews only를 고르면 미리보기 주소만 잠기고 실제 서비스는 열려 있습니다. 가장 흔한 착각입니다. All traffic이면 workers.dev 주소, 내 도메인, 미리보기 주소가 한 번에 보호되고, 나중에 도메인을 더 붙여도 유지됩니다. 옆의 "Protect all Workers"는 앞으로 만들 Worker까지 계정의 모든 Worker를 잠그니, 공개할 Worker가 있다면 누르지 마세요.

  3. 누구를 들일지 정합니다. 간편 화면의 선택지는 두 가지입니다.

    • Cloudflare account: 내 Cloudflare 계정의 구성원. 나만 쓰는 도구라면 이걸로 충분합니다.
    • Email domain: 특정 도메인의 이메일. 회사 도메인이 있을 때 씁니다. gmail.com을 넣으면 전 세계 Gmail 사용자가 통과합니다.

    지인 몇 명처럼 특정 이메일만 들이려면 Zero Trust → Access에서 해당 앱의 정책을 편집해 이메일 주소를 넣으세요.

  4. 시크릿 창으로 확인합니다. 로그인 화면 → 이메일 입력 → 메일로 온 코드 입력 → 앱 진입. 허용하지 않은 이메일로도 시도해 막히는지 봅니다.

  5. 메모를 사용자별로 나눕니다. 새 마이그레이션으로 이메일 컬럼과 인덱스를 더하고, 저장과 조회를 로그인한 이메일 기준으로 바꿉니다.

    migrations/0002_add_user_email.sql
    ALTER TABLE notes ADD COLUMN user_email TEXT;
    CREATE INDEX notes_user ON notes (user_email, created_at);
  6. 레이트 리밋 키를 IP에서 이메일로 바꿉니다. 파트 2에서 "익명 앱의 타협"이라고 했던 부분입니다.

함정: Worker에서 로그인한 이메일 읽기

Cloudflare 문서는 ctx.access로 로그인 정보를 바로 읽는 방법을 안내합니다. 하지만 정적 자산을 함께 서빙하는 Worker에는 ctx.access가 전달되지 않습니다(공식 문서에 명시). 오늘 만든 앱이 바로 그 경우라, AI가 이 방법으로 코드를 짜 주면 로그인했는데도 값이 비어 있습니다. Vite 플러그인처럼 배포 설정에 정적 자산을 자동으로 넣는 도구를 쓰면, 내가 적지 않았어도 해당될 수 있습니다.

대신 요청 헤더 Cf-Access-Jwt-Assertion에 담긴 토큰(JWT)을 검증해서 이메일을 꺼냅니다. 이 세 가지만은 직접 확인하세요.

  • 검증 없이 헤더 값을 믿지 않습니다. Cf-Access-Authenticated-User-Email 같은 헤더를 그대로 쓰는 코드도 받지 마세요.
  • 공개키는 코드에 박지 않습니다. 기본 6주마다 바뀝니다. https://팀이름.cloudflareaccess.com/cdn-cgi/access/certs에서 받게 합니다.
  • 발급자(iss)와 대상(aud)을 둘 다 확인합니다. aud는 Access 앱의 AUD 태그이고, 앱을 지우고 다시 만들면 바뀝니다.
src/auth.ts (jose 라이브러리 사용)
import { createRemoteJWKSet, jwtVerify } from 'jose';

let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;

export async function userEmail(request: Request, env: Env): Promise<string | null> {
  const token = request.headers.get('cf-access-jwt-assertion');
  if (!token) return null;
  // 공개키는 주기적으로 바뀐다. 코드에 고정하지 않고 certs 주소에서 받는다
  jwks ??= createRemoteJWKSet(new URL(`${env.TEAM_DOMAIN}/cdn-cgi/access/certs`));
  try {
    const { payload } = await jwtVerify(token, jwks, {
      issuer: env.TEAM_DOMAIN,   // https://팀이름.cloudflareaccess.com
      audience: env.POLICY_AUD,  // Access 앱의 AUD 태그
    });
    return typeof payload.email === 'string' ? payload.email.toLowerCase() : null;
  } catch {
    return null;
  }
}

TEAM_DOMAIN과 POLICY_AUD는 공식 예제가 쓰는 이름입니다. 둘 다 비밀이 아니라서 wrangler.jsonc의 vars에 둬도 됩니다. 이 자료를 만든 Daheyo Works의 내부 도구도 같은 구조(정적 자산 + Access + jose 검증)로 운영하고 있습니다.

Access가 맞는 경우와 아닌 경우

상황추천
나만 쓰는 도구, 팀 내부 도구, 지인 대상 비공개 베타Access
서비스는 공개, 관리자 페이지만 잠그기Zero Trust에서 example.com/admin 같은 경로 단위 Access 앱을 만들고, "workers_dev": false로 workers.dev 우회로를 닫습니다
누구나 가입하는 공개 서비스전용 인증 서비스(Clerk, Supabase Auth, Auth0 등)

Access는 원래 직원과 협력사가 회사 내부 도구에 들어가도록 만든 문지기입니다. 회원가입, 프로필, 비밀번호 찾기가 필요해지면 전용 인증 서비스로 넘어가세요. 어느 쪽이든 인증 코드를 직접 짜지 않는 것이 원칙입니다.

막히면

로그인 코드 메일이 안 온다

먼저 스팸함을 보세요. 그래도 없다면 정책 문제입니다. 허용되지 않은 이메일을 넣어도 화면은 "메일을 보냈다"고 나오지만 실제로는 보내지 않습니다.

로그인 화면은 있는데 아무나 들어온다

정책을 "모든 사람(Everyone)"으로 열었거나, 로그인 방식(One-time PIN)만 조건으로 넣었거나, gmail.com 같은 공용 도메인을 허용한 경우입니다. 이메일을 아는 것과 접근을 막는 것은 다릅니다.

로그인했는데 ctx.access가 비어 있다

정적 자산을 함께 서빙하는 Worker라서 그렇습니다. 위의 JWT 검증 방식을 쓰세요.

잘 되던 로그인이 어느 날 전부 실패한다

공개키를 코드에 고정해 둔 경우(키는 주기적으로 바뀝니다), 또는 Access 앱을 지우고 다시 만들어 AUD 태그가 바뀐 경우입니다.

WebSocket 연결만 403으로 실패한다

Worker 단위 Access는 WebSocket 연결 요청에 403을 돌려줍니다. 응답 본문도 설명 헤더도 없어 원인을 찾기 어렵습니다. 실시간 기능이 있는 앱은 Zero Trust에서 호스트 이름 기반 Access 앱으로 보호하세요.

한참 뒤에 저장을 누르니 Unexpected token '<' 오류가 난다

로그인 세션이 끝나서 API 요청이 JSON 대신 Access 로그인 페이지로 넘겨진 것입니다. 응답이 JSON이 아니면 새로고침을 안내하도록 프론트를 고치세요.

로컬(wrangler dev)에서는 로그인을 어떻게 테스트하나

로컬에는 Access가 없습니다. .dev.vars에만 둔 개발용 플래그가 있고 주소가 localhost일 때만 정해 둔 테스트 이메일로 통과시키는 식으로 만드세요. 두 조건을 함께 봐야 운영에 플래그가 잘못 들어가도 뚫리지 않습니다.

다른 사이트에서 내 API로 쓰기 요청을 보내면?

Access 로그인 쿠키는 다른 사이트에서 보낸 폼 요청에도 실려 옵니다. 저장·삭제 같은 쓰기 요청은 Sec-Fetch-Site: same-origin이나 Origin 헤더로 같은 사이트에서 온 요청인지 한 번 더 확인하세요.

AI에게 맡기기

프롬프트

이 Cloudflare Worker는 Cloudflare Access로 보호돼 있고 static assets도 함께 서빙해. 그래서 ctx.access 대신, 요청 헤더 Cf-Access-Jwt-Assertion의 JWT를 jose 라이브러리로 검증해서 이메일을 꺼내는 함수를 만들어 줘. 공개키는 createRemoteJWKSet으로 TEAM_DOMAIN/cdn-cgi/access/certs에서 받고(키를 코드에 고정하지 마), issuer는 TEAM_DOMAIN, audience는 POLICY_AUD로 검증해. 두 값은 wrangler.jsonc의 vars에 둬. 검증 실패면 401을 돌려줘. Cf-Access-Authenticated-User-Email 같은 헤더는 믿지 마. notes 테이블에 user_email 컬럼과 (user_email, created_at) 인덱스를 추가하는 새 마이그레이션 파일을 만들고, 메모 저장과 조회를 그 이메일 기준으로 바꿔 줘. 레이트 리밋 key도 이메일로 바꿔.

파트 5

출시 후 운영

사용자는 "안 돼요"라고만 하고, 나는 뭐가 왜 안 되는지 모른다.

AI에게 "디자인 좀 바꿔줘"라고 했다가 저장 기능이 깨진 버전을 배포한 상황입니다. 이 파트는 명령어보다 습관 세 가지를 남기는 것이 목표입니다.

에러는 로그로 보고, 망가지면 되돌리고, 배포 전에 커밋합니다.

습관 1. 로그 보기

  • 로그 저장은 설정 파일에서 켭니다. 파트 1에서 넣은 "observability": { "enabled": true } 덕분에 대시보드에서 요청별 로그와 에러를 검색할 수 있습니다. 대시보드에서만 켜면 다음 배포 때 꺼졌다는 보고가 있으니 설정 파일에 적어 두세요.
  • 지금 들어오는 요청은 npx wrangler tail로 봅니다. 켜 둔 채로 사이트를 쓰면 요청과 에러가 실시간으로 흐릅니다. 다만 tail은 저장하지 않아서 이미 지나간 에러는 볼 수 없습니다. 지난 에러는 대시보드 로그에서 찾습니다.
  • 로그는 문자열 대신 객체로 찍습니다. console.log({ route: '/api/notes', userEmail, error: err.message })처럼 찍으면 대시보드에서 항목별로 걸러 볼 수 있습니다. 키, 토큰, 개인정보는 로그에 넣지 마세요.
  • 에러 로그 한 줄을 복사해 AI에게 주세요. "안 돼, 고쳐줘"보다 훨씬 빨리 끝납니다.

무료 플랜 로그는 하루 20만 건, 3일 보관입니다. 2026년 12월 1일부터는 용량 기준(무료는 하루 0.5GB 수집, 7일 보관)으로 바뀐다고 공지돼 있습니다.

같은 에러를 묶어서 보고, 코딩 에이전트에게 넘기고 싶다 (Issues)

2026년에 나온 Workers Issues(오픈 베타, 베타 기간 무료)는 같은 원인의 에러를 한 묶음으로 보여주고, 그 묶음을 Claude Code, Cursor, Devin 같은 코딩 에이전트에 넘겨 고칠 방안을 받게 할 수 있습니다. 수정과 배포는 사람이 합니다. 설정 파일의 observability 안에 "issues": { "enabled": true }를 넣고 배포하면 켜집니다(Wrangler 4.134.0 이상). 켜기 전에 난 에러는 보이지 않습니다.

함정 하나: try/catch로 잡아서 대체 응답을 돌려주는 코드는 자동으로 잡히지 않습니다. catch 안에서 console.error(err)를 불러야 기록됩니다. AI가 짠 코드에 아주 흔한 모양입니다.

습관 2. 되돌리기

대시보드의 Worker → Deployments에서 돌아갈 버전 옆 메뉴의 Rollback을 누르거나, 터미널에서 실행합니다. 버전 ID를 생략하면 가장 최근 버전 바로 전에 업로드된 버전으로 돌아가고, 대시보드에서는 최근 게시된 100개 버전 중에서 고를 수 있습니다.

터미널
npx wrangler rollback

롤백은 코드만 되돌립니다. D1에 쌓인 데이터와 테이블 구조는 그대로입니다. 테이블 구조를 바꾼 배포를 롤백하면 옛 코드가 새 테이블과 맞지 않아 에러가 날 수 있습니다. 롤백할 버전이 쓰던 R2 버킷이나 KV를 그 사이 지웠다면 롤백 자체가 막힙니다.

데이터를 되돌릴 때는 D1 Time Travel을 씁니다. 따로 켜지 않아도 항상 동작하고, 무료 플랜은 최근 7일, 유료 플랜은 30일 안에서 분 단위 시점으로 DB를 되돌립니다. 복구하면 그 시점 이후 데이터가 덮어써지므로, 복구 전에 지금 시점을 먼저 적어 두면 복구를 다시 되돌릴 수 있습니다.

터미널
# 1. 지금 시점의 북마크를 적어 둔다
npx wrangler d1 time-travel info memo-db
# 2. 원하는 시점으로 되돌린다
npx wrangler d1 time-travel restore memo-db --timestamp=2026-10-02T09:00:00+09:00

습관 3. 배포 전에 커밋하기

롤백은 "배포된 것"을 되돌리고, Git은 "내 코드"를 되돌립니다. 둘 다 있어야 안심입니다. 규칙은 하나입니다. 잘 동작하면 커밋하고, 커밋한 다음 배포합니다. AI에게 "지금 상태 커밋해 줘, 메시지는 알아서"라고 시키면 됩니다.

익숙해지면 GitHub 저장소를 Worker에 연결(Workers Builds)해서, 프로덕션 브랜치에 push할 때마다 자동 배포되게 할 수 있습니다. 다른 브랜치는 미리보기 주소로 올라갑니다. 두 가지만 알아 두세요.

  • 새 방식 미리보기(Worker Previews)는 프로덕션의 변수, 시크릿, 바인딩을 쓰지 않습니다. 미리보기에서 "키가 없다"는 오류가 나면 미리보기용 값을 따로 넣어야 합니다.
  • 무료 플랜은 빌드를 한 번에 하나만 돌립니다. AI 에이전트가 커밋을 자주 올리면 빌드가 줄을 섭니다.

덤: 봇과 스팸 막기

공개된 폼은 금방 스팸 봇에게 발견됩니다. 무료인 Turnstile(Cloudflare의 CAPTCHA 대체)을 메모 저장 폼에 붙이면 사람만 저장할 수 있습니다. 위젯만 붙이면 보호되지 않습니다. Worker에서 Siteverify API로 토큰을 검증해야 하고, 토큰은 300초 동안 한 번만 쓸 수 있으며, secret key는 Worker 시크릿에만 둡니다. 파트 2의 레이트 리밋, 하루 상한과 함께 쓰면 AI 요금을 지키는 장치가 세 겹이 됩니다.

AI에게 맡기기

프롬프트

배포한 Cloudflare Worker에서 아래 에러가 났어. [wrangler tail 또는 대시보드 로그에서 복사한 에러 붙여넣기] 원인이 뭔지 설명하고, 고친 코드를 보여줘. 고치기 전에 지금 상태를 git 커밋해 줘. 고치는 김에 catch 블록에서 에러를 삼키는 곳이 있으면 console.error로 남기고, 로그는 문자열 대신 객체로 찍게 바꿔 줘.

정리

한 시간 동안 바뀐 것

강의 시작강의 끝
주소localhost공개 주소, 내 도메인
API 키브라우저 코드에 노출Worker 시크릿
남용 방지없음이메일 기준 레이트 리밋, 필요하면 Turnstile과 하루 상한
메모새로고침하면 사라짐D1에 저장, Time Travel로 되돌리기 가능
접근주소만 알면 누구나Access 로그인, 사용자별 메모
문제 대응"안 돼요"로그 확인, 롤백, Git
비용모름무료 한도와 AI 콘솔 한도를 앎

AI가 코드를 쓰게 하고, 나는 코드 바깥(배포, 비밀, 데이터, 로그인, 운영)을 책임집니다. Cloudflare는 그 바깥을 한 곳에서 처리하게 해 줍니다.

과제

내가 만든 프로젝트 하나를 골라 아래 배포 전 체크리스트를 통과시켜 보세요. 막히면 해당 파트의 프롬프트를 그대로 AI에게 주면 됩니다.

질문

자주 묻는 질문

Vercel, Supabase랑 뭐가 달라요?

각각 배포와 데이터베이스에 강한 도구이고, 섞어 써도 됩니다. Cloudflare의 장점은 배포, 시크릿, 데이터베이스, 파일, 보안이 한 계정과 한 설정 파일에 모인다는 점입니다.

무료로 얼마나 버틸 수 있어요?

개인 프로젝트는 대부분 무료 플랜으로 충분합니다. Workers는 하루 10만 요청, D1은 하루 쓴 행 10만이고 한국 시간 오전 9시에 초기화됩니다. 무료 플랜은 한도를 넘으면 과금이 아니라 요청이 멈춥니다. 돈은 AI API 쪽에서 나가니 그쪽 한도를 꼭 거세요.

유료로 올리면 지출 한도를 걸 수 있어요?

사용을 자동으로 멈추는 상한은 공식 문서에서 찾지 못했습니다. Billing → Billable Usage에서 Budget alert를 걸 수 있지만, 다음 날 오는 이메일 알림이고 사용을 막지 않습니다. 유료 플랜은 월 $5부터이고, 공식 예시로 월 1,500만 요청(요청당 CPU 7ms)이면 $8입니다.

사용자가 50명을 넘으면요?

Access는 50명 규모의 비공개 앱에 맞습니다. 원래 직원과 협력사용 도구라서, 누구나 가입하는 공개 서비스가 되면 Clerk, Supabase Auth 같은 전용 인증 서비스로 넘어가세요.

로그인을 AI한테 직접 만들게 하면 안 돼요?

비밀번호 저장, 세션, 비밀번호 재설정 메일까지 챙길 게 많고, 플랫폼 제약도 알아야 합니다. 2026년 9월에도 보안 권장값대로 만든 비밀번호 저장 코드가 Workers의 반복 횟수 상한에 걸려 가입이 안 되는 사고가 여러 프로젝트에서 났습니다. 전용 서비스를 쓰세요.

한국에서 접속하면 느린 것 같아요.

https://내주소/cdn-cgi/trace의 colo= 값을 보세요. ICN이 아니라 LAX 등이면 미국 데이터센터를 거치는 중입니다. 2026년에 같은 보고가 여러 건 있었고 공식 원인 설명은 없습니다. 유료 플랜으로 올려도 바뀌지 않았다는 보고도 있으니 먼저 측정부터 하세요.

체크리스트

배포 전 체크리스트

0 / 0

체크한 항목은 이 브라우저에만 저장됩니다.

배포

비밀 키

비용

데이터

운영

로그인 (비공개 앱이라면)

더 읽기

참고 문서와 사례

실제로 있었던 일

대부분 개인 경험담입니다. "이런 일이 있었다"는 참고로 읽어 주세요.