이 장이 끝나면: 온티켓 API 서버가 인터넷에 실제로 떠 있다. 내 컴퓨터가 꺼져 있어도
https://onticket-api....workers.dev/events가 응답한다. 시크릿을 클라우드 금고에 안전하게 올리고, 배포 후 실제 주소로 검증하는 법을 익힌다.
지금까지 온티켓은 localhost — 내 컴퓨터 안에서만 살아 있었다. 노트북을
닫으면 서비스도 사라진다. 배포(deploy) 는 이 코드를 인터넷의 서버로 올려,
내 컴퓨터와 무관하게 24시간 응답하게 만드는 일이다. 오대표가, 고객이,
세상 누구든 접속할 수 있게 되는 순간이다.
여기서 잠깐 localhost라는 말을 뜯어 보자. local은 '이 지역의, 여기의'라는
뜻이고 host는 '서비스를 제공하는 컴퓨터'를 가리킨다. 그러니까 localhost는
"바로 이 컴퓨터"라는 뜻의 특별한 주소다. 브라우저에 localhost:8787을 치면
'지금 내가 앉아 있는 이 노트북 안에서 도는 서버'로 연결된다. 이 주소는
내 컴퓨터를 벗어나지 못한다. 옆자리 동료가 자기 노트북에서 같은 주소를
쳐도 내 서버가 아니라 자기 컴퓨터를 가리킨다. 오대표에게 "여기 들어가 보세요"
하며 localhost 주소를 넘길 수 없는 근본 이유가 이것이다. 그 주소는
받는 사람마다 다른 컴퓨터를 뜻하니까.
'배포(deploy)'라는 단어도 짚어 두면 개념이 또렷해진다. deploy는 원래 군사 용어로 부대를 진지에 배치한다는 뜻이다. 대기하던 병력을 실제 작전 위치에 내보내는 것 — 소프트웨어에서도 뜻이 정확히 같다. 내 컴퓨터에서 대기하던 코드를, 고객을 실제로 맞이할 위치(인터넷의 서버)에 내보내는 일이 배포다. 한국어로는 '띄운다', '올린다', '배포한다'가 모두 같은 뜻으로 쓰인다.
'내 컴퓨터에서는 되는데'는 개발 현장에서 가장 유명한 변명이다. 영어권에는
"It works on my machine(내 컴퓨터에서는 됩니다)"라는 관용구가 아예 농담으로
굳어져 있다. 왜 이게 문제냐면, 내 컴퓨터에서 되는 것과 세상에서 되는 것은
완전히 다른 증명이기 때문이다. 내 컴퓨터에는 내가 깔아 둔 프로그램,
.env 파일에 적어 둔 비밀 값, 열려 있는 포트가 있다. 인터넷의 서버는 그것들이
하나도 없는 백지 상태다. 배포란 "내 컴퓨터에서만 되던 것"을 "그 무엇에도
기대지 않고 세상 어디서든 되는 상태"로 전환하는 일이고, 이 장은 그 전환을
실제로 증명하는 데까지 간다.
배포에는 개발과 다른 긴장이 있다. 로컬의 실수는 나만 본다. 배포된 서비스의 실수는 고객이 본다. 그래서 배포는 되돌릴 수 있는 방식으로, 확인하며 한다 — 이 장의 절차가 그 원칙을 담는다.
이 '긴장'을 좀 더 구체적으로 그려 보자. 개발 중에 코드가 깨지면 화면이 빨개지고 나 혼자 한숨 쉬면 그만이다. 하지만 배포된 서비스가 깨지면, 그 순간 리버사이드 재즈 티켓을 사려던 고객이 하얀 에러 화면을 본다. 결제 직전에 서버가 죽으면 고객은 "돈이 나갔나 안 나갔나" 불안해하며 고객센터에 전화한다. 배포의 무게는 여기서 온다 — 관객이 있는 무대라는 것. 그래서 이 장은 "명령 한 번으로 확 올린다"가 아니라 "올리기 전에 점검하고, 올린 뒤에 검증한다"는 신중한 리듬으로 진행된다.
[짚고 가기] 이 장은 이 과정의 정서적 클라이맥스에 가깝다. 지금까지
localhost만 봐 왔기 때문에, "내 노트북을 닫아도 남이 접속할 수 있다"는 것이 머리로는 이해돼도 몸으로는 실감하기 어렵다. 그래서 실습의 '로컬을 끄고 확인'이 이 장의 하이라이트다. "노트북을 닫고 커피를 마시러 가도 온티켓이 고객을 받는다"는 그 순간을 목표로 삼으면, 다소 지루할 수 있는 점검 절차도 목적을 갖고 따라가게 된다.
분리형이라 배포도 둘이다(API 서버, 프론트엔드). 순서는 API 서버가 먼저다. 프론트엔드는 "API가 어디 있는지"를 알아야 완성되기 때문이다. API 서버를 먼저 띄워 실제 주소를 확보하고, 다음 장에서 프론트가 그 주소를 바라보게 한다. 집을 짓고 주소를 받은 뒤, 그 주소를 명함에 적는 순서다.
'분리형'이라는 말을 복습하자. 온티켓은 화면을 그리는 쪽(web, 프론트엔드)과 데이터를 다루는 쪽(api, 백엔드)을 아예 다른 앱으로 나눠 만들었다. 프론트엔드는 브라우저에 예쁜 화면을 띄우고, 고객이 "예매" 버튼을 누르면 뒤에서 API 서버에게 "EV009 티켓 두 장 주문할게"라고 물어본다. 즉 프론트엔드는 API 서버에게 말을 거는 고객이고, 말을 걸려면 상대방의 주소(전화번호)를 알아야 한다. 그 주소가 아직 없으면 프론트엔드는 "누구한테 주문을 넣어야 하지?"를 몰라 완성될 수 없다. 그래서 순서가 API 먼저인 것이다.
비유를 조금 더 밀어 보자. 배달 음식점을 여는 상황이라고 하자. 주방(API 서버)이 먼저 실제 위치를 잡고 가게 주소를 받아야, 전단지(프론트엔드)에 "주문 전화: 02-...번"을 인쇄할 수 있다. 주소도 없는데 전단지부터 돌리면, 고객이 전화를 걸 곳이 없다. 반대로 주방 주소만 있고 전단지가 아직이면 문제가 덜하다 — 아는 사람은 주소로 직접 찾아올 수 있으니까. 그래서 주소를 가진 쪽(API)을 먼저 세상에 내보내고, 그 주소를 받아 적을 쪽(프론트)을 다음에 붙인다.
한 가지 오해를 풀어 두자. "API를 먼저 배포한다"는 것이 "프론트는 나중에 대충 해도 된다"는 뜻은 아니다. 순서의 문제일 뿐, 둘 다 배포되어야 고객이 화면을 보고 예매까지 할 수 있다. 다만 의존의 방향이 프론트→API 한쪽이라, 의존받는 쪽을 먼저 세우는 것이 순리라는 이야기다.
인프라 엔지니어 역할을 세울 때 wrangler.toml을 만들며 길을 닦아 두었다. 이 장에서 하는 배포도 명령과 설정으로 이루어지므로 AI가 실행한다. 사람의 일은 둘 — 클라우드에 로그인(권한)하고, 시크릿을 금고에 넣고(비밀은 사람 손으로), 배포 결과를 검증하는 것.
여기서 "사람의 일은 둘"이라고 했지만 실제로 손에 꼽으면 셋이다. 로그인, 시크릿 넣기, 검증. 앞의 둘(로그인·시크릿)이 '권한과 비밀'이라 한 묶음으로 묶인 것이다. 이 셋을 관통하는 원칙은 첫 지시를 내리던 날부터 이어져 온 것과 같다 — 되돌리기 어렵거나, 권한이 필요하거나, 비밀이 걸린 일은 사람이 직접 손을 댄다. 나머지 반복 노동(빌드, 업로드, 명령 조합)은 AI에게 맡긴다.
각각이 왜 사람의 일인지 미리 짚어 두자.
[짚고 가기] "AI가 배포까지 다 하면 나는 뭘 배우나?" 싶을 수 있다. 답은 명확하다 — 배포의 '판단'을 배운다. 언제 올려도 되는지(점검), 무엇을 비밀로 지켜야 하는지(시크릿), 올린 뒤 무엇을 확인해야 하는지(검증). 명령어
wrangler deploy를 외우는 건 5초짜리 지식이지만, "빨간 코드를 배포하지 않는다", "배포 성공 여부는 검증으로만 확인된다" 같은 판단은 평생 가는 감각이다. 이 장에서 가져가야 할 것은 명령이 아니라 이 세 가지 판단이라는 점을 기억해 두자.
이 장에서 처음(또는 다시) 만나는 용어들이다. 배포는 낯선 단어가 특히 많이 쏟아지는 구간이라, 여기서 한 번 정확히 잡고 실습으로 들어가면 훨씬 매끄럽다. 완벽히 외울 필요는 없다 — "이런 게 있구나" 정도로 눈에 익혀 두면 된다.
아키텍처를 결정할 때 정했듯 API 서버는 Cloudflare Workers에 올린다. Workers의 특징을 배포를 앞두고 다시 짚자.
이 네 가지가 초심자에게는 뭉뚱그려 들릴 수 있으니 하나씩 풀어 보자.
먼저 Cloudflare라는 회사부터. Cloudflare는 원래 웹사이트를 빠르고 안전하게 만들어 주는 회사로 유명하다. 전 세계 수백 개 도시에 자기네 서버 (데이터센터)를 깔아 두고, 방문자를 가장 가까운 서버로 연결해 준다. 이 '전 세계에 깔린 서버망'이 이 회사의 핵심 자산인데, Workers는 바로 그 서버망 위에서 우리 코드를 돌려 주는 서비스다. 이름의 'Workers(일꾼들)'는 전 세계 서버 하나하나가 우리 코드를 실행하는 일꾼이 된다는 데서 왔다.
서버리스(serverless) — 직역하면 '서버가 없는'이지만, 서버가 정말 없다는 뜻이 아니다. 서버를 내가 빌리고 관리하지 않아도 된다는 뜻이다. 전통적인 방식에서는 서버 컴퓨터 한 대를 통째로 빌려서(월 얼마씩), 고객이 오든 안 오든 24시간 켜 두고 요금을 냈다. 고객이 없는 새벽 3시에도 서버는 돌아가고 돈은 나간다. 서버리스는 다르다. 요청이 들어오는 순간에만 코드가 잠깐 실행되고, 안 들어오면 아무 일도 없다. 식당으로 치면, 고객이 문을 열고 들어올 때만 주방에 불이 켜지고 요리사가 나타났다가, 고객이 나가면 사라지는 셈이다. 놀고 있을 때 드는 비용이 없다는 게 이 뜻이다. 온티켓처럼 티켓 오픈 때만 확 몰리고 평소엔 한산한 서비스에 특히 잘 맞는다.
엣지(edge) — '가장자리, 끝'이라는 뜻이다. 무엇의 가장자리냐면, 네트워크의 가장자리, 즉 고객과 가장 가까운 지점을 말한다. 옛날 방식은 서버가 한 곳(예: 미국 어느 데이터센터)에만 있어서, 서울 고객의 요청이 태평양을 건너갔다 와야 했다. 왕복이 멀면 느리다. 엣지 방식은 코드를 전 세계 서버에 미리 복사해 두고, 서울 고객의 요청은 서울 근처 서버가 응답한다. 물리적 거리가 짧으니 응답이 빠르다. 이 '가까운 곳에서 처리한다'는 발상이 엣지 컴퓨팅의 핵심이다.
잠들지 않음 — 무료 호스팅 중에는 오래 요청이 없으면 서버를 '잠재웠다가', 다음 고객이 왔을 때 다시 깨우느라 첫 응답이 몇 초씩 걸리는 것들이 있다. 이 현상을 '콜드 스타트(cold start, 차게 식은 상태에서 다시 시동)'라 부른다. 새벽에 시동 꺼 둔 차가 바로 안 나가는 것과 같다. Workers는 이 깨어나는 지연이 사실상 없다. 왜 중요하냐면, 티켓 오픈 순간에 수천 명이 동시에 몰릴 때 "첫 고객만 몇 초 기다린다"는 상황조차 티켓 서비스에는 치명적이기 때문이다. 리버사이드 재즈 예매가 열리는 그 순간, 고객이 하얀 화면을 보며 기다리게 두면 안 된다.
무료 한도 — Cloudflare Workers는 하루 10만 번의 요청까지 무료다. '요청'은 고객이 페이지를 한 번 불러올 때마다, API를 한 번 호출할 때마다 하나씩 센다. 하루 10만이면 초기 온티켓에는 넉넉하다. 돈을 내지 않고도 진짜 인터넷 서비스를 띄울 수 있다는 것 — 이것이 개인·소규모 팀에게 Workers가 매력적인 이유다.
왜 하필 Workers인가, 다른 선택지와 비교하면 이렇다.
| 배포 방식 | 특징 | 온티켓에 맞나 |
|---|---|---|
| Cloudflare Workers | 서버리스·엣지·콜드 스타트 없음·무료 한도 넉넉 | API 서버로 채택 |
| 전통 VPS(서버 한 대 임대) | 자유도 높지만 직접 관리·24시간 요금 | 관리 부담이 큼 |
| 다른 서버리스(콜드 스타트 있음) | 저렴하나 첫 응답이 느릴 수 있음 | 티켓 오픈에 불리 |
이 표의 핵심은 "정답이 하나"라는 게 아니라, 우리 상황(티켓 오픈에 몰림 + 관리 인력 없음 + 예산 최소)에 Workers가 잘 맞았다는 것이다. 다른 서비스라면 다른 선택이 나올 수 있다. 도구는 상황에 맞춰 고르는 것이지, 좋은 도구가 따로 정해져 있는 게 아니다.
큰 회사는 스테이징에서 먼저 확인하고 프로덕션에 올린다. 온티켓 첫 배포는 프로덕션 하나로 단순하게 가되, 개념은 알아 두자 — "실전에 올리기 전에 똑같은 무대에서 리허설한다"는 발상이다.
용어의 뜻을 조금 더 풀어 두자. 프로덕션은 원래 '생산, 제작'이라는 뜻이지만, 소프트웨어에서는 실제로 돌아가며 고객을 상대하는 진짜 환경을 가리킨다. 공연으로 치면 관객이 앉아 있는 '본 공연'이다. 여기서 실수가 나면 관객이 본다. 스테이징(staging) 은 'stage(무대)'에서 온 말로, 본 공연과 똑같이 꾸며 놓은 리허설 무대다. 조명도 음향도 본 공연과 같게 맞춰 두고, 관객 없이 먼저 돌려 보며 문제를 잡는다. 리버사이드 재즈 페스티벌을 여는데 관객 앞에서 처음 합을 맞추지는 않는 것과 같다 — 사전 리허설에서 걸러 낸다.
왜 이 둘을 나누느냐면, 고객이 보기 전에 실수를 걸러 낼 안전지대가 필요하기 때문이다. 새 기능을 프로덕션에 바로 올렸다가 버그가 나면 고객이 피해를 본다. 스테이징에 먼저 올려서 똑같은 조건으로 확인하고, 문제없으면 프로덕션에 올린다. 온티켓은 아직 규모가 작아 프로덕션 하나로 단순하게 가지만, 서비스가 커지면 반드시 이 리허설 무대를 만들게 된다. 지금은 "그런 발상이 있다"만 알아 두면 충분하다.
[짚고 가기] 스테이징을 지금 만들지 않는 이유는 "게을러서"가 아니라 "규모에 맞춰서"다. 고객 없는 첫 배포에 무대를 둘씩 짓는 것은 오히려 과한 준비다. "필요해지면 그때 만든다"는 것도 엄연한 실무 판단이다. 이렇게 생각하면 '정석대로 안 하면 잘못'이라는 강박에서 벗어날 수 있다. 반대로 온티켓이 고객을 받기 시작한 뒤라면, 스테이징 없이 프로덕션에 바로 올리는 것은 위험하다는 점도 함께 기억해 두면 균형이 잡힌다.
로컬에서 api는 .env의 DATABASE_URL로 DB에 접속했다. 배포된 Workers는
내 .env를 볼 수 없다(그 파일은 저장소에도 없다 — 훅이 지켰다). 그래서
Workers의 시크릿 저장소에 따로 올린다.
$ wrangler secret put DATABASE_URL
이 명령을 실행하면 값을 입력하라는 창이 뜨고, 입력한 값은 Cloudflare 금고에
암호화되어 저장된다. 코드에도, 저장소에도, 로그에도 남지 않는다. 앞에서
배운 "환경변수의 두 세계"가 여기서 실체가 된다 — 같은 DATABASE_URL이
로컬에선 .env에서, 프로덕션에선 이 금고에서 공급된다. 코드는 어느 쪽인지
모른다.
이 부분이 이 장에서 개념적으로 가장 중요하니 천천히 풀어 보자.
먼저 시크릿(secret) 이란 무엇인가. 말 그대로 '비밀'이다. 코드가 돌아가려면
필요하지만, 남에게 보이면 안 되는 값들 — DB 접속 문자열(DATABASE_URL),
결제사 비밀 키, API 토큰 같은 것들이다. DATABASE_URL 안에는 DB의 주소와
아이디, 그리고 비밀번호가 통째로 들어 있다. 이걸 누가 보면 우리 데이터베이스에
그대로 접속할 수 있으니, 절대 새어 나가면 안 되는 값이다.
그럼 배포된 Workers는 이 값을 어디서 얻나? 로컬에서는 .env라는 파일에 적어
뒀지만, 그 파일은 내 컴퓨터에만 있고 저장소에도 안 올라간다. 왜 저장소에
안 올렸냐면, 시크릿이 코드와 함께 GitHub에 올라가면 전 세계에 비밀번호를
공개하는 꼴이기 때문이다. 앞에서 세운 보안 훅이 바로 이 사고를 막아 줬다 —
'훅이 지켰다'는 그 뜻이다. 그런데 저장소에 안 올렸으니, 인터넷의 Workers는
내 .env를 볼 방법이 없다. 노트북 안의 파일이니까.
여기서 해법이 Workers의 시크릿 금고다. Cloudflare가 계정별로 제공하는
암호화된 비밀 저장소인데, 여기에 DATABASE_URL 값을 따로 넣어 두면 배포된
코드가 실행될 때 그 금고에서 값을 꺼내 쓴다. wrangler secret put DATABASE_URL이
바로 "이 이름의 시크릿을 금고에 넣겠다"는 명령이다. 명령을 실행하면 화면에
값을 입력하라는 프롬프트가 뜨고, 여기에 실제 값을 붙여넣는다. 입력한 값은
암호화되어 저장되며 코드에도, 저장소에도, 화면 로그에도 남지 않는다.
'금고'라는 비유가 정확하다. 금고는 (1) 값을 넣을 수는 있지만 (2) 넣은 값을 다시 꺼내 눈으로 볼 수는 없게 되어 있다. 시크릿 금고도 그렇다. 한 번 넣으면 "DATABASE_URL이라는 시크릿이 존재한다"는 사실은 볼 수 있어도, 그 안의 실제 값은 다시 못 본다(바뀌었으면 새로 덮어쓸 뿐이다). 이게 안전장치다 — 혹시 누가 계정 화면을 들여다봐도 비밀번호 자체는 보이지 않는다.
이제 "환경변수의 두 세계"가 실체가 되는 장면이다. 표로 정리하면 이렇다.
| 값이 어디서 오나 | 저장소에 올라가나 | |
|---|---|---|
| 로컬(내 컴퓨터) | .env 파일 |
안 올라감(훅이 지킴) |
| 프로덕션(배포된 Workers) | Workers 시크릿 금고 | 파일이 아니라 금고라 애초에 대상 아님 |
핵심은 마지막 문장이다 — 코드는 어느 쪽인지 모른다. 코드에는 그냥
"DATABASE_URL이라는 이름의 값을 달라"라고만 적혀 있다. 그 이름표에 붙는
실제 값이 로컬에서는 .env에서, 프로덕션에서는 금고에서 온다. 코드는 값이
어느 세계에서 왔는지 알 필요가 없고, 알지도 못한다. 그래서 코드를 한 줄도
고치지 않고 같은 코드가 내 노트북에서도, 세상에서도 돈다. 이 '이름은 같고
값의 출처만 다르다'는 구조가, 시크릿을 코드 밖으로 빼내는 이유의 전부다.
[짚고 가기] 여기서 헷갈리기 쉬운 점 둘. (1) "그럼 로컬
.env랑 금고에 같은 값을 두 번 넣는 건가?" — 그렇다. 같은 DATABASE_URL을 로컬.env에 한 번, Workers 금고에 한 번 넣는다. 두 세계가 각자 자기 값을 가진다. (2) "금고에 넣은 값이 틀리면?" — 배포는 되는데 DB 접속만 실패한다. 그래서 뒤에 나올 '배포 후 검증'에서 실제 데이터가 나오는지를 반드시 확인하는 것이다. 이 두 가지를 미리 짚어 두면, 시크릿을 넣고 나서 검증까지가 한 흐름이라는 게 자연스럽게 이어진다.
배포가 끝나면 https://onticket-api.<계정>.workers.dev 같은 주소가 나온다.
이것이 API 서버의 인터넷 주소다. 이 주소의 /events, /orders 가
localhost 때와 똑같이 응답한다 — 다만 이제 전 세계에서.
이 주소를 뜯어 보면 배포가 무슨 일을 했는지 손에 잡힌다.
https:// — 암호화된 안전한 연결이라는 뜻. http가 아니라 https인 것은
고객과 서버 사이 통신이 도청되지 않게 암호화된다는 표시다. Cloudflare가
이 암호화를 자동으로 붙여 준다.onticket-api — 우리 앱의 이름. wrangler.toml에 적어 둔 name이 여기로
온다. 처음 이 이름을 정할 때, 그것이 훗날 인터넷 주소의 일부가 된다는
걸 몰랐을 것이다. 지금 그 이름이 주소가 되어 돌아왔다.<계정>.workers.dev — Cloudflare가 무료로 내어 주는 기본 도메인. 계정마다
다른 이름이 붙어, 세상에서 겹치지 않는 고유한 주소가 된다.localhost:8787과 이 주소를 나란히 놓으면 차이가 선명하다.
| localhost 때 | 배포 후 | |
|---|---|---|
| 주소 | http://localhost:8787/events |
https://onticket-api.<계정>.workers.dev/events |
| 누가 접속 가능한가 | 나만(내 컴퓨터 안) | 세상 누구든 |
| 내 노트북 꺼지면 | 죽는다 | 살아 있다 |
| 응답 내용 | 12개 행사 JSON | 똑같이 12개 행사 JSON |
주목할 것은 마지막 줄이다. 응답 내용은 똑같다. 배포가 코드를 바꾼 게
아니라 코드가 '사는 장소'를 바꾼 것뿐이기 때문이다. 같은 /events가 같은
데이터를 반환하되, 이제는 내 노트북이 아니라 세상 어딘가의 서버에서. 이
'주소만 바뀌고 응답은 같다'는 것을 직접 확인하는 순간이 이 장의 실습에 있다.
참고로 나중에 온티켓이 자리를 잡으면 api.onticket.com 같은 내 도메인을
사서 이 기본 주소에 연결할 수 있다. 하지만 지금은 무료로 주어지는
workers.dev 주소로 충분하다. 도메인 구입은 배포와 별개의 일이고, 배포가
먼저 되어야 거기에 도메인을 이어 붙일 수 있다.
배포는 "명령 한 번"이 아니라 앞뒤로 확인이 붙는 절차다.
이 장의 제목만 보면 "wrangler deploy 한 줄 치면 끝 아닌가?"라고 생각하기
쉽다. 명령 자체는 정말 한 줄이다. 하지만 프로다운 배포는 그 한 줄 앞뒤로
확인이 붙는다. 앞에서는 "올려도 되는 상태인가"를 점검하고, 뒤에서는
"정말 올라갔는가"를 검증한다. 이 앞뒤 확인이 없는 배포는 눈 감고 던지는
것과 같다. 비행기 조종사가 이륙 전 점검표(checklist)를 읽고, 착륙 후에도
계기를 확인하는 것과 같은 규율이다.
세 가지를 왜 확인하는지 하나씩 보자.
CI 초록 — 'CI'는 코드가 올라올 때마다 자동으로 검사를 돌려 주는 장치다. 검사를 통과하면 초록불, 실패하면 빨간불이 뜬다. 배포 직전에 이 색을 확인하는 이유는 단순하다 — 빨간 코드는 이미 어딘가 고장 나 있다는 신호이고, 고장 난 것을 세상에 내보내면 고객이 그 고장을 본다. "빨간 코드를 배포하지 않는다"는 이 장에서 가장 짧지만 가장 중요한 규칙이다. 초록불을 확인하고 올린다.
시크릿 준비 — 용어 정리에서 봤듯, 배포된 Workers는 금고에서 시크릿을 꺼내
DB에 접속한다. 금고가 비어 있으면 배포는 되는데 DB 접속만 실패한다.
겉으로는 서버가 떴는데 데이터를 못 불러오는, 가장 헷갈리는 상태가 된다.
그래서 배포 전에 "필요한 시크릿이 금고에 다 올라갔나"를 미리 확인한다.
이 장에서는 DATABASE_URL 하나지만, 결제 키 등이 늘면 목록도 길어진다.
설정 확인 — wrangler.toml은 인프라를 정리할 때 만든 '앱의 신분증명서'다.
여기 적힌 name(앱 이름)이 배포 주소가 되고, main(진입점)이 "어느 파일부터
실행할지"를 정한다. 이 값들이 틀리면 엉뚱한 이름으로 배포되거나 실행이
안 된다. 배포 전에 한 번 훑어 "이름·진입점이 맞나"를 본다.
이 셋을 점검표로 정리하면 이렇다.
| 점검 항목 | 확인 방법 | 통과 못 하면 |
|---|---|---|
| CI 초록 | GitHub의 main 최근 커밋이 초록인가 | 실패 원인부터 고치고 배포는 미룸 |
| 시크릿 준비 | 필요한 시크릿이 금고에 올라갔나 | 먼저 wrangler secret put으로 올림 |
| 설정 확인 | wrangler.toml의 name·main이 맞나 |
설정을 바로잡고 배포 |
배포는 "됐다는 말"로 끝나지 않는다. 실제 주소로 요청을 보내 봐야 끝난다.
/health 또는 /events 가 응답하는가 (curl·브라우저)"배포했다"와 "배포가 됐다"는 다르다. 후자는 검증으로만 확인된다.
이 세 검증이 각각 무엇을 증명하는지가 중요하다.
첫째, 주소가 응답하는가. 배포 URL의 /events(또는 상태 확인용 /health)를
실제로 열어 본다. 브라우저 주소창에 붙여넣거나 curl이라는 명령으로 요청을 보내
본다. 응답이 오면 "서버가 살아서 떠 있다"는 것이 증명된다. 여기서
/health라는 주소를 잠깐 짚자 — 많은 서비스가 "나 살아 있어요"만 짧게
대답하는 전용 주소를 하나 둔다. 병원의 건강검진(health check)처럼, 서버가
정상인지 가볍게 물어보는 창구다.
둘째, DB에 연결됐는가. 서버가 떴다는 것과 DB가 붙었다는 것은 다르다.
/events가 빈 배열이 아니라 실제 12개 행사 데이터를 반환하면, 서버가
금고의 DATABASE_URL로 DB에 제대로 접속했다는 뜻이다. 데이터가 안 나오면
십중팔구 시크릿이 안 올라갔거나 값이 틀린 것 — 시크릿을 설명하며 [짚고 가기]에서
예고한 바로 그 상황이다.
셋째, 로컬을 끄고도 응답하는가. 이것이 이 장의 절정이다. 앞의 두 검증까지는 내 노트북에서 로컬 서버가 돌고 있어도 통과할 수 있다(혹시 브라우저가 로컬을 보고 있었을 수도 있으니까). 하지만 로컬 서버를 완전히 끄고 나서도 배포 URL이 응답한다면, 그 응답은 내 노트북과 무관하게 세상 어딘가에서 온 것이 확실하다. 이 대비 실험이 곧 배포의 결정적 증명이다.
'검증(verify)'이라는 말은 라틴어 verus(참된)에서 왔다 — 참인지 직접 확인한다는 뜻이다. "배포했다"는 것은 내가 명령을 내렸다는 사실이고, "배포가 됐다"는 것은 그 결과가 참임을 확인한 사실이다. 이 둘 사이의 간극이 바로 이 절의 주제다. AI가 "배포 완료했습니다"라고 보고해도, 그것은 명령이 끝났다는 보고일 뿐 실제로 세상에서 응답한다는 증명이 아니다. 증명은 사람이 실제 주소로 요청을 보내 봐야 나온다. 첫날 배운 '검증은 사람의 일'이 배포에서 이런 모습으로 나타난다.
[짚고 가기] '전 점검 + 후 검증'은 비행기에 빗대면 기억에 잘 남는다. 조종사는 이륙 전에 점검표를 소리 내어 읽고, 착륙 후에도 계기를 확인한다. 배포도 똑같다 — "빨간 코드를 안 올린다(점검)"와 "실제 주소가 응답하는지 본다(검증)"가 앞뒤로 붙는다. 특히 "배포했다 ≠ 배포가 됐다"는 이 장 전체를 한 문장으로 압축한 문구다. 크게 새겨 두고, "완료했습니다"라는 AI 보고만 믿고 넘어가려 할 때마다 "그건 '배포했다'지, '배포가 됐다'는 어떻게 확인하지?"라고 스스로 되물으면 습관이 잡힌다.
배포는 여러 단계(로그인 → 점검 → 시크릿 → 배포 → 검증)가 이어지는 절차라, 한 번에 "다 배포해줘"라고 몰아치기보다 단계별로 시키고 사이사이 확인하는 것이 안전하다. 각 단계의 프롬프트에는 공통 원칙이 흐른다 — 목표를 주되, 사람이 손대야 할 지점(로그인 승인·시크릿 입력·검증)을 명시해서 AI가 그 선을 넘지 않게 한다.
Cloudflare에 배포할 준비를 하자. 먼저 wrangler login 으로 로그인 —
브라우저 승인은 내가 한다. 그다음 배포 전 점검을 해줘:
main이 CI를 통과한 상태인지, wrangler.toml 설정이 맞는지, 프로덕션에
필요한 시크릿 목록이 무엇인지 알려줘.
이 프롬프트가 좋은 이유를 뜯어 보자. "wrangler login 으로 로그인"이라고 목표를 주되, "브라우저 승인은 내가 한다"라고 사람의 몫을 못 박았다. 로그인은 권한을 넘기는 일이라 사람이 직접 눌러야 한다는 이 장 첫머리의 원칙이 프롬프트에 그대로 들어 있다. 그리고 점검 세 가지(CI·설정·시크릿 목록)를 구체적으로 나열해, AI가 "점검했다"는 뭉뚱그린 대답 대신 항목별 결과를 내놓게 만든다.
프로덕션에 필요한 시크릿을 Workers 금고에 올리려고 해. 어떤 값들을
wrangler secret put 으로 올려야 하는지 목록을 주고, 명령을 알려줘.
값 입력은 내가 직접 한다.
값 입력은 사람이 한다 — DB 비밀번호가 든 연결 문자열이니까. AI는 명령까지, 비밀은 사람 손으로.
이 프롬프트의 마지막 줄 "값 입력은 내가 직접 한다"가 핵심이다. 이 한 줄이 없으면 AI가 친절하게 "그럼 값을 알려주시면 제가 넣어 드릴게요"라고 나올 수 있고, 그 순간 비밀번호가 대화창에 남는다. 미리 선을 그어 두면 AI는 명령의 틀만 안내하고 실제 값은 사람이 화면 프롬프트에 직접 붙여넣게 된다. "AI는 명령까지, 비밀은 사람 손으로"를 프롬프트에 문장으로 심는 요령이다.
api/ 를 Cloudflare Workers에 배포해줘. 배포가 끝나면 나온 URL을 알려주고,
그 URL의 /events 를 실제로 호출해서 응답이 오는지 확인까지 해줘.
이 프롬프트는 배포와 1차 검증을 한 지시에 묶었다. "배포해줘"에서 멈추지 않고 "나온 URL을 알려주고, 그 URL의 /events를 실제로 호출해서 확인까지"라고 붙여, AI가 자기 입으로 "배포 완료"라고 말하고 끝내지 못하게 했다. 바로 앞에서 배운 "배포했다 ≠ 배포가 됐다"를 프롬프트로 강제하는 것이다. 다만 이건 어디까지나 AI가 대신 요청을 보내 본 1차 확인이고, 결정적 검증(로컬 끄고 확인)은 사람이 실습에서 직접 한다.
방금 배포된 API를 검증하자:
1) {배포URL}/events 가 12개 행사를 반환하는가
2) {배포URL}/events/EV009 가 리버사이드 재즈를 반환하는가
3) 초과 주문 POST가 여전히 409를 반환하는가
curl로 실제 호출한 결과를 보여줘.
이 검증 프롬프트가 잘 짜인 이유는 눈으로 확인 가능한 사실로 기준을 적었다는 데 있다. "잘 되는지 봐줘"가 아니라 "12개 행사를 반환하는가", "리버사이드 재즈를 반환하는가", "409를 반환하는가"처럼 정답이 정해진 질문을 던진다. 셋을 고른 데에도 뜻이 있다.
/events가 12개 — 목록 조회와 DB 연결이 함께 확인된다./events/EV009가 리버사이드 재즈 — 개별 조회가 정확히 되는지,
특정 데이터가 제대로 나오는지 본다.'curl로 실제 호출한 결과를 보여줘'라고 명시한 것도 좋다. AI의 "확인했습니다"라는 말이 아니라 실제 호출의 원문 결과를 요구해, 검증을 증거 기반으로 만든다.
여기서 배포 상황에서 자주 쓰는 프롬프트를 약한 지시 → 강한 지시로 정리해 두자.
| 흔한(약한) 지시 | 고친(강한) 지시 | 무엇을 바꿨나 |
|---|---|---|
| 배포해줘 | api/ 를 Workers에 배포하고, 나온 URL의 /events를 호출해 응답까지 확인해줘 | 명령 실행에 1차 검증을 묶음 |
| 시크릿 올려줘 | 필요한 시크릿 목록과 wrangler secret put 명령을 알려줘. 값 입력은 내가 직접 한다 |
사람이 값을 넣는 선을 명시 |
| 로그인해줘 | wrangler login을 실행해줘. 브라우저 승인은 내가 누른다 | 권한 승인이 사람 몫임을 못 박음 |
| 잘 됐는지 봐줘 | {배포URL}/events가 12개 행사를, /events/EV009가 리버사이드 재즈를 반환하는지 curl로 확인해줘 | 눈으로 확인 가능한 사실로 기준화 |
| 배포 완료됐지? | 로컬 서버를 끈 상태에서 {배포URL}/events가 여전히 응답하는지 확인하자 | '됐다는 말' 대신 결정적 증명을 요구 |
두 가지 실전 감각을 덧붙인다.
[짚고 가기] 실습에서 배포가 한 번에 성공하지 않는 경우가 흔하다. 이때 "실패는 정상이고, 실패 로그를 읽는 게 배포의 절반"이라고 생각하면 위축되지 않는다. 가장 흔한 실패는 시크릿을 안 올린 채 배포한 경우 — 서버는 떴는데
/events가 DB 에러를 뱉는다. "떴는데 데이터가 없다 = 금고를 의심"이라는 진단 공식을 알아 두면, 스스로 원인을 좁혀 갈 수 있다.
이제 지금까지 익힌 절차를 순서대로 실행한다. 로그인 → 시크릿 → 배포 → 검증 → 로컬 끄고 확인 → 기록의 여섯 단계다. 각 단계가 끝날 때마다 결과를 눈으로 확인하고 다음으로 넘어가는 리듬을 지키자.
아래 로그인·점검 지시를 실행한다. wrangler login에서 브라우저가 열리면
승인 버튼을 누른다(권한을 넘기는 일 — 사람의 손). 점검 보고에서 CI 초록
여부와 필요한 시크릿 목록을 확인한다.
Cloudflare에 배포할 준비를 하자. 먼저 wrangler login 으로 로그인 —
브라우저 승인은 내가 한다. 그다음 배포 전 점검을 해줘:
main이 CI를 통과한 상태인지, wrangler.toml 설정이 맞는지, 프로덕션에
필요한 시크릿 목록이 무엇인지 알려줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
브라우저가 열리면 Cloudflare 계정으로 로그인하고 "이 도구가 내 계정에 접근하는 것을 허용" 버튼을 누른다. 맨 처음 Claude Code에 로그인하던 것과 똑같은 흐름이다 — 브라우저에서 사람이 직접 승인해, 이 컴퓨터의 wrangler에게 내 클라우드 계정을 다룰 권한을 연결한다. Cloudflare 계정이 아직 없다면 이 단계에서 무료로 하나 만든다(이메일 가입이면 충분하다).
점검 보고가 오면 두 가지를 눈으로 확인한다 — main이 CI 초록인지, 그리고
AI가 알려 준 '필요한 시크릿 목록'에 무엇이 있는지. 이 장에서는 목록이
DATABASE_URL 하나일 것이다. 이 목록이 바로 다음 단계에서 금고에 넣을
대상이 된다.
아래 안내 요청으로 절차를 받아 wrangler secret put DATABASE_URL을 실행하고, 값을
직접 입력한다(Supabase 연결 문자열). 입력한 값이 화면·로그에 남지 않는 것을
확인한다 — 금고에 들어갔다.
프로덕션에 필요한 시크릿을 Workers 금고에 올리려고 해. 어떤 값들을
wrangler secret put 으로 올려야 하는지 목록을 주고, 명령을 알려줘.
값 입력은 내가 직접 한다.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
여기서 넣을 값은 로컬 .env에 적어 둔 것과 같은 DATABASE_URL이다.
Supabase(우리 DB)의 연결 문자열로, postgresql://... 로 시작하고 중간에
비밀번호가 들어 있다. 명령을 실행하면 화면에 값을 입력하라는 프롬프트가
뜨는데, 여기에 연결 문자열을 붙여넣고 Enter를 누른다. 붙여넣은 값이 화면에
그대로 찍히지 않고(또는 별표로 가려지고), 입력이 끝나면 "시크릿이 저장됐다"는
메시지만 남는다. 값 자체는 어디에도 남지 않는다 — 방금 그 값이 금고에
암호화되어 들어갔다는 증거다.
한 가지 주의. 이 값은 AI에게 채팅으로 넘기지 않는다. wrangler secret put이
띄운 터미널 입력 프롬프트에 사람이 직접 붙여넣는다. AI는 "이 명령을
실행하세요"까지만 안내하고, 실제 비밀 값은 사람 손을 거친다. 프롬프트 작성법에서
"값 입력은 내가 직접 한다"라고 못 박은 것이 여기서 실행되는 장면이다.
아래 배포 지시를 실행한다. 빌드와 업로드가 진행되고, 끝나면 배포 URL이 나온다. 이 주소를 메모한다 — 다음 장에서 프론트가 바라볼 주소다.
api/ 를 Cloudflare Workers에 배포해줘. 배포가 끝나면 나온 URL을 알려주고,
그 URL의 /events 를 실제로 호출해서 응답이 오는지 확인까지 해줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
'빌드(build)'와 '업로드(upload)'가 무슨 일인지 짚어 두자. 빌드는 우리가
쓴 코드를 Workers가 실행할 수 있는 형태로 묶고 다듬는 과정이다(재료를
손질해 요리로 만드는 것에 가깝다). 업로드는 그 결과물을 Cloudflare의
서버망으로 올려 전 세계 엣지에 배치하는 것이다. 이 둘이 끝나면 화면에
https://onticket-api.<계정>.workers.dev 형태의 주소가 찍힌다. 이것이
용어 정리에서 예고한 '배포 URL'이며, 온티켓 API의 인터넷 주소다.
이 주소를 반드시 메모해 둔다. 눈에 잘 띄는 곳에 적어 두자 — 다음 장에서 프론트엔드가 "주문은 이 주소로 넣어라"라고 바라볼 대상이고, 이 장 뒤쪽 실습 끝에서 문서에도 기록할 값이다. 앞의 배달 음식점 비유로 치면, 방금 우리 주방이 실제 가게 주소를 발급받은 순간이다.
브라우저에서 {배포URL}/events 를 연다. 인터넷 주소에서 우리 데이터가
JSON으로 나온다. localhost가 아니다. 이어서 아래 검증 지시로 상세·409까지
확인한다.
방금 배포된 API를 검증하자:
1) {배포URL}/events 가 12개 행사를 반환하는가
2) {배포URL}/events/EV009 가 리버사이드 재즈를 반환하는가
3) 초과 주문 POST가 여전히 409를 반환하는가
curl로 실제 호출한 결과를 보여줘.
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
브라우저 주소창에 방금 메모한 배포 URL 뒤에 /events를 붙여 연다. 화면에
12개 행사가 JSON(데이터를 표현하는 텍스트 형식)으로 주르륵 나오면, 두 가지가
한꺼번에 증명된다 — 서버가 인터넷에 떠 있고, 금고의 DATABASE_URL로 DB에도
붙었다. 주소창을 다시 보자. localhost가 아니라 ...workers.dev다.
이 데이터는 내 노트북이 아니라 세상 어딘가의 서버에서 온 것이다.
이어서 위 검증 지시(1: 12개, 2: EV009 리버사이드 재즈, 3: 초과 주문 409)를 AI에게 실행시켜 상세 조회와 비즈니스 규칙까지 확인한다. 세 가지가 모두 기대대로 나오면 배포된 API가 로컬 때와 똑같이 동작한다는 것이 확인된 것이다 — 다만 이번엔 세상에서.
로컬 api 서버를 끈다(터미널에서 종료). 그 상태로 다시 {배포URL}/events
를 연다. 여전히 응답한다. 내 컴퓨터와 무관하게 살아 있는 것 — 이것이
배포의 진짜 의미다. 온티켓의 API가 이제 세상에 존재한다.
이 단계가 이 장 전체의 클라이맥스다. 로컬 api 서버가 돌고 있는 터미널에서
종료(Ctrl + C)해 완전히 끈다. 이제 내 노트북에서는 어떤 서버도 돌지
않는다. 그 상태로 브라우저에서 배포 URL의 /events를 다시 연다(새로고침).
여전히 12개 행사가 나온다. 방금 내 노트북의 서버를 껐는데도 응답이
왔다면, 그 응답은 명백히 내 컴퓨터 밖 — 세상 어딘가의 Cloudflare 엣지
서버에서 온 것이다.
이 대비 실험 하나가 '배포 후 검증'에서 말한 결정적 검증이다. 앞의 '실제 주소로 검증'까지는 혹시 브라우저가 로컬을 보고 있었을 가능성이 남지만, 로컬을 완전히 끄고도 응답하면 그 가능성이 사라진다. "내 컴퓨터에서 됨"을 넘어선 순간을 자기 눈으로 확인하는 것이다. 노트북을 닫고 커피를 마시러 가도 온티켓은 고객을 받는다. 이제 온티켓의 API가 세상에 존재한다.
[짚고 가기] 이 순간을 그냥 지나치지 말고 의식처럼 새겨 두자. "방금 내 노트북에서 서버를 껐는데, 화면에는 데이터가 나온다 — 이게 배포다." 가능하면 다른 사람의 배포 URL을 열어 보면 실감이 배가된다 — 남의 노트북에서 만든 서비스에 내가 접속되는 경험은, 이 과정의 첫머리에서 예고한 "옆자리 사람이 내 URL로 접속"의 축소판이다. 이 정서적 완성이 다음 장(프론트 배포)과 마지막 데모까지 이어지는 동력이 된다.
CLAUDE.md와 docs 에 프로덕션 API 주소를 기록해줘:
- 프로덕션 API: {배포URL}
- 배포 방법: api/ 에서 wrangler deploy (설정은 wrangler.toml)
- 시크릿은 Workers secret 금고 (DATABASE_URL)
위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.
왜 굳이 기록하는가. 배포 URL은 다음 장에서 프론트가 바라볼 주소이고,
나중에 팀원이 합류하거나 몇 주 뒤 내가 다시 볼 때 "우리 API가 어디 떠
있더라", "배포는 어떻게 하더라"를 기억에 의존하지 않게 하기 위해서다.
CLAUDE.md에 적어 두면 앞으로 AI도 이 정보를 참고해 일한다 — "프로덕션
API가 저 주소에 있고, 배포는 wrangler로 한다"는 사실을 AI가 알고 움직이게
된다. 주소·배포 방법·시크릿 위치 세 가지를 남겨, 다음에 이 절차를 반복할
사람(미래의 나 포함)이 헤매지 않게 한다. 단, 여기 적는 것은 주소와 방법일
뿐 시크릿 값 자체는 절대 적지 않는다 — 값은 금고에만 있다.
wrangler login 이 완료되어 있다이 여섯 줄은 "배포했다"가 아니라 "배포가 됐다"를 확인하는 항목들이다. 특히 다섯째 줄(로컬을 끈 상태에서도 응답)이 나머지를 대표하는 결정적 증명이다. 하나라도 체크가 안 되면 그 단계로 돌아가 원인을 좁힌다 — 공개 URL이 안 나왔으면 배포 단계를, 데이터가 안 나오면 시크릿 올리기 단계를 의심한다.
.env가 아니라 Workers 금고(wrangler secret)에 올린다. 같은
DATABASE_URL이 로컬과 프로덕션에서 다른 곳으로부터 공급되고, 코드는 모른다.한 장을 통과하며 온티켓은 처음으로 내 노트북을 벗어났다. 지금까지의 실습이
전부 localhost 안에서 이뤄졌다면, 이제 API는 세상에 실재하는 주소를 얻어
누구에게나 응답한다. 남은 것은 이 주소를 바라볼 화면(프론트엔드)을 세상에
올려, 고객이 직접 예매까지 하게 만드는 일이다. 주소를 발급받은 주방 위에,
다음 장에서 고객을 맞을 홀(프론트)을 세운다.