2026. 7. 15. 16:28ㆍAI 기반 프론트엔드 웹개발자 양성과정

[Vercel, Claude Web] Vercel로 진짜 배포하기
+ Claude Code로 웹사이트 설계·디자인 단계별로 발전시키기
2026.07.15(수) · Vercel(CLI 배포) · Claude Web(Plan Mode ~ design.md) · Anaconda(설치)
📋 오늘 한 일
어제 만든 Groq 챗봇을 진짜 서버리스 구조로 바꿔 Vercel에 배포했고, 지난주 만든 React 미니 프로젝트(트래블틱)도 다시 배포해봤다. 오후에는 Claude Code로 같은 계산기 앱을 일곱 번 다시 만들며 "계획 없이 vs 계획하고 vs 계획+질문하고", 그리고 "공식 디자인 스킬 vs design.md 명세서 기반"으로 결과물이 어떻게 달라지는지 직접 비교했다. 마지막엔 다음 단계(Python·데이터)를 위해 Anaconda까지 설치했다.
1. Vercel — 상시 서버를 "필요할 때만 켜지는 함수"로
어제 만든 08_groq는 node index.js로 계속 켜둬야 하는 Express 서버였다. 오늘은 이걸 Vercel이 요청 올 때만 실행하는 서버리스 함수 구조로 바꿨다. 핵심은 "상시 서버 → 파일 하나짜리 함수"라는 발상의 전환이었다.
| 바꾸기 전 (Express) | 바꾼 뒤 (Vercel 서버리스) |
|---|---|
server/index.js — app.listen(3000)으로 상시 대기 | api/chat.js — 요청 올 때만 실행되는 함수 하나 |
fetch('http://localhost:3000/api/chat') — 절대경로 | fetch('/api/chat') — 상대경로 (같은 도메인이라 그대로 동작) |
cors() 필요 (화면·서버 포트가 다름) | 같은 도메인이라 CORS 자체가 불필요 |
// 08_groq/api/chat.js — Vercel 서버리스 함수, 파일 하나가 곧 /api/chat 창구 require('dotenv').config() module.exports = async (req, res) => { if (req.method !== 'POST') { res.status(405).json({ error: 'Method not allowed' }) return } const { prompt } = req.body || {} const key = process.env.GROQ_API_KEY try { const groqRes = await fetch("https://api.groq.com/openai/v1/chat/completions", { method: "POST", headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, body: JSON.stringify({ model: 'llama-3.1-8b-instant', messages: [{ role: "user", content: prompt }] }) }) const data = await groqRes.json() const reply = data.choices?.[0]?.message?.content || "응답 없음" res.status(200).json({ reply }) } catch (err) { res.status(500).json({ error: err.message }) } }
app.js는 /api/chat을 불렀는데 서버는 /chat만 갖고 있어서 응답을 못 받는 버그가 있었다. 오늘 Vercel용으로 새로 짠 api/chat.js는 파일 경로 자체가 /api/chat 주소가 되는 구조라(Vercel이 api/ 폴더를 자동으로 라우팅), 처음부터 주소가 어긋날 일이 없었다. 구조를 바꾸는 김에 어제의 실수가 자연스럽게 해결된 셈이다.CLI 배포 — 터미널 몇 줄이면 끝
vercel은 실행한 현재 디렉토리를 배포 대상으로 잡는다 — 그래서 항상 프로젝트 폴더 안에서 실행하는 게 첫 번째 원칙이다.
// ① 어디서 실행? — 프로젝트 루트(package.json 있는 곳)에서 cd my-app # 프로젝트 루트로 이동 ls # package.json 보이면 위치 맞음 // ② 설치 · 로그인 · 배포 npm i -g vercel # CLI 설치, 최초 1회 vercel login # 브라우저 인증, 최초 1회 vercel # preview 배포 — 임시 URL vercel --prod # production 배포 — 실서비스 URL
vercel(옵션 없이) = preview — 매번 새 임시 URL이 나오고 실서비스엔 영향 없음. vercel --prod = production — 실서비스 도메인이 바로 갱신됨. 단, 첫 배포는 아직 production이 없어서 자동으로 승격된다. --pord(오타)와 --prod 헷갈리지 않게 주의.처음 vercel을 실행하면 몇 가지를 물어본다 — 이미 만들어둔 프로젝트라면 대부분 기본값(엔터)으로 넘어가면 된다.
// ③ vercel 첫 실행 시 묻는 것 ? Set up and deploy? → y ? Project name? → my-app ? Code directory? → ./ (현재 폴더 = 프로젝트 루트) ✔ Detected Vite → 빌드 설정 자동 감지 (vite build · dist) ? Customize settings? → no
⚠️ 배포하자마자 터진 진짜 문제 — 서버(함수)가 응답을 안 함
명령어로는 배포가 "성공"이라고 뜨는데, 정작 배포된 사이트에서 채팅을 보내면 서버(함수) 쪽이 응답을 안 했다. 로컬에서 node index.js로 돌릴 땐 멀쩡했던 게, Vercel에만 올리면 /api/chat이 제대로 안 붙는 문제였다. 원인은 vercel.json이 너무 단순했다는 것 — 처음엔 이 정도만 적어뒀었다.
// 처음 vercel.json — 화면(index.html) 라우팅만 있고, 정작 api·public 구조는 설명이 없었음 { "rewrites": [ { "source": "/", "destination": "/index.html" } ] }
이 상태에서는 Vercel이 "이 프로젝트에서 뭐가 서버리스 함수고, 뭐가 그냥 정적 파일인지"를 스스로 추측해야 했다. 마침 폴더 루트에 예전 Express용 index.js가 그대로 남아 있던 것까지 겹쳐서, 자동 감지가 꼬였던 것 같다. 결국 무엇을 함수로 돌리고, 무엇을 그대로 서빙할지, 주소마다 어디로 보낼지를 전부 직접 적어주는 vercel.json으로 바꾸고 나서야 해결됐다.
// 고친 vercel.json — builds·routes를 전부 명시 { "version": 2, "builds": [ { "src": "api/chat.js", "use": "@vercel/node" }, { "src": "public/**", "use": "@vercel/static" } ], "routes": [ { "src": "/api/chat", "dest": "/api/chat.js" }, { "src": "/(.*)", "dest": "/public/$1" } ] }
•
builds — "이 파일은 이 방식으로 다뤄줘"를 파일별로 지정. api/chat.js는 @vercel/node(요청 올 때 실행되는 서버리스 함수)로, public/**는 @vercel/static(그대로 내려주는 정적 파일)으로 분리해서 명시했다.•
routes — 주소별로 "어디로 보낼지"를 직접 매핑. /api/chat으로 오는 요청만 함수(api/chat.js)로 보내고, 나머지 전부(/(.*))는 public 폴더 안에서 찾도록(/public/$1) 했다.
예전
rewrites 한 줄짜리 설정은 "화면 진입 주소"만 알려줬을 뿐, 어떤 게 함수고 어떤 게 정적 파일인지는 전혀 설명하지 않았다 — 그래서 Vercel의 자동 감지에 맡겨졌던 거고, 서버·화면 폴더가 뒤섞인 이 프로젝트 구조에선 그 자동 감지가 실패했던 것. 배포 자동화가 안 될 땐 "무엇을 어떻게 처리할지"를 내가 직접 적어줘야 한다는 걸 이번에 제대로 배웠다.
// 환경변수 등록 (배포된 함수도 키가 있어야 함) # 키는 .env에만 있고 깃·배포엔 안 올라가므로, Vercel에 따로 등록 vercel env add GROQ_API_KEY production vercel --prod # 환경변수는 등록 후 반드시 재배포해야 적용됨
.env는 로컬에만 있고 깃에도 배포 서버에도 안 올라간다 — 그래서 배포된 함수가 GROQ_API_KEY를 읽으려면 vercel env add로 Vercel 쪽에 따로 키를 등록해야 한다. 이걸 빠뜨리면 api/chat.js의 key가 비어서 "응답 없음"만 계속 뜨는데, 처음엔 코드가 잘못된 줄 알고 한참 코드만 들여다봤다 — 원인은 코드가 아니라 환경변수 등록이었다. 그리고 환경변수를 바꾼 뒤엔 꼭 다시 vercel --prod로 재배포해야 반영된다는 것도 오늘의 수확.화면도 이 김에 업그레이드했다. 기존엔 질문·답 한 줄씩만 보여주는 단순한 폼이었는데, 오늘은 말풍선이 쌓이는 채팅창으로 바꿨다 — 내 질문은 오른쪽 빨간 말풍선, AI 답은 왼쪽 회색 말풍선으로 구분하고 "생각 중..." 자리표시자와 대화 지우기 버튼까지 추가했다.
// 08_groq/public/app.js — 말풍선 채팅 UI 핵심 function addMsg(text, who) { const div = document.createElement('div') div.className = 'msg ' + who // who: 'me'(질문) 또는 'ai'(응답) div.textContent = text chat.appendChild(div) chat.scrollTop = chat.scrollHeight // 항상 맨 아래로 스크롤 return div } async function sendMessage() { const prompt = q.value.trim() if (!prompt) return addMsg(prompt, 'me') q.value = '' const aiBubble = addMsg('생각 중...', 'ai') // AI 자리 먼저 만들고 const res = await fetch('/api/chat', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({prompt}) }) const data = await res.json() aiBubble.textContent = data.reply || data.error || '응답 없음' // 자리표시자를 실제 답으로 교체 }
aiBubble)를 "생각 중..."으로 먼저 만들어두고, 응답이 오면 그 자리의 textContent만 바꿔치기하는 패턴이 마음에 들었다 — 기다리는 동안 화면이 멈춘 것처럼 보이지 않고, 채팅앱 특유의 "타이핑 중" 느낌이 자연스럽게 났다.지난주 만든 React 미니 프로젝트(06_react/mini, 트래블틱)도 같은 방식으로 vercel --prod를 다시 실행해 재배포했다. 그리고 독립적으로 배포되는 이 두 프로젝트(챗봇·트래블틱)는 메인 hancom 저장소 안에 코드를 그대로 두는 대신, 각자 자기 GitHub 저장소를 갖게 하고 서브모듈로 참조하도록 정리했다 — 배포용 프로젝트와 학습 기록용 저장소를 분리하는 감각을 처음 잡아봤다.
2. Claude Code로 웹사이트 만들기 — 같은 계산기, 일곱 번의 재해석
오후엔 같은 계산기 앱을 워크플로를 바꿔가며 계속 다시 만들었다. 코드를 한 줄도 안 짜고 전부 Claude Code와의 대화만으로 진행했는데, "무엇을 어떻게 부탁하느냐"에 따라 결과물 완성도가 확연히 달라지는 걸 눈으로 확인한 시간이었다.
① 계획 없이 (01_no_plan)
"계산기 만들어줘" 정도로 바로 코드를 생성시켰다. 결과는 사칙연산·AC·부호전환·퍼센트가 되는 기능은 멀쩡한 계산기였지만, 디자인은 무난한 기본기 수준이었다.
② Plan Mode로 설계 먼저 (02_yes_plan)
Shift+Tab으로 Plan Mode에 진입해 요구사항을 먼저 정리하고, Claude가 제시한 구현 계획을 승인한 뒤 코드를 생성했다. 버튼 클래스 이름(btn--func, btn--op 같은 BEM 스타일)부터 훨씬 체계적으로 정리된 게 눈에 띄었다.
③ Plan Mode + 질문까지 주고받은 뒤 (03_yes_plan_and_QNA)
계획 단계에서 Claude에게 궁금한 걸 되묻고 답하는 과정까지 거친 뒤 만든 버전. 결과물이 완전히 달라졌다 — 제목이 "네온 계산기"로 바뀌고, 다크/라이트 테마 전환, 계산 기록 토글, 결과 복사 버튼, Google Fonts(Orbitron)까지 들어간 훨씬 풍부한 앱이 나왔다.
// 03_yes_plan_and_QNA/index.html — 네온 계산기 상단 일부 <body data-theme="dark"> <div class="calc" role="application" aria-label="계산기"> <div class="topbar"> <span class="brand">NEON CALC</span> <button id="historyToggle" title="계산 기록">...</button> <button id="themeToggle" title="테마 전환">...</button> </div>
④ frontend-design 공식 스킬 (04, 05)
Claude의 공식 UI 디자인 스킬을 사용했다. 이 스킬은 코드부터 짜지 않고, 색(4~6개 named hex)·타이포(디스플레이+본문+유틸 서체)·레이아웃(ASCII 와이어프레임)·시그니처(이 페이지만의 기억에 남을 요소 하나) 네 가지를 먼저 정하고, 그게 "어디서나 나올 법한 기본값"은 아닌지 스스로 되짚어 본 뒤에야 코드를 짠다는 게 인상적이었다.
tokens.css의 :root 변수 하나에 몰아두고, 나머지 스타일은 전부 그 변수만 참조하게 만드니 색 하나만 바꿔도 페이지 전체가 같이 바뀌는 구조가 됐다. "예쁘게 해줘"보다 "이 페이지만의 시그니처 요소 하나를 정해달라"고 구체적으로 요청하는 게 훨씬 나은 결과로 이어진다는 걸 배웠다.⑤ design.md 명세서 기반 — 배민 (06) · 라인 + Haiku 모델 (07)
가장 흥미로웠던 두 실습. design.md는 한 브랜드의 색·글씨·간격·부품을 문서 하나로 정리해둔 "디자인 설계도"다. 배달의민족과 LINE의 디자인 시스템을 각각 조사·정리한 문서를 만들고 그걸 근거로 계산기 UI를 새로 입혔다.
• getdesign.md — design.md 형식 디자인 명세 갤러리 (이번 워크플로 핵심 포맷)
• getdesign.kr — 한국 서비스(토스·배민·클래스101 등 14개) 디자인 시스템을 design.md 포맷으로 정리한 갤러리 (getdesign.md 한국판)
// 06_frontend_design_ko_md/baemin_design.md 발췌 name: 배달의민족 category: delivery lang: ko // "한국 길거리 간판 미학을 디지털로 가져온다"는 한 줄 정체성 // 2024년 Baemin 2.0 리브랜드 — 프라이머리 민트를 더 밝고 채도 높은 톤으로 교체
// 07_haiku/line_design.md 발췌 — LINE Design System(LDSG) 컬러 체계 ldsg-color-linegreen: oklch(0.73 0.197 152) # #06C755 · 공식 공개값 ldsg-color-black: oklch(0 0 0) # #000000 · 공식 공개값 ldsg-color-disabled-gray: oklch(0.92 0 0) # #E4E4E4 · 공식 공개값 // 호버 70% / 프레스 50% — 색 대신 투명도로만 상태 변화
#06C755 그린 하나와 절제된 회색조로 "은행 창구처럼 조용한" 인상을 그대로 재현했다. LINE 문서는 공식 공개 색상이 딱 3개(그린·블랙·비활성 회색)뿐이라 나머지 11단 그레이는 스와치 이미지에서 직접 값을 추정해 채워야 했는데, 디자인 시스템도 결국 "출처가 확실한 값"과 "추정한 값"을 구분해서 관리해야 한다는 걸 이번에 알았다.--color-primary: #06C755 등)을 그대로 반영한 결과가 나왔다. "더 똑똑한 모델이 항상 필요한 건 아니고, 명세가 명확하면 가벼운 모델로도 충분하다"는 감각을 처음 잡아본 실습이었다.3. Anaconda — 다음 단계를 위한 환경 설정
오늘 마지막으로 Anaconda를 설치했다. 프로젝트마다 필요한 Python 라이브러리·버전이 다른데, Anaconda는 프로젝트별로 독립된 "방(가상환경)"을 만들어줘서 방끼리 서로 간섭하지 않게 해주는 관리자라는 개념만 먼저 잡았다. 다음 시간의 Python·데이터 파트를 위한 준비 단계.
# 설치 확인 conda --version conda info python --version # 다음 시간에 쓸 명령 — 프로젝트별 가상환경 conda create -n myproject python=3.11 conda activate myproject
4. 배운 것 정리
| 배운 것 | 핵심 |
|---|---|
| 서버리스 함수 | 상시 서버 대신 요청 올 때만 실행되는 api/*.js 파일 하나 |
vercel.json(builds·routes) | 함수·정적파일 구분과 주소별 라우팅을 자동감지에 맡기지 말고 직접 명시 |
| Vercel 환경변수 | .env는 로컬 전용, 배포엔 vercel env add로 별도 등록 + 재배포 |
| Plan Mode | 코딩 전 설계 → 결과물 완성도가 크게 달라짐 |
| 질문 유도 | 계획 단계에서 되묻고 답하는 과정 자체가 기획 |
| 디자인 토큰 | 색·치수를 변수 하나로 몰아 전체를 한 곳에서 제어 |
| design.md | 브랜드 디자인을 문서로 명세하면 그대로 재현 가능 |
🔁 회고
/api/chat 버그를 오늘 구조를 갈아엎으면서 자연스럽게 해결한 것도 좋았지만, 오늘 진짜 수확은 vercel.json에 builds·routes를 직접 명시해서 배포 후 서버(함수)가 응답 안 하던 문제를 고친 것이다. "배포는 명령어만 치면 알아서 되는 것"이라 생각했는데, 서버리스 함수와 정적 파일이 섞인 구조에선 자동 감지가 실패할 수 있고, 그럴 땐 무엇을 어떻게 처리할지 직접 적어줘야 한다는 걸 배웠다. 같은 계산기를 일곱 번 반복해서 만들어본 것도 좋은 선택이었다 — 앱이 같으니 "워크플로 차이"만 순수하게 비교할 수 있었고, 계획·질문·design.md가 각각 결과물에 어떻게 반영되는지 뚜렷하게 보였다.
✍️ 마치며
오늘 가장 크게 남은 건 "AI에게 무엇을 어떻게 부탁하느냐가 결과물을 결정한다"는 감각이었다. 같은 계산기인데 계획 없이 시키면 무난한 기본값이 나오고, 질문을 주고받으면 진짜 필요한 게 반영되고, design.md처럼 근거 문서를 주면 그 브랜드의 톤을 그대로 재현했다. 배포도 마찬가지였다 — 상시 서버에서 서버리스로 구조를 바꾸는 것 자체가 어제의 버그를 없애는 결과로 이어졌다. 다음엔 Anaconda 가상환경 실습부터 이어서, Python·데이터 파트로 넘어갈 예정이다.
🏷️ Vercel | 서버리스 | Claude Code | Plan Mode | design.md | Anaconda | 학습일지
'AI 기반 프론트엔드 웹개발자 양성과정' 카테고리의 다른 글
| [260720 15일차] Data, YOLO (0) | 2026.07.20 |
|---|---|
| [260716 14일차] Anaconda, Python (0) | 2026.07.16 |
| [260714 12일차] Node·Express, Groq API (0) | 2026.07.14 |
| [260713 11일차] Mini Project #3 (1) | 2026.07.13 |
| [260710 10일차] React (0) | 2026.07.10 |