Next.js 없이도 됩니다. Vite + Cloudflare Pages 조합으로 더 빠르고 더 저렴하게 SaaS를 배포하는 법, 직접 겪으면서 정리했습니다.
[핵심 요약]
→ 스택: Vite + React (TypeScript) + Supabase + Cloudflare Pages
→ 장점: Next.js보다 빠른 빌드, Cloudflare 글로벌 CDN 무료
→ 함정 1: Vite 환경변수는 VITE_ 접두사 필수 (CRA와 다름)
→ 함정 2: SPA 라우팅 직접 접속 시 404 → _redirects 파일로 해결
→ 함정 3: Supabase 무료 티어 7일 미접속 시 일시정지 → GitHub Actions로 해결
→ 추가: 구글 서치 콘솔 사이트명 오인식 문제 → JSON-LD 구조화 데이터로 해결
→ 비용: 이 스택 전부 무료 티어로 운영 가능
왜 이 스택인가
[Next.js vs Vite + Cloudflare Pages]
Next.js + Vercel:
→ SSR/SSG 필요한 복잡한 앱에 적합
→ Vercel 유료 플랜 빠르게 도달
→ 빌드 시간 길어짐 (앱 커질수록)
→ 서버 사이드 로직이 필요할 때
→ 구글 애드센스 불가
Vite + Cloudflare Pages:
→ SPA 구조 앱에 최적
→ 빌드 속도 압도적으로 빠름
→ Cloudflare CDN 전 세계 무료 배포
→ Supabase로 백엔드 대체 → 서버 불필요
→ 사이드 프로젝트, SaaS MVP에 적합
→ 구글 애드센스 연동 가능
결론:
→ 백엔드 없이 Supabase + Cloudflare Pages면
→ 월 $0으로 글로벌 서비스 운영 가능
실전 1 — Vite 환경변수 설정 (첫 번째 함정)
# 프로젝트 생성
npm create vite@latest my-saas-app -- --template react-ts
cd my-saas-app
npm install
npm install @supabase/supabase-js
# .env.local 생성
# ❌ CRA 방식 (Vite에서 작동 안 함)
REACT_APP_SUPABASE_URL=https://xxx.supabase.co
REACT_APP_SUPABASE_KEY=eyJ...
# ✅ Vite 방식 (VITE_ 접두사 필수)
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=eyJ...
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
// ❌ CRA 방식 (Vite에서 undefined 반환)
// process.env.REACT_APP_SUPABASE_URL
// ✅ Vite 방식
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
if (!supabaseUrl || !supabaseKey) {
throw new Error('Supabase 환경변수 누락 — .env.local 확인')
}
export const supabase = createClient(supabaseUrl, supabaseKey)
[Vite 환경변수 규칙 요약]
→ VITE_ 접두사: 클라이언트 사이드에 노출 (브라우저에서 접근 가능)
→ 접두사 없음: 서버 사이드 전용 (빌드 스크립트에서만)
→ 접근 방법: import.meta.env.VITE_변수명
→ .env.local: 로컬 개발 전용 (git에 절대 커밋 금지)
→ 타입 지원: vite-env.d.ts에 타입 정의 추가 권장
// vite-env.d.ts (타입 자동완성 지원)
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SUPABASE_URL: string
readonly VITE_SUPABASE_PUBLISHABLE_KEY: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
실전 2 — Supabase 테이블 설정 (RLS + Grant)
-- 예시: posts 테이블 생성
-- 반드시 3단계 세트로 (Supabase 4월 28일 변경사항 반영)
-- Step 1: 테이블 생성
CREATE TABLE posts (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
title text NOT NULL,
content text,
created_at timestamptz DEFAULT now()
);
-- Step 2: Grant 추가 (4월 28일 이후 신규 필수)
GRANT SELECT, INSERT, UPDATE, DELETE ON posts TO authenticated;
-- 비로그인 읽기 허용 시:
-- GRANT SELECT ON posts TO anon;
-- Step 3: RLS 활성화 + 정책
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "본인 글만 조회"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "본인만 작성"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "본인만 수정"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "본인만 삭제"
ON posts FOR DELETE
USING (auth.uid() = user_id);
// Supabase 클라이언트 사용 예시
import { supabase } from '@/lib/supabase'
// 데이터 조회
async function getPosts() {
const { data, error } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
if (error) {
// 403 에러 = Grant 없음 (4월 28일 이후 흔한 에러)
if (error.code === '42501') {
console.error('Grant 없음 — Supabase 대시보드에서 권한 확인')
}
throw error
}
return data
}
// 데이터 삽입
async function createPost(title: string, content: string) {
const { data, error } = await supabase
.from('posts')
.insert({ title, content })
.select()
.single()
if (error) throw error
return data
}
실전 3 — Cloudflare Pages 배포
# Cloudflare Pages 배포 전 빌드 테스트
npm run build
# → dist/ 폴더 생성 확인
[Cloudflare Pages 대시보드 설정]
Workers & Pages → Create → Pages → Connect to Git
Build Settings:
→ Framework preset: Vite
→ Build command: npm run build
→ Build output directory: dist
Environment Variables (Production + Preview 모두):
→ VITE_SUPABASE_URL: https://xxx.supabase.co
→ VITE_SUPABASE_PUBLISHABLE_KEY: eyJ...
[SPA 라우팅 404 문제 — 두 번째 함정]
증상:
→ 앱 내 네비게이션: 정상 작동
→ /dashboard 직접 접속: 404 Not Found
→ /room/123 새로고침: 404 Not Found
원인:
→ Cloudflare Pages가 /dashboard 경로의 정적 파일을 찾음
→ 해당 파일 없음 → 404
해결:
# public/_redirects 파일 생성 (이 파일 하나로 해결)
echo "/* /index.html 200" > public/_redirects
파일 내용:
/* /index.html 200
→ 모든 경로 요청을 index.html로 → React Router가 처리
→ public/ 폴더의 파일은 빌드 시 dist/로 자동 복사
→ Cloudflare Pages가 이 규칙을 읽어 SPA 라우팅 정상 처리
실전 4 — Supabase 무료 티어 일시정지 방지 (세 번째 함정)
[Supabase 무료 티어 일시정지 조건]
→ 7일간 활성 API 호출/대시보드 접속이 없으면 자동 일시정지
→ 일시정지 후 재시작: 수 분 ~ 수십 분 소요
→ 사용자 입장: "서비스가 갑자기 안 된다"
해결: GitHub Actions cron으로 3일마다 자동 핑
# GitHub 레포지토리 → Settings → Secrets → Actions
# 새 시크릿 2개 추가:
# SUPABASE_URL: https://xxx.supabase.co
# SUPABASE_PUBLISHABLE_KEY: eyJ...
# .github/workflows/keep_alive.yml
name: Keep Supabase Alive
on:
schedule:
- cron: '0 0 */3 * *' # 3일마다 자정 실행
workflow_dispatch: # 수동 실행 버튼
jobs:
ping:
runs-on: ubuntu-latest
steps:
- name: Ping Supabase API
run: |
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
"${{ secrets.SUPABASE_URL }}/rest/v1/posts?select=id&limit=1" \
-H "apikey: ${{ secrets.SUPABASE_PUBLISHABLE_KEY }}" \
-H "Authorization: Bearer ${{ secrets.SUPABASE_PUBLISHABLE_KEY }}")
echo "응답 코드: $RESPONSE"
if [ "$RESPONSE" != "200" ] && [ "$RESPONSE" != "206" ]; then
echo "❌ Supabase 핑 실패: $RESPONSE"
exit 1
fi
echo "✅ Supabase 정상 응답"
[테스트 방법]
1. GitHub → Actions 탭
2. "Keep Supabase Alive" 선택
3. "Run workflow" 클릭
4. 초록 체크마크 + "✅ Supabase 정상 응답" 확인
[주의사항]
→ curl 대상 테이블(posts)이 실제 존재해야 함
→ anon 역할에 SELECT Grant 있어야 200 반환
→ 없으면 403 → 다른 조회 가능한 테이블로 변경
실전 5 — 구글 서치 콘솔 + SEO (네 번째 함정)
[Cloudflare Pages 서브도메인 사이트명 오인식 문제]
증상:
→ 구글 검색 결과에 사이트명이 "Cloudflare" 또는 "Pages"로 표시
→ 실제 서비스 이름이 안 나옴
원인:
→ your-app.pages.dev 사용 시 구글이 호스팅사 이름을 우선 인식
→ 공유 서브도메인 = 소유 도메인으로 인식 안 함
해결: JSON-LD 구조화 데이터 + OG 태그
{
"@context": "<a href=https://schema.org>https://schema.org</a>",
"@type": "WebSite",
"name": "실제 서비스 이름",
"url": "<a href=https://your-app.pages.dev/>https://your-app.pages.dev/</a>"
}
[구글 서치 콘솔 설정 순서]
1. search.google.com/search-console 접속
2. 속성 추가 → URL 접두사 방식
3. HTML 태그 방식으로 인증
→ <meta name="google-site-verification" content="코드"> 삽입 후 배포
4. 확인(Verify) 클릭
[색인 빠르게 반영하는 법]
1. 서치 콘솔 상단 URL 검사창에 도메인 입력
2. 색인 생성 요청 클릭
3. 보통 24시간 이내 사이트명 정상 반영
실전 6 — 전체 배포 체크리스트
# 로컬 개발 → 배포 전 체크리스트
# 1. 환경변수 확인
cat .env.local
# → VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY 있는지
# 2. 빌드 테스트
npm run build
# → 에러 없이 dist/ 생성되는지
# 3. SPA 라우팅 파일 확인
cat public/_redirects
# → /* /index.html 200 한 줄 있는지
# 4. .gitignore 확인
cat .gitignore | grep env
# → .env.local 포함 여부
# 5. Supabase 테이블 Grant 확인
# → SQL Editor에서 Grant 쿼리 실행
# 6. GitHub Actions 파일 확인
ls .github/workflows/
# → keep_alive.yml 있는지
[Cloudflare Pages 배포 후 체크리스트]
□ 환경변수 Production + Preview 모두 설정됨
□ 빌드 성공 (Cloudflare Dashboard → 배포 로그 확인)
□ /some-route 직접 접속 시 404 안 나옴 (_redirects 작동)
□ Supabase 데이터 정상 조회됨
□ GitHub Actions Keep Alive 테스트 통과
□ 구글 서치 콘솔 소유권 인증 완료
□ 색인 생성 요청 완료
전체 스택 구성 요약
[무료 티어로 운영 가능한 SaaS 스택]
프론트엔드:
→ Vite + React + TypeScript
→ Cloudflare Pages (무료: 빌드 500회/월, 대역폭 무제한)
백엔드:
→ Supabase 무료 티어
- Postgres DB 500MB
- Auth 50,000 MAU
- Storage 1GB
- Edge Functions 500,000회/월
자동화:
→ GitHub Actions (무료: 공개 레포 무제한, 비공개 2,000분/월)
SEO:
→ Google Search Console (무료)
→ 월 $0으로 전 세계 서비스 운영 가능
→ 유료 전환 시: Supabase Pro $25/월, Cloudflare Pages Pro $20/월
마무리
✅ 이 스택 쓰면 좋은 경우
→ 사이드 프로젝트, SaaS MVP 빠르게 출시
→ Next.js 없이 가벼운 SPA 앱
→ 서버 없이 Supabase로 백엔드 대체
→ 월 $0 운영이 중요한 초기 단계
→ 글로벌 CDN이 필요한 경우
❌ 다른 스택이 나은 경우
→ SEO가 핵심인 서비스 → Next.js SSR
→ 서버사이드 로직이 복잡한 경우 → Next.js API Routes
→ 대규모 트래픽 → Supabase Pro 이상
[오늘 당장 시작하는 법]
1. npm create vite@latest my-app -- --template react-ts
2. npm install @supabase/supabase-js
3. .env.local에 VITE_ 접두사로 환경변수 추가
4. public/_redirects 파일 생성
5. Cloudflare Pages에 연결 → 배포
6. GitHub Actions keep_alive.yml 추가
→ 30분이면 전부 완료
관련 글:
https://cell-devlog.tistory.com/166
Supabase + Claude Code MCP 완전 가이드 — AI 에이전트가 Postgres를 올바르게 다루게 만드는 법
AI 에이전트가 Supabase를 알고는 있습니다. 그런데 제대로 쓰는 건 다른 얘기입니다. RLS 없이 테이블 만들고, 없는 CLI 명령 날조하고, security_invoker 빠뜨린 뷰를 만듭니다. MCP + Agent Skills로 이걸 고
cell-devlog.tistory.com
https://cell-devlog.tistory.com/167
Supabase 보안 대변화 완전 가이드 — 4월 28일부터 테이블 자동 노출 비활성화
Lovable, Claude Code, Cursor로 만든 테이블이 5월 30일부터 Data API에서 자동으로 사라집니다. 미리 대응하지 않으면 프로덕션이 조용히 터집니다.[핵심 요약]→ 변경: public 스키마 테이블의 Data API/GraphQL
cell-devlog.tistory.com
https://cell-devlog.tistory.com/40
AI 네이티브 앱 아키텍처 설계 — 처음부터 AI를 고려한 풀스택 구조 (with Supabase)
"AI 기능 추가해야 해"라는 말을 들으면 많은 개발자가 기존 앱에 LLM API 호출을 끼워 넣어요.# 이렇게 하면 안 돼요@app.post("/chat")def chat(message: str): response = openai.chat.completions.create(...) # 그냥 때려
cell-devlog.tistory.com