Vercel
프레임워크 통합 — Next.js를 만든 회사다. App Router, Server Components, 스트리밍이 최적화되어 있다.
글로벌 CDN — 정적 자산과 캐시된 페이지를 사용자 가까이에서 서빙.
프리뷰 배포 — PR마다 고유 URL. 코드 리뷰의 절반이 여기서 끝난다.
이미지 최적화 · 미들웨어 · 관측성 · 빌드 파이프라인.
Vercel은 지워도 데이터가 안 사라진다. Supabase를 지우면 사라진다
두 회사 모두 “백엔드도 됩니다”라고 말하기 때문에 생기는 혼란이다. Vercel에는 Functions, 파트너 Postgres/Redis, Blob, Cron이 있고, Supabase에는 Edge Functions, Cron, Queues, Storage, Vector가 있다.
하지만 본질은 명확하게 다르다.
Vercel은 코드를 실행하는 회사이고, Supabase는 상태를 보관하는 회사다.
| Vercel = 실행 계층 | Supabase = 상태 계층 |
|---|---|
| 프론트엔드 빌드와 배포 | 데이터 (Postgres) |
| 렌더링 (SSR/SSG/ISR/스트리밍) | 사용자 신원 (Auth) |
| CDN과 엣지 캐싱 | 파일 (Storage) |
| 요청 단위로 짧게 사는 서버 코드 | 실시간 연결 (Realtime) |
| 프리뷰 배포와 팀 협업 | 권한 규칙 (RLS) |
Vercel
프레임워크 통합 — Next.js를 만든 회사다. App Router, Server Components, 스트리밍이 최적화되어 있다.
글로벌 CDN — 정적 자산과 캐시된 페이지를 사용자 가까이에서 서빙.
프리뷰 배포 — PR마다 고유 URL. 코드 리뷰의 절반이 여기서 끝난다.
이미지 최적화 · 미들웨어 · 관측성 · 빌드 파이프라인.
Supabase
영속 데이터 — 관계형, 트랜잭션, 제약, 인덱스가 있는 진짜 데이터베이스.
행 단위 권한(RLS) — 접근 경로가 늘어나도 규칙은 한 곳에만 존재한다.
완결된 인증 — 소셜, MFA, SSO, 세션 관리까지 제품으로 제공.
상태 있는 연결(Realtime) — WebSocket은 서버리스 함수가 잘 못 하는 영역이다.
DB 내장 기능 — cron, 큐, 벡터 검색, 외부 데이터 연동.
WebSocket과 영속 연결은 서버리스와 근본적으로 궁합이 나쁘다. Realtime을 Supabase가 맡는 건 취향이 아니라 구조적 필연이다.
| 기능 | Vercel | Supabase | 판단 |
|---|---|---|---|
| 서버 함수 | Route Handler / Server Action | Edge Functions | 상황에 따라 (아래에서 상세히) |
| 스케줄 작업 | Vercel Cron | pg_cron | DB 작업이면 Supabase |
| 큐 | Vercel Queues | pgmq | DB 트랜잭션과 묶이면 Supabase |
| 파일 저장 | Blob | Storage | 권한이 필요하면 Supabase |
| KV 캐시 | 파트너 Redis | (Postgres/외부) | 세션 캐시는 Vercel 쪽 |
| 이미지 최적화 | next/image |
Storage 변환 | 한 쪽만 쓴다 (중복 과금) |
| 인증 | (직접 구현) | Auth | Supabase |
| DB | 파트너 Postgres | Postgres | Supabase (통합 이점) |
flowchart TB
U["사용자 브라우저"]
subgraph V["Vercel"]
direction TB
CDN["Edge Network / CDN"]
MW["Middleware<br/>세션 갱신 · 라우트 보호"]
RSC["Server Components<br/>서버에서 데이터 조회"]
RH["Route Handlers<br/>Server Actions"]
CC["Client Components"]
end
subgraph S["Supabase"]
direction TB
AU["Auth"]
PG[("Postgres + RLS")]
RT["Realtime"]
ST["Storage"]
EF["Edge Functions"]
end
EXT["외부 서비스<br/>Stripe · LLM · 메일"]
U --> CDN --> MW
MW --> RSC --> PG
MW --> AU
RH --> PG
RH --> EXT
CC -.->|"WebSocket"| RT
CC -.->|"직접 업로드"| ST
EXT -.->|"웹훅"| EF --> PG
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class PG key
class U,CDN,MW,RSC,RH,CC,AU,RT,ST,EF,EXT mute
| 흐름 | 경로 | 장점 |
|---|---|---|
| 1. 서버 렌더링 조회 | 브라우저 → Vercel(RSC) → Supabase REST → Postgres(RLS) | 초기 로딩이 빠르고 SEO에 유리. 쿠키의 JWT가 그대로 전달된다 |
| 2. 클라이언트 직접 조회 | 브라우저 → Supabase REST → Postgres(RLS) | Vercel을 아예 거치지 않는다. 함수 실행 비용이 0 |
| 3. 서버 액션 / Route Handler | 브라우저 → Vercel 함수 → (Supabase + 외부 API) | 시크릿을 쓸 수 있고 여러 작업을 조율할 수 있다 |
세 가지를 다 쓰는 게 정상이다. 하나로 통일하려 하지 말 것. 페이지 첫 화면은 1번, 무한 스크롤은 2번, 결제는 3번 — 이런 식으로 섞인다.
flowchart LR
Q1{"DB 이벤트가<br/>트리거인가?"}
Q1 -->|"예"| SB1["Supabase<br/>Edge Function"]
Q1 -->|"아니오"| Q2{"프론트엔드 배포와<br/>수명주기를 같이 해야 하나?"}
Q2 -->|"예"| VC1["Vercel<br/>Route Handler"]
Q2 -->|"아니오"| Q3{"여러 클라이언트가<br/>공유하는 로직인가?"}
Q3 -->|"예"| SB2["Supabase<br/>Edge Function"]
Q3 -->|"아니오"| Q4{"Node 전용 패키지가<br/>필요한가?"}
Q4 -->|"예"| VC2["Vercel"]
Q4 -->|"아니오"| VC3["Vercel — 기본값"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SB1,SB2 ok
class VC1,VC2,VC3 warn
class Q1,Q2,Q3,Q4 mute
기본값은 Vercel이다. 이미 그 레포에서 개발하고 배포하고 있기 때문이다. Supabase Edge Function은 “이유가 있을 때” 선택한다.
| Vercel Route Handler | Supabase Edge Function | |
|---|---|---|
| 런타임 | Node.js 또는 Edge | Deno |
| 배포 단위 | 프론트엔드와 함께 | 독립 |
| 코드 위치 | 같은 레포, 같은 타입 | supabase/functions/ |
| 타입 공유 | ○ (같은 프로젝트) | 별도 관리 필요 |
| DB 근접성 | 리전 설정에 따름 | DB와 가까움 |
| 프리뷰 배포 | PR마다 자동 | 브랜치 연동 필요 |
| DB 웹훅 대상 | 가능하지만 우회적 | 자연스러움 |
| Node 패키지 | 전부 사용 가능 | 제한적 |
| 시크릿 관리 | Vercel 환경 변수 | supabase secrets |
| 하는 일 | 배치 | 이유 |
|---|---|---|
| 상품 목록 페이지 렌더링 | Vercel RSC | 렌더링과 조회가 붙어 있다 |
| 무한 스크롤 추가 로딩 | 브라우저 → Supabase 직접 | Vercel 함수 비용 절약 |
| 결제 세션 생성 | Vercel Route Handler | Stripe 시크릿 + 프론트 흐름과 결합 |
| Stripe 웹훅 수신 | Supabase Edge Function | 프론트 배포와 무관하게 살아야 |
| 회원가입 시 프로필 생성 | Postgres 트리거 | DB 안에서 원자적으로 |
| 가입 환영 메일 | Supabase Edge Function | DB 이벤트가 트리거 |
| 채팅 메시지 수신 | Supabase Realtime | WebSocket은 서버리스가 못 한다 |
| 이미지 업로드 | 브라우저 → Storage 직접 | Vercel 함수 본문 크기 제한 회피 |
| 야간 집계 배치 | pg_cron | DB 안에서 완결 |
| OG 이미지 생성 | Vercel | 프레임워크 기능 |
supabase.from('posts').select()적합한 곳: 사용자가 자기 데이터를 다루는 모든 경로.
db.select().from(posts)6543) 필수적합한 곳: 관리자 도구, 배치, 복잡한 리포트.
flowchart LR
U["서울 사용자"] -->|"10ms"| V1["Vercel 함수<br/>서울 리전"]
V1 -->|"5ms"| S1["Supabase<br/>서울 리전"]
U2["서울 사용자"] -->|"10ms"| V2["Vercel 함수<br/>워싱턴 리전"]
V2 -->|"180ms × N회"| S2["Supabase<br/>서울 리전"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class S1 ok
class V2 bad
class U,U2,V1,S2 mute
함수와 DB가 멀면 왕복마다 대가를 치른다. 한 페이지에 쿼리 5개면 5배로 곱해진다.
{ "functions": { "app/api/**/*.ts": { "maxDuration": 30 } }, "regions": ["icn1"] }Edge 런타임은 사용자에게는 가깝지만 DB에서는 멀 수 있다. DB를 많이 읽는 라우트는 Node 런타임 + DB 리전이 낫다.
| 담당 | 무엇을 |
|---|---|
| Vercel | 정적 자산과 빌드 결과물(CDN), ISR/revalidate 페이지 캐시, Data Cache와 태그 무효화 |
| Supabase | Storage 파일의 CDN 캐시(cacheControl), 이미지 변환 결과 캐시 |
# Vercel — 클라이언트에도 노출 (브라우저 번들 포함)NEXT_PUBLIC_SUPABASE_URL=https://<ref>.supabase.coNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
# Vercel — 서버 전용SUPABASE_SECRET_KEY=sb_secret_xxxSTRIPE_SECRET_KEY=sk_live_xxxDATABASE_URL=postgres://...pooler.supabase.com:6543/postgres # 직접 연결용
# Supabase — Edge Function 전용supabase secrets set RESEND_API_KEY=re_xxxNEXT_PUBLIC_ 접두사 = 공개라고 외운다. 여기 secret이 들어가면 즉시 사고다.
Vercel과 Supabase를 잇는 방법은 두 가지다 — Vercel Marketplace 네이티브 통합(리소스를 Vercel 안에서 관리, 통합 청구)과 수동 연결(Supabase에서 프로젝트를 만들고 키를 환경 변수에 붙여넣기). 팀이 이미 Supabase 대시보드를 쓰고 있다면 후자가 자연스럽다.
flowchart LR
PR["GitHub PR 생성"] --> V["Vercel<br/>프리뷰 배포"]
PR --> SB["Supabase<br/>Preview Branch<br/>독립 DB + 키"]
SB -->|"환경 변수 주입"| V
MERGE["PR 머지"] --> PROD["프로덕션 배포<br/>+ 마이그레이션 적용"]
MERGE --> DEL["프리뷰 브랜치 정리"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SB ok
class V warn
class PR,MERGE,PROD,DEL mute
프리뷰 배포에서 소셜 로그인이 깨지는 대표적 원인이다.
Vercel 프리뷰 URL은 배포마다 바뀌는데(myapp-git-feat-x-team.vercel.app),
Supabase의 Redirect URL 허용 목록에 없으면 로그인 후 리다이렉트가 거부된다.
# 해결 1: 와일드카드 등록https://*-myteam.vercel.app/**https://myapp-*.vercel.app/**// 해결 2: 콜백에서 원래 주소로 되돌리기const origin = request.headers.get('origin')const forwardedHost = request.headers.get('x-forwarded-host') // Vercel이 붙여준다const redirectBase = process.env.NODE_ENV === 'development' ? origin : `https://${forwardedHost}`해결 3: 프리뷰에도 고정 도메인을 할당한다 (브랜치별 별칭 도메인).
| Vercel의 비용 동인 | Supabase의 비용 동인 |
|---|---|
| 함수 실행 시간과 호출 수 | 컴퓨트 크기 (상시 과금) |
| 대역폭 (전송량) | 대역폭 (egress) |
| 이미지 최적화 횟수 | 스토리지 용량 |
| 빌드 시간 | MAU, 브랜치 시간 |
돈이 되는 판단들 —
flowchart TB
subgraph BAD["나쁜 업로드 경로"]
direction LR
B1["브라우저"] -->|"파일 전송"| B2["Vercel 함수"] -->|"파일 전송"| B3["Storage"]
end
subgraph GOOD["좋은 업로드 경로"]
direction LR
G1["브라우저"] -->|"권한 요청"| G2["Vercel 함수"]
G2 -->|"서명 URL"| G1
G1 -->|"파일 직접 전송"| G3["Storage"]
G1 -->|"완료 알림"| G2
G2 --> G4[("DB 기록")]
end
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class B2 bad
class G3 ok
class B1,B3,G1,G2,G4 mute
나쁜 경로의 문제는 요청 본문 크기 제한, 함수 실행 시간 제한, 양쪽 대역폭 이중 과금이다. 좋은 경로에서 서버는 권한 판단과 기록만 하고 바이트는 지나가지 않는다.
Supabase는 Vercel 전용이 아니다.
상시 실행 서버를 쓴다면 서버리스 특유의 제약(커넥션 폭발, 콜드 스타트, WebSocket 불가)이 사라진다. 그 경우 Supabase를 “그냥 관리형 Postgres”로만 써도 충분히 합리적이다.
next/image와 Storage 변환을 동시에 — 양쪽에 과금5432로 두고 서버리스 배포 — 커넥션 고갈