master-project
CHAPTER 27 / 50
읽기 약 2분
FUNCTION
CRUD API: Supabase + Server Actions
핵심 개념
Server Action 우선·필요 시 Route Handler·zod 검증·revalidate·rate limit — Next.js 16 모던 CRUD.
본문
Server Action vs Route Handler 선택
[Server Action 우선]
- Form submit (POST/PATCH/DELETE)
- 같은 도메인 클라이언트만 호출
- 자동 CSRF 보호
[Route Handler 사용]
- 외부 통합 (Webhook, Mobile App)
- GET (URL 직접 접근)
- 파일 업로드 multipartServer Action — 생성
// src/app/(app)/generate/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { rateLimit } from '@/lib/rate-limit'
import { generateAI } from '@/lib/ai'
const schema = z.object({
prompt: z.string().min(1).max(500),
template_id: z.string().uuid().optional(),
})
export async function createGeneration(prevState: any, formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: '로그인 필요' }
// Rate limit (분당 10회)
const rl = await rateLimit(`gen:${user.id}`, 10, 60)
if (!rl.success) return { error: '분당 10회 초과', retry_after: rl.reset }
// 사용량 체크 (무료 5/월)
const { count } = await supabase
.from('generations')
.select('*', { count: 'exact', head: true })
.eq('user_id', user.id)
.gte('created_at', new Date(Date.now() - 30 * 86400e3).toISOString())
const { data: sub } = await supabase
.from('subscriptions')
.select('plan')
.eq('user_id', user.id)
.single()
if ((sub?.plan ?? 'free') === 'free' && (count ?? 0) >= 5) {
return { error: '무료 5회 한도 초과', upgrade: '/pricing' }
}
// 검증
const parsed = schema.safeParse({
prompt: formData.get('prompt'),
template_id: formData.get('template_id') || undefined,
})
if (!parsed.success) return { error: '입력값 오류', details: parsed.error.errors }
// AI 호출
const result = await generateAI(parsed.data.prompt)
// DB 저장
const { data, error } = await supabase
.from('generations')
.insert({
user_id: user.id,
prompt: parsed.data.prompt,
result: result.text,
tokens_used: result.tokens,
})
.select()
.single()
if (error) return { error: error.message }
revalidatePath('/history')
redirect(`/history/${data.id}`)
}Server Action — 수정·삭제
'use server'
export async function updateGeneration(id: string, prompt: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: '로그인 필요' }
const { error } = await supabase
.from('generations')
.update({ prompt })
.eq('id', id)
.eq('user_id', user.id) // 본인만
if (error) return { error: error.message }
revalidatePath('/history')
return { success: true }
}
export async function deleteGeneration(id: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
await supabase
.from('generations')
.delete()
.eq('id', id)
.eq('user_id', user.id)
revalidatePath('/history')
}Route Handler — 외부 API용
// src/app/api/generations/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const cursor = searchParams.get('cursor')
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'unauthorized' }, { status: 401 })
let q = supabase.from('generations').select('*').order('created_at', { ascending: false }).limit(20)
if (cursor) q = q.lt('created_at', cursor)
const { data } = await q
const nextCursor = data?.[data.length - 1]?.created_at ?? null
return NextResponse.json({
data,
meta: { cursor: nextCursor, has_more: data?.length === 20 },
})
}Rate Limit (Upstash Redis)
pnpm add @upstash/redis @upstash/ratelimit// src/lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
export async function rateLimit(key: string, limit: number, windowSec: number) {
const rl = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(limit, `${windowSec} s`),
})
return await rl.limit(key)
}다음 챕터
CH.28 "인증 API: JWT + 소셜 로그인 + 미들웨어".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 마스터 프로젝트의 CRUD API 부분을 분석해서 실전 적용 + 개선 우선순위 3가지를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
CRUD API 관련 모범 사례·안티패턴 5개를 비교 분석해서 실전 적용를 위한 추천 방안을 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 CRUD API 최적화 가능 위치와 리스크를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 1인 개발자 시장의 CRUD API 트렌드와 차별화 포인트를 정리해줘.
⭐ 이것만 기억하세요
CRUD API: Supabase + Server Actions는 이 3가지만 확실히 잡으세요
1.Server Action 우선 + Route Handler는 외부용
2.zod 검증 + Rate Limit + 소유자 체크 = 안전 CRUD
3.다음 챕터에서 인증 미들웨어
공유하기
진행도 27 / 50