22장. 프론트엔드 배포 — Vercel과 연결

이 장이 끝나면: 온티켓의 모든 과정이 인터넷에서 동작한다. 공개된 웹 주소에 접속하면 화면이 뜨고, 그 화면이 배포된 API를 불러 실제로 예매가 된다. 배포 환경의 CORS·환경변수를 다루고, push 하면 자동 배포되는 마지막 자동화(CD)까지 연결한다.


22.1 개념 — 두 조각을 하나의 서비스로

마지막 연결

바로 앞에서 API 서버가 인터넷에 떴다. 이 장에서 프론트엔드를 배포하고, 그 둘을 잇는다. 지금까지 로컬에서 3000↔8787로 통신하던 두 앱이, 이제 각자의 공개 주소를 가지고 인터넷을 가로질러 통신한다. 이 연결이 완성되는 순간 온티켓은 '프로젝트'가 아니라 '서비스'가 된다.

이 대목을 조금 더 풀어 보자. 지금까지 우리가 만든 온티켓은 두 개의 프로그램이 한 컴퓨터(내 노트북) 안에서 나란히 돌고 있는 상태였다. 프론트엔드는 localhost:3000, API는 localhost:8787. 둘 다 '내 컴퓨터 안'이라는 같은 방 안에 있어서, 서로를 부르기가 쉬웠다. 하지만 이건 나만 볼 수 있는 세계다. 전원을 끄면 사라지고, 옆자리 동료에게 보여 줄 수도 없다. '만들었다'고는 할 수 있어도 '서비스한다'고는 할 수 없다.

배포(deploy)란 이 프로그램을 내 컴퓨터 밖, 항상 켜져 있는 인터넷상의 컴퓨터로 옮겨 누구나 접속할 수 있게 하는 일이다. 'deploy'는 원래 군대를 '배치한다'는 뜻인데, 소프트웨어에서는 코드를 실제 서비스되는 자리에 갖다 놓는다는 의미로 굳어졌다. API는 이미 배치했고, 이 장에서 프론트를 배치하면 두 조각이 모두 세상에 나온다. 그리고 마지막 한 걸음 — 세상에 나온 두 조각이 서로를 알아보게 연결하는 것 — 이 이 장의 핵심이다.

로컬에서 배포로 — 무엇이 달라지나

같은 코드인데 환경이 바뀌면 두 가지를 다시 맞춰야 한다.

  1. API 주소: 로컬에선 localhost:8787이었다. 배포된 프론트는 배포된 API 주소(onticket-api....workers.dev)를 바라봐야 한다 — 환경변수를 프로덕션 값으로
  2. CORS: API 서버가 허용하는 출처가 localhost:3000이었다. 이제 배포된 프론트의 주소도 허용해야 한다 — 안 그러면 프론트엔드를 처음 붙일 때 만난 그 CORS의 벽이 프로덕션에서 다시 선다

이 둘은 배포 초보가 반드시 부딪히는 지점이다. 미리 알고 넘으면 당황하지 않는다.

왜 하필 이 둘일까? 답은 간단하다. 로컬에서는 프론트와 API가 같은 컴퓨터 안에 있었기 때문에, 서로의 위치(주소)와 서로에 대한 신뢰(허용)가 자동으로 성립했다. 배포하면 둘이 서로 다른 회사의 서버, 서로 다른 인터넷 주소로 흩어진다. 그러면 "너 어디 있어?"(주소)와 "너를 믿어도 돼?"(CORS 허용)를 다시 명시적으로 알려 줘야 한다. 이 두 가지는 성격이 다르다.

다시 맞출 것 로컬 값 배포 값 어디를 고치나 성격
API 주소 http://localhost:8787 https://onticket-api....workers.dev 프론트의 환경변수 (Vercel) "상대가 어디 있는지"
CORS 허용 출처 http://localhost:3000 https://<프론트>.vercel.app API의 CORS 설정 (api 코드) "상대를 믿어도 되는지"

주소를 고치는 곳은 프론트 쪽(프론트가 API를 부르니까)이고, 허용을 고치는 곳은 API 쪽(API가 프론트를 받아 주니까)이라는 점을 헷갈리지 말자. 초보가 가장 자주 뒤섞는 지점이다. "화면이 안 나온다 → 프론트를 고쳐야지"라고 착각하기 쉬운데, CORS 문제라면 실제로 손대야 할 곳은 API다. 로컬에서 프론트를 붙일 때 이미 겪은 원리이고, 프로덕션이라고 달라지지 않는다.

[짚고 가기] 이 표 한 장이 이 장 전체의 지도다. 나중에 "화면은 뜨는데 목록이 안 나와요" 같은 상황을 만나면 이 표로 돌아오면 된다. "지금 문제는 '주소'인가 '허용'인가?"를 먼저 묻는 습관을 들여두면 좋다. 콘솔에 CORS 에러가 찍혀 있으면 허용(API), 요청이 아예 엉뚱한 곳으로 가면 주소(환경변수)다.

이번에는 자동 배포까지

앞서 한 API 배포는 명령을 실행하는 방식이었다. 프론트는 한 걸음 더 나아간다 — GitHub 저장소와 Vercel을 연결하면, main에 코드가 올라올 때마다 Vercel이 자동으로 배포한다. 앞에서 세운 CI(검사)에 이어, 배포까지 자동화되는 것 — 이것이 CD(지속적 배포) 다. push 한 번이 검사와 배포로 이어지는 완전한 흐름이 이 장에서 완성된다.

여기서 두 방식의 차이를 조금 더 또렷이 해 두자. API는 wrangler deploy처럼 사람이 배포 명령을 직접 쳐서 세상에 내보냈다. "지금 배포해"라고 버튼을 누르는 방식이다. 반면 프론트는 push라는 일상적인 행동에 배포를 얹는다. 코드를 저장소에 올리는 순간, 별도의 배포 명령 없이 배포가 시작된다. 개발자가 "배포"를 의식하지 않아도 최신 코드가 자동으로 세상에 반영되는 상태 — 이것이 현대 웹 서비스가 일하는 표준 방식이고, 이 장에서 우리가 손에 넣는 것이다.

이 흐름을 한 줄로 그리면 이렇다.

코드 수정 → git push → (CI) 자동 검사 → main 병합 → (CD) Vercel 자동 배포 → 공개 URL 갱신
                         ↑ CI를 세울 때               ↑ 이번 장

CI를 세우며 만든 것이 왼쪽 절반(검사까지 자동), 이 장에서 채우는 것이 오른쪽 절반(배포까지 자동)이다. 둘을 합치면 "사람은 코드만 밀어 넣고, 검사와 배포는 기계가 알아서" 하는 파이프라인이 완성된다. 온티켓을 처음 만들 때 코드를 손으로 실어 나르던 것(첫날 본 복사-붙여넣기 루프)에서 시작해, 이제 배포조차 사람 손을 떠나는 지점까지 온 것이다.


22.2 용어 정리

이 장에서 처음, 또는 본격적으로 만나는 용어들이다. 배포는 낯선 단어가 몰려 나오는 구간이라 여기서 한 번 정리하고 가면 실습이 훨씬 수월하다. 완벽히 외울 필요는 없다 — "이런 게 있구나" 정도로 눈에 익혀 두면 된다.

Vercel과 Git 연동 배포

Vercel은 프론트엔드(Next.js) 배포에 특화된 호스팅이다. 핵심 방식은 Git 연동 — 저장소를 Vercel에 연결해 두면, Vercel이 저장소를 지켜보다가 새 코드가 올라오면 스스로 빌드하고 배포한다. 사람이 "배포해"라고 명령할 필요조차 없어진다. (인프라를 정리할 때 vercel 프로젝트를 link해 둔 것이 이 연결의 준비였다.)

Vercel을 조금 더 풀어 보자. 호스팅(hosting) 이란 내 웹사이트를 항상 켜져 있는 남의 컴퓨터(서버)에 올려 두고, 전 세계 방문자가 접속할 수 있게 해 주는 서비스를 말한다. 옛날에는 서버 컴퓨터를 직접 사서 방에 두고, 운영체제를 깔고, 웹 서버 프로그램을 설정하고, 도메인을 연결하고… 하는 복잡한 일을 사람이 다 했다. Vercel은 이 모든 것을 대신 해 주는 회사다. 특히 Next.js를 만든 회사가 직접 운영하는 호스팅이라, Next.js 앱을 올리면 별다른 설정 없이도 최적의 상태로 배포된다. 이 궁합 때문에 온티켓의 프론트(Next.js)를 Vercel에 올리는 것이다.

'Vercel'이라는 이름은 라틴어 어원의 조어로 흔히 '전환·바꾼다'는 어감으로 읽히는데, 정작 중요한 건 이름보다 그들이 유명하게 만든 개념이다. Vercel은 "git push하면 배포된다" 는 경험을 대중화한 회사로 통한다. 개발자가 배포를 신경 쓰지 않고 코드에만 집중하게 하는 것이 이 회사의 철학이다.

Vercel 말고 다른 선택지도 많다. 표로 감을 잡아 두자.

서비스 특징 이 과정에서
Vercel Next.js 제작사 운영. Git 연동 자동배포·프리뷰가 강점 프론트(web) 배포에 사용
Netlify Vercel과 비슷한 프론트 특화 호스팅. 오래된 경쟁자 대안. 방식이 거의 같다
Cloudflare Pages Cloudflare의 프론트 호스팅. Workers와 궁합 대안
GitHub Pages 정적 사이트에 특화. 무료·간단하지만 기능 제한 단순 페이지엔 충분
Cloudflare Workers 서버(API) 실행에 특화 API(api) 배포에 사용(앞 장에서 배포 완료)

온티켓은 프론트를 Vercel에, API를 Cloudflare Workers에 나눠 올린다. "왜 한 군데에 안 몰아넣지?"라는 질문이 나올 수 있는데, 프론트(화면 그리기)와 API(데이터 처리)는 성격이 다른 일이라 각자 잘하는 집을 골라 준 것이다. 이렇게 화면과 서버를 갈라서 배포하는 구조를 분리형 아키텍처라 하고, 온티켓은 처음부터 이 구조로 설계됐다. 두 집이 다르기 때문에 이 장 첫머리의 두 가지(주소·CORS)를 맞춰 줄 필요가 생기는 것이기도 하다.

요금 이야기도 초보에게는 걱정거리다. Vercel은 개인·학습용으로 쓸 수 있는 무료 등급(Hobby) 을 제공한다. 온티켓처럼 학습용 프로젝트를 배포하고 접속을 확인하는 정도는 무료 등급 안에서 충분히 소화된다. 팀이 상업적으로 쓰거나 트래픽이 커지면 유료 등급으로 올리지만, 이 과정에서는 무료 등급으로 처음부터 끝까지 경험할 수 있다.

[짚고 가기] Vercel이 뭐고 Cloudflare가 뭔지 자꾸 헷갈리기 쉽다. 한 줄로 정리하면 이렇다 — "화면은 Vercel, 서버는 Cloudflare." 온티켓의 두 폴더(web/·api/)와 두 호스팅을 짝지어 그려 두면 이후 CORS 개념이 훨씬 쉽게 잡힌다. 그리고 "무료로 되나?" 하는 걱정도 흔한데, 이 과정은 Hobby 등급 안에서 모든 실습을 마칠 수 있으니 안심하고 실습하면 된다.

빌드 (build)

우리가 쓴 코드는 사람이 읽기 좋은 형태다. 브라우저가 빠르게 실행하려면 압축·최적화된 형태로 변환해야 하는데, 이 과정이 빌드다. 로컬 개발 서버는 빌드 없이 즉석 실행했지만, 배포에는 빌드된 결과물이 올라간다. Vercel이 배포할 때마다 이 빌드를 자동으로 한다. "로컬에선 됐는데 배포에서 깨지는" 문제의 상당수가 빌드 단계에서 드러난다 — 그래서 배포 후 검증이 중요하다.

'빌드(build)'는 말 그대로 '짓는다'는 뜻이다. 흩어져 있는 재료(우리가 쓴 여러 파일)를 모아 하나의 완성된 결과물(브라우저가 바로 실행할 수 있는 최적화된 파일 묶음)로 조립하는 과정이라 이 이름이 붙었다. 요리에 비유하면, 개발 서버는 고객이 주문할 때마다 즉석에서 만들어 주는 오픈 키친이고, 빌드는 미리 대량으로 조리해 포장까지 끝내 두는 것이다. 포장된 완제품이라야 인터넷 어디서든 빠르게 배달될 수 있다.

여기서 초보가 자주 놀라는 지점 하나. 로컬 개발 서버에서 잘 돌던 화면이 빌드할 때 처음으로 에러를 뿜는 경우가 있다. 개발 서버는 관대해서 몇몇 문제를 눈감고 넘어가지만, 빌드는 엄격하게 전부 검사하기 때문이다. 대표적으로 타입 오류, 쓰지 않는 import, 서버에서만 되는 코드를 화면에 섞어 쓴 경우 등이 빌드에서 걸린다. 이건 실패가 아니라 "세상에 나가기 전 마지막 정밀 검사" 라고 보면 된다. 빌드가 막히면 당황하지 말고 에러 원문을 그대로 AI에게 주면 된다.

프리뷰 배포 (Preview Deployment)

Vercel의 강력한 기능 하나. PR을 올리면 그 변경만 반영된 임시 미리보기 주소를 자동으로 만들어 준다. main에 합치기 전에 "실제 배포된 모습"을 안전하게 확인할 수 있다. API를 배포하며 말한 스테이징의 가벼운 버전이 PR마다 공짜로 생기는 셈이다. 팀이라면 리뷰어가 이 주소를 열어 눈으로 확인한다.

이 개념이 왜 대단한지 짚어 보자. 전통적인 방식에서는 "이 변경이 실제 배포하면 어떻게 보일까?"를 확인하려면, 위험을 무릅쓰고 진짜 서비스에 올려 보거나, 따로 테스트용 서버를 손수 준비해야 했다. Vercel은 PR 하나하나마다 그 변경만 반영된 진짜 배포 주소를 자동으로 뽑아 준다. 프로덕션(진짜 서비스)은 건드리지 않은 채, "만약 이 코드가 나갔다면" 화면을 안전하게 미리 볼 수 있다.

두 배포를 구분해 두면 헷갈리지 않는다.

구분 언제 생기나 주소 성격 목적
프리뷰 배포 PR을 올릴 때(브랜치마다) 임시·매번 새로 생김 병합 전 미리보기·리뷰
프로덕션 배포 main에 병합될 때 고정된 공개 서비스 주소 고객이 실제로 쓰는 서비스

온티켓에서 이 기능은 실습의 '자동 배포 확인'에서 직접 눈으로 확인한다. 홈 화면 문구 한 줄을 바꾼 PR을 올리면, Vercel이 그 PR에 미리보기 주소를 달아 주는지 본다. 이 경험이 CD가 진짜로 돌고 있다는 첫 번째 증거가 된다.

프로덕션 환경변수

프론트에도 환경변수가 있다. 로컬은 web/.env.localNEXT_PUBLIC_API_URL=http://localhost:8787 이었다. 배포 환경에서는 이 값을 Vercel의 환경변수 설정에 프로덕션 API 주소로 넣는다. 이름은 같고 값만 환경마다 다르다 — 코드는 한 글자도 안 바뀐다. "환경변수의 두 세계"가 프론트에서도 똑같이 작동한다.

(주의: NEXT_PUBLIC_으로 시작하는 환경변수는 화면 코드에 포함되어 브라우저에 노출된다. 그래서 여기엔 공개돼도 되는 값만 넣는다 — API 주소는 공개돼도 되지만, 시크릿 키는 절대 여기 넣지 않는다. 결제를 붙일 때 배운 클라이언트/시크릿 키의 구분이 여기서도 지켜진다.)

이 주의사항은 배포에서 사고가 가장 자주 나는 지점이라 조금 더 깊이 파 두자. 먼저 환경변수(environment variable) 는 코드 안에 값을 직접 박아 넣는 대신, "환경(로컬이냐 배포냐)에 따라 달라지는 값"을 코드 바깥에 따로 빼 둔 것이다. API 주소가 대표적이다 — 로컬에선 localhost:8787, 배포에선 ...workers.dev. 값을 바깥에 빼 두었기 때문에, 코드는 NEXT_PUBLIC_API_URL이라는 이름만 부르고, 실제 값은 환경마다 다르게 채워진다. 그래서 "코드는 한 글자도 안 바뀐다".

이제 NEXT_PUBLIC_이라는 접두어의 의미다. Next.js에는 두 종류의 코드가 있다 — 서버에서만 도는 코드브라우저(고객의 화면)까지 내려가는 코드. 환경변수는 기본적으로 서버에만 머무는데, 이름 앞에 NEXT_PUBLIC_을 붙이면 "이 값은 브라우저에 실어 보내도 좋다" 는 표시가 된다. 접두어의 뜻 그대로 'public(공개)'인 것이다. 브라우저로 내려간 값은 누구나 개발자 도구를 열어 들여다볼 수 있다 — 숨길 수 없다.

여기서 철칙이 나온다.

'클라이언트 키'와 '시크릿 키'의 구분은 결제를 붙일 때 이미 배웠다. 그 구분이 배포에서도 그대로 살아 있다는 것이 핵심이다. 판별법은 한 문장이면 된다 — "이 값이 브라우저 개발자 도구에 그대로 보여도 괜찮은가?" 괜찮으면 NEXT_PUBLIC_, 안 괜찮으면 서버(api) 환경변수로 보낸다.

[짚고 가기] 여기서 "그럼 시크릿 키는 어디에 두냐"가 헷갈리기 쉽다. 토스 결제 승인처럼 시크릿 키가 필요한 일은 프론트가 아니라 api(서버)가 한다. 그래서 시크릿 키는 api 쪽 환경변수(API를 배포하며 Cloudflare에 넣은 시크릿)에 있고, 프론트에는 올 일이 없다. "프론트는 공개 값만, 서버는 비밀 값도"라는 한 줄로 정리해 두면 헷갈리지 않는다. 실무에서 시크릿 키를 프론트에 잘못 넣어 유출되는 사고가 흔하니, 이 구분은 확실히 익혀두는 게 좋다.

CD (지속적 배포, Continuous Deployment)

CD는 코드가 저장소에 올라오면 사람이 손대지 않아도 자동으로 배포까지 이어지는 방식을 말한다. 'Continuous Deployment'의 머리글자로, '지속적 배포'로 옮긴다. 앞에서 만든 CI(Continuous Integration, 지속적 통합) — 코드가 올라올 때마다 자동으로 검사(테스트·린트)를 돌리는 것 — 의 짝이다. CI가 "올라온 코드가 멀쩡한지 자동 검사"라면, CD는 "검사를 통과한 코드를 자동으로 세상에 배포"다.

두 글자를 나란히 두면 관계가 보인다.

약자 풀이 하는 일 온티켓에서
CI 지속적 통합 push하면 자동으로 테스트·검사 CI를 세울 때 (GitHub Actions)
CD 지속적 배포 검사 통과분을 자동으로 배포 이번 장 (Vercel Git 연동)

"왜 이름에 '지속적(Continuous)'이 붙나?"도 짚어 두자. 예전에는 몇 주에 한 번씩 날을 잡아 큰 배포를 몰아서 했다("이번 주말이 배포일"). 실패하면 크게 실패했다. 지금은 작은 변경이 생길 때마다 끊임없이(지속적으로) 검사하고 배포한다. 작게 자주 내보내면, 문제가 생겨도 방금 바꾼 작은 조각만 의심하면 되니 안전하다. '지속적'이라는 말에는 이 "작게, 자주, 끊김 없이"라는 철학이 담겨 있다.

온티켓에서 CD는 Vercel의 Git 연동이 담당한다. GitHub 저장소를 Vercel에 연결해 두면, main에 코드가 병합되는 순간 Vercel이 알아서 빌드하고 배포한다. 사람이 배포 명령을 치지 않는다는 것 — 그 상태를 마지막 실습에서 눈으로 증명한다.


22.3 개념 — 배포 순서와 검증의 눈

두 조각을 잇는 순서는 정해져 있다.

1. 프론트의 프로덕션 API 주소 설정  (Vercel 환경변수 = 배포된 API URL)
2. 프론트 배포                      (Vercel이 빌드·배포 → 프론트 공개 URL)
3. API의 CORS에 프론트 주소 허용     (배포된 프론트 URL을 api 허용 목록에)
4. 처음부터 끝까지 검증               (공개 화면에서 실제 예매 한 바퀴)

배포된 web·api·DB가 인터넷에서 연결되는 최종 구조와 push→CI→CD 파이프라인

3번을 빠뜨리면 화면은 뜨는데 데이터가 안 나온다(CORS 차단). 이때 당황하지 않는 법은 로컬에서 프론트를 붙일 때 배웠다 — 콘솔의 CORS 에러 원문을 그대로 AI에게 주고, "고칠 곳이 api"임을 안다. 프로덕션이라고 원리가 다르지 않다.

이 순서에는 이유가 있다. 왜 하필 이 순서인지 풀어 보자.

여기서 가장 중요한 안전장치는 "3번을 빠뜨려도 화면은 뜬다" 는 점을 미리 아는 것이다. 초보는 "화면이 떴으니 다 됐다"고 착각하기 쉽다. 하지만 화면(HTML·CSS)은 프론트 혼자서 그릴 수 있고, 목록 데이터는 API에서 와야 한다. CORS 허용 전에는 화면은 멀쩡히 뜨는데 목록 자리만 비어 있는 중간 상태가 된다. 이건 고장이 아니라 3번을 아직 안 한 정상적인 중간 단계다. 이 사실을 미리 알면, 그 순간 당황하지 않고 "아, CORS 차례구나" 하고 다음 단계로 넘어갈 수 있다.

CORS를 다시 한 줄로 복습하면: 브라우저는 안전을 위해, 화면을 띄운 출처(origin)와 다른 주소로 요청을 보낼 때 "저쪽 서버가 나를 허락했는가" 를 따진다. 서버가 허락 목록(CORS 허용 출처)에 내 주소를 넣어 두지 않으면, 브라우저가 스스로 응답을 막아 버린다. 로컬에선 API가 localhost:3000을 허락해 뒀지만, 배포된 프론트는 주소가 달라졌으니 다시 허락받아야 한다. 전체 허용(*)으로 뚫어 버리는 것은 금물 — 아무 사이트나 우리 API를 부를 수 있게 되기 때문이다. 필요한 출처만 콕 집어 허용한다.

검증의 핵심은 처음부터 끝까지(E2E) 다. 조각별로 "떴다"가 아니라, 고객이 실제로 하는 여정 — 공개 주소 접속 → 목록 → 예매 → 결제 → QR — 이 처음부터 끝까지 인터넷 위에서 도는지 본다.

'E2E'는 'End to End(처음부터 끝까지)'의 약자다. 왜 조각 검사로는 부족한가? 프론트도 떴고, API도 떴고, DB도 살아 있다 — 조각은 다 초록불인데, 정작 고객이 예매를 못 하는 경우가 있다. 조각 사이의 연결(주소·CORS·인증)이 어긋나면, 개별 부품은 멀쩡해도 여정이 끊긴다. E2E 검증은 부품이 아니라 고객의 여정 전체를 따라가며, 인터넷을 가로지르는 그 연결까지 실제로 도는지 확인한다. 온티켓에서는 "리버사이드 재즈 목록 → 예매 → 테스트 결제 → QR → DB에 주문 저장"이 한 줄로 이어지는지를 본다. 이 한 바퀴가 성공하면, 온티켓은 정말로 '서비스'가 된 것이다.

[짚고 가기] 이 절은 "화면 떴으니 끝"이라는 착각을 깨는 자리다. 직접 CORS 허용 전 상태(화면은 뜨는데 목록이 빈 상태)를 만들어 보고, 콘솔의 빨간 CORS 에러를 읽은 뒤, 허용을 넣고 새로고침해서 목록이 채워지는 장면을 확인해 보면 확실히 몸에 남는다. "빈 화면이 고장이 아니라 3번 차례라는 신호"라는 것을 손으로 익히는 것이 이 절의 목표다.


22.4 프롬프트 작성법 — 연결하고 검증한다

이 장의 지시는 대부분 "설정을 맞추고, 배포하고, 연결을 검증"하는 종류다. 프롬프트에서 잊지 말아야 할 것은 세 가지다 — 어떤 값을 넣는지 명확히, 어디를 고치는지(프론트인지 api인지) 명확히, 무엇이 보이면 됐는지(완료 기준)를 넣는 것. 아래 사례들을 이 기준으로 읽어 보자.

프로덕션 환경변수 설정 안내

프롬프트
web/ 을 Vercel에 배포하기 전에 프로덕션 환경변수를 맞춰야 해.
NEXT_PUBLIC_API_URL 을 배포된 API 주소({배포API URL})로 설정하려고 하는데,
Vercel에서 환경변수를 넣는 방법을 알려줘. (내가 웹에서 설정한다.)
토스 클라이언트 키도 프로덕션 환경변수로 넣어야 하면 함께 안내해줘.

이 지시가 좋은 이유는 (1) 어떤 값(NEXT_PUBLIC_API_URL)을 (2) 무엇으로 (배포 API 주소) 넣는지 못 박고, (3) 실제 클릭은 내가 웹에서 하겠다고 역할을 나눠 준 데 있다. Vercel 환경변수 설정은 웹 대시보드에서 하는 일이라, AI에게는 "방법을 안내"하게 시키고 손은 사람이 움직이는 분업이 자연스럽다.

배포 트리거

Git 연동이 되어 있으면 배포는 push로 일어난다.

프롬프트
web/ 을 프로덕션에 배포하자. main 에 반영하면 Vercel이 자동 배포하는
상태가 맞는지 확인하고, 배포가 시작되면 진행 상황과 최종 프론트 URL을 알려줘.

여기서 핵심 요구는 마지막 문장 — "최종 프론트 URL을 알려줘" 다. 이 URL이 바로 다음 단계(CORS 허용)에 넣을 값이라, 반드시 받아 둬야 한다. "자동 배포하는 상태가 맞는지 확인"까지 시킨 것은, Git 연동이 제대로 걸려 있는지 점검하는 안전장치다.

CORS 프로덕션 허용 지시

프롬프트
/backend 배포된 프론트 주소({프론트URL})를 api의 CORS 허용 출처에 추가해줘.
로컬(localhost:3000)도 개발용으로 유지하고, 전체 허용(*)은 쓰지 마.
반영하려면 api를 재배포해야 하면 그것까지.

이 지시에는 좋은 제약이 세 겹 들어 있다 — (1) 로컬도 유지(개발을 계속해야 하니까), (2) 전체 허용 금지(보안), (3) 재배포까지(코드만 고치고 반영을 안 하면 소용없으니까). CORS는 api 코드에 있으므로 고칠 곳이 api임을 지시 첫머리에 박아 둔 것도 요령이다.

처음부터 끝까지 검증 지시

프롬프트
프로덕션 전체를 검증하자. 배포된 프론트 URL을 열었을 때:
1) 이벤트 목록이 뜨는가 (배포 API에서 데이터가 오는가)
2) 예매 → 테스트 결제 → 완료(QR) 까지 되는가
3) 그 주문이 Supabase 프로덕션 DB에 실제로 저장됐는가
각 단계에서 확인할 지점을 알려줘. 내가 브라우저로 직접 밟아볼게.

이 지시의 미덕은 검증을 고객의 여정 순서대로 쪼개, 각 단계에서 "무엇을 보면 통과인지"를 AI에게 정리하게 시킨 데 있다. 실제 클릭은 사람이 하되(브라우저로 직접 밟아 본다), 확인 포인트는 AI가 챙겨 주는 분업이다.

약한 지시 → 강한 지시

같은 목적이라도 어떻게 말하느냐에 따라 결과가 갈린다. 배포 구간에서 특히 흔한 약한 지시들을, 이 장의 원칙(어떤 값·어디·완료 기준)으로 고쳐 보자.

흔한(약한) 지시 고친(강한) 지시 무엇을 바꿨나
배포해줘 web/을 프로덕션에 배포하고, 끝나면 최종 프론트 URL을 알려줘 무엇을 배포하는지 + 받아야 할 결과(URL) 명시
화면에 목록이 안 떠, 고쳐줘 (콘솔의 CORS 에러 원문 붙여넣고) 이 에러가 났어. 원인부터 설명하고, api의 CORS 허용에 배포된 프론트 주소를 추가해줘 증상만 말하지 말고 근거(에러 원문) 제공 + 고칠 곳(api) 지정
환경변수 좀 넣어줘 NEXT_PUBLIC_API_URL을 {배포API URL}로 Vercel 프로덕션 환경변수에 넣는 방법을 안내해줘. 시크릿 키는 여기 넣지 않아 어떤 값·어디에·금지선(시크릿 금지)까지
CORS 열어줘 배포된 프론트 주소만 api CORS에 추가하고, 전체 허용(*)은 쓰지 마. 로컬도 유지 "열어줘"의 범위를 콕 집고 위험한 전체 허용을 차단
잘 되는지 봐줘 공개 프론트 URL에서 목록→예매→테스트결제→QR→DB저장까지 한 바퀴 확인할 지점을 단계별로 알려줘 막연한 "잘"을 고객 여정 단계별 완료 기준으로

세 가지 실전 감각을 덧붙인다.

[짚고 가기] 가장 흔히 저지르는 약한 지시가 "화면에 아무것도 안 떠요, 고쳐줘"다. 이럴 때는 "무엇을 붙여야 AI가 고칠 수 있을까?"를 스스로 되물어, 콘솔 에러 원문을 직접 복사해 붙이는 게 좋다. 에러 원문을 읽고 붙이는 습관이 배포 트러블슈팅의 8할이다. 첫날 익힌 "원인부터 설명하고 고쳐줘"가 여기서 다시 위력을 발휘한다.


22.5 실습 — 서비스를 완성한다

이제 앞에서 정한 4단계 순서를 실제로 밟는다. 이 실습이 끝나면 온티켓은 인터넷 위에서 처음부터 끝까지 도는 진짜 서비스가 된다. 각 따라하기는 4단계 순서와 그대로 맞물린다 — 환경변수(1) → 배포(2) → CORS(3) → 검증(4), 그리고 자동 배포 증명.

따라하기 1 — 프론트 프로덕션 환경변수

아래 지시로 Vercel 환경변수 설정 방법을 안내받아, Vercel 웹에서 값을 넣는다. NEXT_PUBLIC_API_URL = API를 배포할 때 메모해 둔 배포 API 주소, 그리고 토스 클라이언트 키(테스트 키). 시크릿 키는 여기 넣지 않는다는 것을 다시 확인한다.

프롬프트
web/ 을 Vercel에 배포하기 전에 프로덕션 환경변수를 맞춰야 해.
NEXT_PUBLIC_API_URL 을 배포된 API 주소({배포API URL})로 설정하려고 하는데,
Vercel에서 환경변수를 넣는 방법을 알려줘. (내가 웹에서 설정한다.)
토스 클라이언트 키도 프로덕션 환경변수로 넣어야 하면 함께 안내해줘.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

구체적으로는 Vercel 대시보드에서 온티켓 프로젝트를 열고, 설정(Settings)의 Environment Variables 화면에서 값을 추가한다. AI에게 "방법을 안내해줘"라고 시켜 두었으니, 화면 경로를 안내받아 그대로 따라가면 된다. 넣을 값은 두 개다.

이름 왜 공개 값인가
NEXT_PUBLIC_API_URL 배포된 API 주소(https://onticket-api....workers.dev) 브라우저가 그 주소로 요청하니 어차피 공개됨
토스 클라이언트 키(테스트) 토스에서 받은 테스트용 클라이언트 키 결제창을 브라우저에서 띄우는 공개용 키

넣기 전에 스스로 한 번 물어본다 — "이 값이 브라우저 개발자 도구에 보여도 괜찮은가?" 둘 다 "괜찮다"이므로 프론트 환경변수에 넣어도 된다. 반대로 토스 시크릿 키는 이 질문에 "안 괜찮다"이므로 여기 넣지 않는다. 그 키는 이미 api 쪽에 들어가 있고, 결제 승인은 api가 처리한다. 이 확인이 용어 정리에서 배운 NEXT_PUBLIC_ 철칙을 실제로 지키는 순간이다.

따라하기 2 — 프론트 배포

main에 반영해 Vercel 자동 배포를 트리거한다. Vercel 대시보드(또는 AI가 알려 주는 진행 상황)에서 빌드가 도는 것을 보고, 끝나면 프론트 공개 URL을 받는다. 그 주소를 브라우저로 연다 — 화면은 뜨는데, 아직 목록이 안 나올 수 있다 (3번 CORS 전이라면). 정상적인 중간 상태다.

배포가 도는 동안 Vercel 대시보드에서는 빌드 로그가 실시간으로 흐른다. "Building..." 상태를 지나 초록불(Ready)이 되면 배포 완료다. 이때 받는 공개 URL(https://<프로젝트>.vercel.app 형태)을 잘 적어 둔다 — 바로 다음 단계의 CORS 허용에 넣을 바로 그 주소다.

여기서 화면은 떴는데 목록 자리가 비어 있어도 놀라지 말자. 배포 순서를 설명하며 예고한 그 중간 상태다. 화면(HTML·CSS)은 프론트 혼자 그리지만, 목록 데이터는 아직 허락받지 못한 API에서 오지 못하고 있는 것이다. 고장이 아니라 다음 단계(CORS)를 기다리는 정상 신호다. 브라우저 개발자 도구(F12)의 콘솔을 열어 두면, 다음 단계에서 다룰 CORS 에러 원문을 미리 볼 수 있다.

만약 배포가 초록불에 닿기 전에 빌드에서 실패한다면(프론트 배포 단계에서 URL을 못 받는 경우), 용어 정리에서 말한 "세상에 나가기 전 마지막 정밀 검사"에 걸린 것이다. Vercel 대시보드의 빌드 로그 맨 아래에 실패 원인이 찍혀 있으니, 그 원문을 그대로 복사해 AI에게 주고 "빌드가 이 에러로 실패했어. 원인부터 설명하고 고쳐줘"라고 시킨다.

따라하기 3 — CORS 프로덕션 허용

목록이 안 뜨고 콘솔에 CORS 에러가 보이면, 원문을 확인하고 아래 CORS 지시를 실행한다. api에 프론트 주소를 허용하고 재배포한다. 다시 프론트 주소를 새로고침 — 이제 목록이 뜬다. 배포된 두 앱이 인터넷을 가로질러 악수했다.

콘솔에 찍히는 CORS 에러는 대략 이런 모습이다(문구는 브라우저마다 조금씩 다르다).

Access to fetch at 'https://onticket-api....workers.dev/events'
from origin 'https://<프론트>.vercel.app' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

이 원문이 말하는 바를 읽어 보자 — "<프론트> 주소에서 API로 보낸 요청이 CORS 정책에 막혔다. 응답에 나(프론트)를 허용한다는 표시가 없다." 즉 API가 아직 이 프론트 주소를 허락 목록에 안 넣었다는 뜻이다. 고칠 곳은 명백히 api다. 아래 CORS 지시에서 {프론트URL} 자리에 프론트를 배포할 때 받은 실제 주소를 넣어 실행한다. 로컬(localhost:3000)은 계속 개발해야 하니 함께 유지하고, 전체 허용(*)은 쓰지 않는다.

프롬프트
/backend 배포된 프론트 주소({프론트URL})를 api의 CORS 허용 출처에 추가해줘.
로컬(localhost:3000)도 개발용으로 유지하고, 전체 허용(*)은 쓰지 마.
반영하려면 api를 재배포해야 하면 그것까지.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

CORS는 api 코드에 있으니, 고친 뒤 api를 재배포해야 반영된다(코드만 고치고 배포를 안 하면 세상에 나간 API는 그대로다). 재배포까지 끝나면 프론트 주소를 새로고침한다. 비어 있던 자리에 리버사이드 재즈를 비롯한 이벤트 목록이 채워지면, 배포된 두 앱이 인터넷을 가로질러 악수한 것이다. 이 순간이 이 장 첫머리에서 말한 "두 조각을 하나의 서비스로" 잇는 연결이 완성되는 지점이다.

따라하기 4 — 처음부터 끝까지 예매

공개 프론트 주소에서 실제로 예매를 한 바퀴 돈다. 리버사이드 재즈 → 등급·수량 → 이메일 → 결제(테스트) → 완료 화면 QR. 그리고 Supabase 프로덕션 DB의 orders에서 방금 주문을 확인한다. 인터넷 위에서, 처음부터 끝까지 도는 온티켓을 눈으로 본다.

이것이 앞에서 말한 처음부터 끝까지(E2E) 검증이다. 조각별로 "떴다"가 아니라, 고객의 실제 여정이 인터넷 위에서 끊김 없이 도는지 확인한다. 각 단계에서 무엇을 보면 통과인지 정리하면 이렇다.

단계 고객의 행동 통과 신호 어떤 연결을 증명하나
접속 공개 프론트 URL 열기 화면이 뜬다 프론트 배포 성공
목록 이벤트 목록 확인 리버사이드 재즈 등이 보인다 프론트↔API 주소·CORS 연결
예매 등급·수량 선택, 이메일 입력 주문 단계로 넘어간다 API 주문 처리
결제 테스트 결제 진행 결제 완료로 넘어간다 토스(클라이언트 키)↔api(승인)
완료 완료 화면 확인 QR 티켓이 보인다 예매 전체 마무리
저장 Supabase orders 확인 방금 주문이 실제로 있다 API↔프로덕션 DB 저장

마지막 줄이 특히 중요하다. 화면에서 "완료"가 떴다고 끝이 아니라, 그 주문이 프로덕션 DB(Supabase)의 orders 테이블에 실제로 남았는지까지 확인해야 진짜 E2E다. 화면만 성공한 척하고 데이터는 안 남는 경우를 잡아내는 마지막 관문이다. 방금 예매의 이메일이나 주문번호로 orders에서 찾으면 된다. 이 한 줄까지 확인되면, 온티켓은 인터넷 위에서 처음부터 끝까지 도는 서비스임이 증명된다.

따라하기 5 — 자동 배포 확인 (CD의 증명)

CD가 진짜 도는지 확인한다. 아주 작은 변경(예: 홈 화면의 문구 한 줄)을 브랜치에서 만들어 PR을 올린다.

push → 검사(CI) → 병합 → 자동 배포(CD). 사람이 배포 명령을 치지 않았는데 변경이 세상에 반영됐다. 완전한 자동화의 흐름이 닫혔다.

이 실습을 조금 더 음미해 보자. 변경은 일부러 아주 작게 잡는다 — 홈 화면의 문구 한 줄이면 충분하다. 작게 잡는 이유는 "무엇이 바뀌었는지"를 눈으로 즉시 확인하기 쉽기 때문이다. 이 실습에서 보는 것은 코드가 아니라 자동화의 흐름 그 자체다.

이 마지막 문장을 곱씹어 보자 — "사람이 배포 명령을 치지 않았는데 변경이 세상에 반영됐다." 첫날 코드 한 줄을 손으로 복사해 나르던 데서 시작해, 3일 만에 배포조차 사람 손을 떠나는 지점에 온 것이다. push 한 번이 검사와 배포로 이어지는 완전한 흐름 — 이것이 이 과정 전체가 향해 온 종착점이다.

따라하기 6 — 서비스 정보 기록

프롬프트
CLAUDE.md와 docs에 서비스 주소를 정리해줘:
- 프론트(서비스): {프론트URL}
- API: {배포API URL}
- 배포: web은 Vercel Git 연동 자동배포, api는 wrangler deploy
- 프론트 환경변수: NEXT_PUBLIC_API_URL, 토스 클라이언트 키 (Vercel 설정)

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

마지막으로 서비스의 좌표를 문서에 남긴다. 이 기록이 중요한 이유는, 며칠 뒤 다시 이 프로젝트를 열었을 때(또는 다른 사람이 이어받을 때) "프론트 주소가 뭐였지? 배포는 어떻게 되지?"를 헤매지 않게 하기 위해서다. AI(Claude Code)도 CLAUDE.md를 읽고 프로젝트의 현재 상태를 파악하므로, 여기에 배포 정보를 적어 두면 이후 지시에서 AI가 두 주소와 배포 방식을 정확히 인지한 채로 일한다. 두 앱의 배포 방식이 다르다는 것(web은 Git 연동 자동배포, api는 wrangler 명령)까지 적어 두는 것이 요령이다.

완료 기준 체크리스트

[짚고 가기] 이 체크리스트는 앞에서 정한 4단계 + CD 증명과 일대일로 대응한다. "다 됐다" 싶을 때, 특히 아래에서 두 번째 항목(주문이 프로덕션 DB에 실제로 저장됐는지)은 반드시 눈으로 확인하고 넘어가자. 화면만 성공한 척하고 데이터가 안 남는 경우가 실무에서 흔한 함정이라, 이 항목을 건너뛰지 않는 것이 E2E 검증 습관의 핵심이다. 시간이 부족하면 '자동 배포 확인'(CD 증명)은 눈으로만 확인하고 넘어가도 되지만, 나머지는 직접 손으로 밟아 보길 권한다.


22.6 정리

오명운 · macro@prag-ai.com