요새 핫하다는 Antigravity 나도 한 번 찍먹 해보고자 간단한 포트폴리오 사이트를 만들기로 했다. 최근에 프로젝트하면서 어떤 팀원이 Antigravity 칭찬을 엄청 하셨었다. 우리 프젝 레포지토리를 기반으로(프론트가 전혀 없었던) 웹사이트 만든 걸 보여주셨는데, 결과물이 너무 충격적이었다. 이런 퀄리티가 가능하다고..?
그분이 말씀하시기를 "레포지토리 통째로 넘겨서 프롬프트 위주로 만들었다"라고 하셨는데 정말 그게 가능한가 싶어 직접 경험해보기로 했다.
디자인 잡도리
Behance나 Dribble 등의 사이트를 통해 내 마음에 드는 느낌의 포트폴리오 웹사이트 디자인을 찾아 이런 느낌과 비슷하게 해달라고 요청했다. 완전 마음에 쏙 들게는 아니지만 50% 정도는 "이 정도면 괜찮군"할 법한 결과물을 보여준다. 디자인 잡는 것에 생각보다 시간을 많이 썼다. (디테일 면에서 수정할 것들이 꽤나 많다.)
아래와 같이 디자인 기획안을 여러 개 달라고하면 이렇게 뽑아서 보여준다.


중간 결과
현재 여기까지 진행했는데 Task별로 작업 목록을 만들어주고, 말그대로 나는 '승인'만 해주면 되는 것이었다. 참고로 나는 React 등과 같은 프론트엔드 작업에 대해 아예 모르다보니 너무 충격적이었다.. (React 찍먹은 몇 번 해보았었지만)


Supabase로 DB 연동하기
포트폴리오 콘텐츠 관리와 추후 붙일 챗봇을 위해 Supabase를 활용해보기로 했다. 이런 외부 라이브러리도 Antigravity 내장 터미널을 통해 설치된다. 이를 통해 해당 프로젝트의 package.json 파일에 기록되고, 클라우드 환경의 node_modules 폴더에 설치되게 된다.
이렇게 설치된 내역은 package.json 파일의 dependencies 항목에 해당 라이브러리의 이름과 버전이 기록된다.
로컬 맥 vs 내장 터미널
- 로컬 맥 환경: 만약 로컬 환경의 폴더를 Antigravity로 열어서 작업 중이라면, Antigravity 터미널에서 설치했다는 것은 결국 내 맥의 특정 폴더에 파일을 저장했다는 뜻이다.
- Antigravity 환경: 만약 Antigravity가 클라우드 IDE 환경이라면 해당 클라우드 서버의 프로젝트 폴더에 저장된다. 내 맥 전체에 설치되는 것이 아니라 현재 진행 중인 그 프로젝트 폴더에만 종속된다. 따라서 다른 프로젝트를 만들면 거기엔 이 라이브러리가 없다.

SQL 에디터에 '프로젝트' 테이블을 생성해주고, 보여주고싶은 컬럼들을 추가했다.

Table Editor 탭에서는 'Insert Row'해서 데이터만 입력해주면 끝이다.


Vercel로 배포하기
이걸 또 굳이 글로 쓰는게 민망할 정도로 빠르게 배포해준다. Github Repository 기반으로 Import 해와서 배포해주면 끝이다.
환경 변수로 Supabase URL이랑 Anon Key추가해주는 것 잊지말기!

챗봇 도입해볼까?
내 포트폴리오 사이트 내용을 기반으로 궁금한 정보에 대해 응답을 해주면 재밋겠다 싶어 도입해보기로 했다.
챗봇 UI
클로드에게 프로젝트 css 코드를 던져주고 '챗봇 UI 기획안 여러개 만들어달라'고 하니 아래와 같이 생성해준다. 이제 이런거 보면 충격적이고 신기한 걸 떠나서 '이제 뭐 먹고 살아야하나' 싶다. 이건 안티그래비티에게 맡기지 않고, 클로드가 제공해준 jsx와 css코드를 가지고 구현하였다.

챗봇 연동하기
일단 .env에 Supabase 키들과 Claude API키 값을 아래와 같이 정의해준다. Vercel 환경변수에도 똑같이 동일하게 등록해준다.
# .env.local
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJxxxxxxxx
# 챗봇용 (VITE_ 없는 버전도 함께)
SUPABASE_URL=https://xxxx.supabase.co
SUPABASE_ANON_KEY=eyJxxxxxxxx
CLAUDE_API_KEY=sk-ant-xxxxxxxx

Supabase 환경변수가 2개씩이나 필요한 이유
위의 VITE_SUPABASE_URL이나 SUPABASE_URL이나 같은 값이다. 이렇게 2개로 나누어지는 이유는 브라우저 환경과 Vercel 서버 환경에 따라 달라지게 되기 때문이다.
VITE_SUPABASE_URL -> 브라우저(프론트엔드)가 읽음
SUPABASE_URL -> Vercel 서버(백엔드)가 읽음
브라우저 환경 - Vite_ 접두사 필요
Vite는 보안상 빌드 타임에 VITE_ 접두사가 붙은 변수만 빌드 결과물(브라우저 JS)에 포함시킨다. 접두사 없는 변수는 번들에서 제외돼서 브라우저가 읽을 수 없게 된다.
// src/ 안의 React 코드에서는 이렇게 읽음
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL, // 브라우저 번들에 포함됨
import.meta.env.VITE_SUPABASE_ANON_KEY
)
import.meta.env.SUPABASE_URL // undefined — 브라우저에 노출 안 됨
Vercel 서버 환경 - VITE_ 접두사 불필요 (오히려 못 읽음)
api/chat.js는 브라우저가 아니라 Vercel의 Node.js 서버에서 실행된다. Vite 빌드 과정을 거치지 않기 때문에 import.meta.env 자체가 없고, Node.js 방식인 process.env로 읽는다. 따라서 VITE_ 접두사를 붙이면 못읽어오게 된다.
// api/chat.js (Vercel Node.js 서버에서 실행)
process.env.VITE_SUPABASE_URL // undefined — Vite가 처리 안 함
process.env.SUPABASE_URL // <적합> Vercel 환경변수에서 직접 읽음
Chat.js
api라는 폴더에 Chat.js라는 파일을 하나 만들어두었다. 이 파일이 하는 역할은 '중간 서버' 역할인데, 브라우저(React)에서 직접 Claude API나 Supabase에 접근하면 API 키가 노출되기 때문에, Vercel이 이 파일을 서버리스 함수로 실행해서 키를 안전하게 숨겨준다.
해당 파일에 선언된 이름, 이메일, GitHub 등과 같은 정적인 고정 정보들과 Supabase에서 가져오는 실시간 데이터를 통해 프롬프트를 조립하여 Claude API를 호출하고 응답을 받게 된다. 이렇게 3개의 외부 서비스(React 프론트/Supabase/Claude API)를 하나의 파일로 중계하는 구조이다.

전체 코드는 아래와 같다.
// api/chat.js — Vercel Serverless Function + Supabase 연동
//
// 필요한 환경변수 (.env.local & Vercel Dashboard):
// CLAUDE_API_KEY = sk-ant-xxxxxxxx
// SUPABASE_URL = https://xxxx.supabase.co
// SUPABASE_ANON_KEY = eyJxxxxxxxx
//
// ⚠ 주의: Vercel Serverless에서는 VITE_ 접두사 변수를 읽을 수 없어요.
// 프론트엔드용 VITE_SUPABASE_URL 과는 별개로
// SUPABASE_URL / SUPABASE_ANON_KEY 를 Vercel 환경변수에 따로 추가해야 해요.
// ─────────────────────────────────────────────────────────
const OWNER_PROFILE = {
name: '(이름)',
role: '(무슨?) 개발자',
career: '신입',
location: '서울, 한국',
email: '이메일',
github: '깃헙',
blog: '블로그',
};
// ── Supabase fetch ────────────────────────────────────────
async function fetchProjects() {
// VITE_ 접두사 없는 변수만 사용 (Vercel Serverless는 VITE_ 못 읽음)
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
console.warn('[chat.js] Supabase env vars missing. SUPABASE_URL:', !!supabaseUrl, '/ SUPABASE_ANON_KEY:', !!supabaseAnonKey);
return [];
}
const url = `${supabaseUrl}/rest/v1/projects?select=*&order=created_at.desc`;
const res = await fetch(url, {
headers: {
apikey: supabaseAnonKey,
Authorization: `Bearer ${supabaseAnonKey}`,
},
});
if (!res.ok) {
const errText = await res.text();
console.error('[chat.js] Supabase fetch failed:', res.status, errText);
return [];
}
const data = await res.json();
console.log(`[chat.js] Supabase: fetched ${data.length} projects`);
return data;
}
// ── 프로젝트 → 프롬프트 텍스트 ──────────────────────────────
function formatProjects(projects) {
if (!projects.length) return '(등록된 프로젝트가 없습니다)';
return projects
.map((p) => {
const lines = [`### ${p.title} [${p.category}]`];
if (p.short_description) lines.push(`- 한 줄 요약: ${p.short_description}`);
if (p.full_description) lines.push(`- 상세 설명: ${p.full_description}`);
if (p.technologies?.length) lines.push(`- 사용 기술: ${p.technologies.join(', ')}`);
if (p.github_url) lines.push(`- GitHub: ${p.github_url}`);
if (p.link_url) lines.push(`- 배포/링크: ${p.link_url}`);
if (p.blog_url?.length) lines.push(`- 블로그 글: ${p.blog_url.join(', ')}`);
return lines.join('\n');
})
.join('\n\n');
}
// ── System Prompt 조립 ───────────────────────────────────
function buildSystemPrompt(projects) {
const p = OWNER_PROFILE;
return `
당신은 ${p.name}의 포트폴리오 AI 어시스턴트입니다.
포트폴리오를 방문한 사람들(채용담당자, 협업 제안자, 개발자 동료)의 질문에 답변합니다.
## 페르소나 & 톤
- 1인칭("저는", "제가")이나 3인칭("${p.name}님은") 대신 "이 개발자는", "포트폴리오에서는" 같은 자연스러운 표현을 사용합니다.
- 친절하고 전문적인 톤. 불필요한 서두("네, 좋은 질문이에요!")는 생략하고 바로 핵심을 답합니다.
- 기술 용어는 영어 원문 유지 (React, Swift, UIKit 등).
- 답변은 항상 한국어로.
- 마크다운은 **굵게**와 \`인라인 코드\`만 사용. 헤더(#)나 불릿(-) 리스트는 쓰지 않습니다.
## 답변 전략
- 프로젝트 관련 질문 → 해당 프로젝트를 구체적으로 언급하고 GitHub/링크 제공.
- 기술 스택 질문 → 단순 나열 대신 "어떤 프로젝트에서 왜 썼는지" 맥락 포함.
- 채용/협업 문의 → 연락처를 자연스럽게 안내.
- 긴 답변은 핵심 2~3줄 요약 후 "더 궁금한 점 있으면 알려주세요"로 마무리.
## 기본 정보
- 직군: ${p.role}
- 경력: ${p.career}
- 위치: ${p.location}
## 연락처
- 이메일: ${p.email}
- GitHub: ${p.github}
- Blog: ${p.blog}
## 프로젝트 목록
${formatProjects(projects)}
## 경계 규칙
- 포트폴리오·채용·개발과 무관한 질문(날씨, 주식 등)에는 "저는 이 포트폴리오에 대한 질문만 답변할 수 있어요! 다른 궁금한 점 있으신가요?"라고만 답변.
- 포트폴리오에 없는 내용은 절대 지어내지 않습니다. 없으면 "해당 내용은 포트폴리오에 없어요."라고 솔직하게 답변.
- 전화번호, 정확한 주소 등 민감한 개인정보는 공개하지 않습니다.
`.trim();
}
// ── Vercel Handler ────────────────────────────────────────
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { messages } = req.body;
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Invalid messages format' });
}
// Supabase fetch (실패해도 기본 정보로 fallback)
let projects = [];
try {
projects = await fetchProjects();
} catch (err) {
console.error('[chat.js] Supabase unexpected error:', err);
}
const systemPrompt = buildSystemPrompt(projects);
// Claude API 키 체크
if (!process.env.CLAUDE_API_KEY) {
console.error('[chat.js] CLAUDE_API_KEY is missing');
return res.status(500).json({ error: 'Server misconfiguration' });
}
try {
const claudeRes = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.CLAUDE_API_KEY,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-haiku-4-5-20251001',
max_tokens: 1000,
system: systemPrompt,
messages,
}),
});
// Claude가 에러 반환한 경우 → 상세 내용을 로그에 남기고 프론트에 전달
if (!claudeRes.ok) {
const errBody = await claudeRes.json().catch(() => ({}));
console.error('[chat.js] Claude API error:', claudeRes.status, errBody);
return res.status(claudeRes.status).json({
error: errBody?.error?.message ?? 'Claude API error',
type: errBody?.error?.type ?? 'unknown',
});
}
const data = await claudeRes.json();
return res.status(200).json(data);
} catch (error) {
console.error('[chat.js] Fetch to Claude failed:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}

'Project' 카테고리의 다른 글
| [RUSH_CREW] 트러블슈팅 - AWS ECS 서비스 HEALTH CHECK 실패 해결기 (0) | 2025.12.30 |
|---|---|
| [RUSH_CREW] 12. ECR & ECS로 무중단배포하기 (3) (0) | 2025.12.22 |
| [SURFING THE GANGWON] 기상청 API 호출 성능 최적화 (0) | 2025.10.01 |