OPEN HYPER STEP
← 목록으로 (master-project)
MASTER-PROJECT · 27 / 50
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 선택

📋 코드 (9줄)
[Server Action 우선]
- Form submit (POST/PATCH/DELETE)
- 같은 도메인 클라이언트만 호출
- 자동 CSRF 보호

[Route Handler 사용]
- 외부 통합 (Webhook, Mobile App)
- GET (URL 직접 접근)
- 파일 업로드 multipart

Server Action — 생성

TS📋 코드 (67줄)
// 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 — 수정·삭제

TS📋 코드 (30줄)
'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용

TS📋 코드 (23줄)
// 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)

BASH📋 코드 (1줄)
pnpm add @upstash/redis @upstash/ratelimit
TS📋 코드 (13줄)
// 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