시리즈 목차 1편 – Supabase란 무엇인가? Firebase와 제대로 비교해보기 2편 – 데이터베이스 & 자동 API (REST/GraphQL) 3편 – 인증(Auth) — 소셜 로그인, JWT, MFA, SSO 4편 – RLS 보안 — Row Level Security 완벽 가이드 5편 – 실시간 기능(Realtime) — Broadcast, Presence, Postgres Changes 6편 – 스토리지(Storage) — 파일 업로드, 이미지 최적화, CDN 7편 – Edge Functions — Deno 기반 서버리스 함수 완벽 가이드 8편 – AI & 벡터 검색 (pgvector + RAG) 9편 👉 Cron Jobs & Queues — 백그라운드 작업 자동화 완벽 가이드 (현재 글) 10편 – MCP 서버 연동 — AI 에이전트와 Supabase …
들어가며
서비스를 운영하다 보면 “지금 당장 처리하면 안 되는” 작업들이 생깁니다.
- 매일 자정에 만료된 세션 정리
- 가입 후 7일이 지났는데 첫 게시글을 안 쓴 사용자에게 리마인더 이메일
- 사용자가 버튼을 클릭하면 AI 분석이 시작되는데, 결과가 나올 때까지 기다리면 타임아웃
- 상품이 등록될 때마다 자동으로 임베딩 생성
이런 요구사항을 해결하는 두 가지 핵심 도구가 바로 Cron Jobs와 Message Queues입니다.
Supabase는 이 둘을 PostgreSQL 안에서 기본 제공합니다.
- pg_cron: crontab 문법으로 SQL 또는 Edge Function을 정기 실행
- pgmq: PostgreSQL 기반 메시지 큐 (Redis나 RabbitMQ 없이도 구현 가능)
- pg_net: DB에서 HTTP 요청 발송 (Edge Function 비동기 호출)
이번 편에서 다룰 내용:
- pg_cron 기본 사용법과 cron 문법
- 주요 Cron Job 패턴 (DB 정리, 리마인더, 통계 집계)
- pg_net으로 Edge Function 주기적 호출
- pgmq 메시지 큐 기본 개념
- 큐에 메시지 등록 → Edge Function 워커로 처리
- 실전 패턴: 이메일 큐, AI 작업 큐, 웹훅 재시도
- Next.js에서 큐에 작업 등록하기
- 모니터링 및 실패 처리
pg_cron — 정기 작업 스케줄러
활성화
-- 대시보드: Database → Extensions → pg_cron 검색 후 활성화
-- 또는 SQL:
CREATE EXTENSION IF NOT EXISTS pg_cron WITH SCHEMA extensions;
CREATE EXTENSION IF NOT EXISTS pg_net WITH SCHEMA extensions;
또는 대시보드 통합 메뉴: Integrations → Cron 에서 활성화 (권장, 2024년 이후)
cron 문법
┌─── 분 (0-59)
│ ┌─── 시 (0-23)
│ │ ┌─── 일 (1-31)
│ │ │ ┌─── 월 (1-12)
│ │ │ │ ┌─── 요일 (0-7, 0=일요일, 7=일요일)
│ │ │ │ │
* * * * *
자주 쓰는 패턴:
| 표현식 | 설명 |
|---|---|
* * * * * | 매분 실행 |
*/5 * * * * | 5분마다 |
0 * * * * | 매시 정각 |
0 9 * * * | 매일 오전 9시 (UTC) |
0 0 * * 0 | 매주 일요일 자정 |
0 0 1 * * | 매월 1일 자정 |
0 2 * * 1-5 | 평일 새벽 2시 |
⚠️ pg_cron은 UTC 기준입니다. 한국 시간(KST)은 UTC+9이므로, KST 오전 9시 = UTC 오전 0시 (
0 0 * * *)
Cron Job 기본 패턴
1. 만료된 데이터 정리
-- 30일 이상 된 알림 삭제 (매일 새벽 3시 UTC)
SELECT cron.schedule(
'cleanup-old-notifications', -- 작업 이름 (고유)
'0 3 * * *', -- 스케줄
$$
DELETE FROM notifications
WHERE created_at < NOW() - INTERVAL '30 days'
AND is_read = true;
$$
);
-- 만료된 세션/토큰 정리 (1시간마다)
SELECT cron.schedule(
'cleanup-expired-sessions',
'0 * * * *',
$$
DELETE FROM user_sessions
WHERE expires_at < NOW();
$$
);
-- 소프트 딜리트된 레코드 영구 삭제 (매주 일요일)
SELECT cron.schedule(
'purge-deleted-records',
'0 0 * * 0',
$$
DELETE FROM posts WHERE deleted_at < NOW() - INTERVAL '90 days';
DELETE FROM comments WHERE deleted_at < NOW() - INTERVAL '90 days';
$$
);
2. 통계 집계 (Materialized View 갱신)
-- 일별 통계 집계 Materialized View
CREATE MATERIALIZED VIEW daily_stats AS
SELECT
DATE(created_at) AS date,
COUNT(*) AS new_users,
COUNT(DISTINCT user_id) FILTER (WHERE event_type = 'login') AS active_users
FROM events
GROUP BY DATE(created_at);
-- 매시간 갱신
SELECT cron.schedule(
'refresh-daily-stats',
'0 * * * *',
$$
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats;
$$
);
3. 상태 변경 처리
-- 결제 후 48시간이 지나도록 배송이 없으면 상태 변경
SELECT cron.schedule(
'auto-flag-delayed-orders',
'*/30 * * * *', -- 30분마다
$$
UPDATE orders
SET status = 'delayed'
WHERE status = 'paid'
AND created_at < NOW() - INTERVAL '48 hours'
AND shipped_at IS NULL;
$$
);
-- 구독 만료 처리
SELECT cron.schedule(
'expire-subscriptions',
'*/10 * * * *', -- 10분마다
$$
UPDATE subscriptions
SET status = 'expired'
WHERE status = 'active'
AND expires_at < NOW();
$$
);
Cron Job 관리
-- 등록된 작업 목록 확인
SELECT * FROM cron.job;
-- 실행 이력 확인 (최근 10건)
SELECT *
FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 10;
-- 실패한 작업 확인
SELECT *
FROM cron.job_run_details
WHERE status = 'failed'
ORDER BY start_time DESC;
-- 작업 비활성화
SELECT cron.unschedule('cleanup-old-notifications');
-- 작업 수정 (삭제 후 재등록)
SELECT cron.unschedule('cleanup-old-notifications');
SELECT cron.schedule('cleanup-old-notifications', '0 4 * * *', $$ ... $$);
pg_net + pg_cron으로 Edge Function 주기적 호출
SQL만으로 해결하기 어려운 작업(외부 API 호출, 복잡한 로직)은 Edge Function을 스케줄로 실행합니다.
Vault에 URL & 토큰 저장 (권장)
-- Supabase Vault에 시크릿 저장 (대시보드: Database → Vault)
SELECT vault.create_secret(
'https://[project].supabase.co',
'project_url'
);
SELECT vault.create_secret(
'Bearer eyJhbGciOi...', -- service_role key
'service_role_token'
);
pg_net으로 Edge Function 호출 스케줄
-- 매일 오전 9시(UTC) 리마인더 이메일 발송 Edge Function 호출
SELECT cron.schedule(
'send-reminder-emails',
'0 0 * * *',
$$
SELECT net.http_post(
url := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'project_url')
|| '/functions/v1/send-reminders',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'service_role_token')
),
body := '{}'::JSONB
);
$$
);
-- 10분마다 큐 처리 워커 실행
SELECT cron.schedule(
'process-email-queue',
'*/10 * * * *',
$$
SELECT net.http_post(
url := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'project_url')
|| '/functions/v1/process-email-queue',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'service_role_token')
),
body := '{"batch_size": 20}'::JSONB
);
$$
);
pgmq — PostgreSQL 메시지 큐
활성화
-- 대시보드: Integrations → Queue 활성화 (권장)
-- 또는:
CREATE EXTENSION IF NOT EXISTS pgmq WITH SCHEMA pgmq;
pgmq는 pgmq_public 스키마를 통해 supabase-js에서도 접근할 수 있습니다.
큐 생성 및 기본 조작
-- 큐 생성
SELECT pgmq.create('email_queue');
SELECT pgmq.create('ai_jobs');
SELECT pgmq.create('webhooks');
-- 메시지 전송
SELECT pgmq.send(
'email_queue',
'{"to": "user@example.com", "subject": "안녕하세요", "template": "welcome"}'::JSONB
);
-- 메시지 읽기 (visibility timeout: 30초)
-- 읽은 메시지는 30초간 다른 워커에게 보이지 않음
SELECT * FROM pgmq.read(
queue_name := 'email_queue',
vt := 30, -- visibility timeout (초)
qty := 5 -- 한 번에 읽을 메시지 수
);
-- 처리 완료 후 삭제
SELECT pgmq.delete('email_queue', 42); -- 42: message id
-- 처리 실패 시 즉시 재노출 (visibility 초기화)
SELECT pgmq.set_vt('email_queue', 42, 0);
-- 큐에 남은 메시지 수 확인
SELECT * FROM pgmq.metrics('email_queue');
pgmq 핵심 개념: Visibility Timeout
메시지를 읽으면 일정 시간 동안 다른 워커에게 보이지 않습니다. 처리 완료 시 삭제하고, 실패하면 시간이 지나면 자동으로 다시 보이게 됩니다. 이것이 Redis/Bull의 “at-least-once” 보장과 같은 원리입니다.
메시지 전송 → [큐에 대기]
↓
워커가 읽기 (visibility timeout 시작)
↓
├── 처리 성공 → DELETE → 큐에서 영구 제거
│
└── 처리 실패 / 타임아웃 → visibility 만료 → 다시 대기 상태 → 재시도
실전 패턴 1: 이메일 큐 워커
1단계: 큐 생성 및 이메일 등록 함수
-- 이메일 큐 생성
SELECT pgmq.create('email_queue');
-- 이메일 큐에 추가하는 헬퍼 함수
CREATE OR REPLACE FUNCTION queue_email(
to_email TEXT,
subject TEXT,
template TEXT,
data JSONB DEFAULT '{}'
)
RETURNS BIGINT
LANGUAGE SQL
AS $$
SELECT pgmq.send(
'email_queue',
jsonb_build_object(
'to', to_email,
'subject', subject,
'template', template,
'data', data
)
);
$$;
-- 사용 예시: 새 사용자 가입 시 환영 이메일 큐 등록
SELECT queue_email(
'user@example.com',
'가입을 환영합니다!',
'welcome',
'{"name": "홍길동"}'
);
2단계: Next.js Server Action에서 큐 등록
// app/actions/email.ts
'use server'
import { createClient } from '@/lib/supabase/server'
export async function queueWelcomeEmail(userId: string) {
const supabase = await createClient()
const { data: user } = await supabase
.from('profiles')
.select('email, name')
.eq('id', userId)
.single()
if (!user) return
// pgmq_public 스키마로 메시지 전송
await supabase.schema('pgmq_public').rpc('send', {
queue_name: 'email_queue',
message: {
to: user.email,
subject: `${user.name}님, 환영합니다!`,
template: 'welcome',
data: { name: user.name },
},
})
}
// 일반 알림 이메일
export async function queueNotificationEmail(
to: string,
subject: string,
content: string
) {
const supabase = await createClient()
await supabase.schema('pgmq_public').rpc('send', {
queue_name: 'email_queue',
message: { to, subject, template: 'notification', data: { content } },
})
}
3단계: 이메일 워커 Edge Function
// supabase/functions/process-email-queue/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')!
interface EmailMessage {
to: string
subject: string
template: string
data: Record<string, string>
}
// HTML 템플릿 렌더러 (간단 버전)
function renderTemplate(template: string, data: Record<string, string>): string {
const templates: Record<string, string> = {
welcome: `
<h1>환영합니다, ${data.name}님! 🎉</h1>
<p>저희 서비스에 가입해 주셔서 감사합니다.</p>
`,
notification: `
<p>${data.content}</p>
`,
}
return templates[template] ?? `<p>${data.content ?? ''}</p>`
}
Deno.serve(async (req: Request) => {
const { batch_size = 10 } = await req.json().catch(() => ({}))
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
{ db: { schema: 'pgmq_public' } }
)
// 큐에서 메시지 읽기 (30초 visibility timeout)
const { data: messages, error } = await supabase.rpc('read', {
queue_name: 'email_queue',
sleep_seconds: 30,
n: batch_size,
})
if (error || !messages?.length) {
return new Response(
JSON.stringify({ processed: 0, message: '처리할 메시지 없음' }),
{ headers: { 'Content-Type': 'application/json' } }
)
}
let processed = 0
let failed = 0
for (const msg of messages) {
const { to, subject, template, data } = msg.message as EmailMessage
try {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${RESEND_API_KEY}`,
},
body: JSON.stringify({
from: 'noreply@yourapp.com',
to,
subject,
html: renderTemplate(template, data),
}),
})
if (!res.ok) throw new Error(`Resend 오류: ${await res.text()}`)
// 성공 → 큐에서 삭제
await supabase.rpc('delete', {
queue_name: 'email_queue',
message_id: msg.msg_id,
})
processed++
} catch (err) {
console.error(`이메일 발송 실패 (msg_id: ${msg.msg_id}):`, err.message)
// 실패 시 아무것도 안 해도 됨 — visibility timeout 후 자동 재시도
failed++
}
}
return new Response(
JSON.stringify({ processed, failed }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
4단계: pg_cron으로 워커 주기 실행
-- 5분마다 이메일 큐 처리
SELECT cron.schedule(
'process-email-queue',
'*/5 * * * *',
$$
SELECT net.http_post(
url := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'project_url')
|| '/functions/v1/process-email-queue',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'service_role_token')
),
body := '{"batch_size": 20}'::JSONB
);
$$
);
실전 패턴 2: AI 장기 작업 큐
AI 처리(이미지 분석, 문서 요약, 임베딩 생성)는 수 초에서 수십 초가 걸리는 장기 작업입니다. 사용자가 버튼을 클릭하면 즉시 응답하고, 결과가 준비되면 알려주는 패턴이 이상적입니다.
-- AI 작업 추적 테이블
CREATE TABLE ai_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
type TEXT NOT NULL, -- 'summarize', 'embed', 'analyze' 등
input JSONB NOT NULL,
status TEXT DEFAULT 'pending', -- pending / processing / done / failed
result JSONB,
error_msg TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE ai_jobs ENABLE ROW LEVEL SECURITY;
CREATE POLICY "본인 작업만 접근"
ON ai_jobs FOR ALL
TO authenticated
USING ((SELECT auth.uid()) = user_id);
-- AI 작업 큐 생성
SELECT pgmq.create('ai_jobs');
// app/actions/ai-jobs.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export async function submitAiJob(
type: 'summarize' | 'analyze',
input: Record<string, unknown>
) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('로그인 필요')
// 1. DB에 작업 레코드 생성
const { data: job, error } = await supabase
.from('ai_jobs')
.insert({ user_id: user.id, type, input, status: 'pending' })
.select('id')
.single()
if (error) throw error
// 2. 큐에 작업 ID 등록
await supabase.schema('pgmq_public').rpc('send', {
queue_name: 'ai_jobs',
message: { job_id: job.id, type, input },
})
revalidatePath('/dashboard')
return job.id
}
// supabase/functions/process-ai-jobs/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const OPENAI_API_KEY = Deno.env.get('OPENAI_API_KEY')!
Deno.serve(async () => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const supabasePgmq = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
{ db: { schema: 'pgmq_public' } }
)
// 큐에서 최대 5개 작업 읽기 (60초 visibility timeout)
const { data: messages } = await supabasePgmq.rpc('read', {
queue_name: 'ai_jobs',
sleep_seconds: 60,
n: 5,
})
if (!messages?.length) {
return new Response('처리할 작업 없음', { status: 200 })
}
for (const msg of messages) {
const { job_id, type, input } = msg.message
// 작업 상태를 'processing'으로 변경
await supabase
.from('ai_jobs')
.update({ status: 'processing', updated_at: new Date().toISOString() })
.eq('id', job_id)
try {
let result: unknown
if (type === 'summarize') {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: '다음 텍스트를 3문장으로 요약하세요.' },
{ role: 'user', content: input.text },
],
}),
})
const data = await res.json()
result = { summary: data.choices[0].message.content }
}
// 성공: 결과 저장 + 큐에서 삭제
await supabase
.from('ai_jobs')
.update({ status: 'done', result, updated_at: new Date().toISOString() })
.eq('id', job_id)
await supabasePgmq.rpc('delete', {
queue_name: 'ai_jobs',
message_id: msg.msg_id,
})
} catch (err) {
// 실패: 에러 저장 + 큐에서 삭제 (재시도 방지)
await supabase
.from('ai_jobs')
.update({
status: 'failed',
error_msg: err.message,
updated_at: new Date().toISOString(),
})
.eq('id', job_id)
await supabasePgmq.rpc('delete', {
queue_name: 'ai_jobs',
message_id: msg.msg_id,
})
}
}
return new Response(JSON.stringify({ processed: messages.length }), {
headers: { 'Content-Type': 'application/json' },
})
})
Next.js에서 작업 상태 폴링
// hooks/useJobStatus.ts
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function useJobStatus(jobId: string | null) {
const [status, setStatus] = useState<string | null>(null)
const [result, setResult] = useState<unknown>(null)
const supabase = createClient()
useEffect(() => {
if (!jobId) return
// Realtime으로 작업 상태 구독
const channel = supabase
.channel(`job-${jobId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'ai_jobs',
filter: `id=eq.${jobId}`,
},
(payload) => {
setStatus(payload.new.status)
setResult(payload.new.result)
}
)
.subscribe()
// 초기 상태 조회
supabase
.from('ai_jobs')
.select('status, result')
.eq('id', jobId)
.single()
.then(({ data }) => {
if (data) {
setStatus(data.status)
setResult(data.result)
}
})
return () => { supabase.removeChannel(channel) }
}, [jobId])
return { status, result }
}
실전 패턴 3: 웹훅 재시도 큐
외부 서비스로 보내는 웹훅이 실패할 경우 자동으로 재시도하는 패턴입니다.
-- 웹훅 큐 생성
SELECT pgmq.create('webhooks');
-- 웹훅 전송 함수 (트리거에서 호출)
CREATE OR REPLACE FUNCTION queue_webhook(
url TEXT,
payload JSONB,
headers JSONB DEFAULT '{"Content-Type": "application/json"}'
)
RETURNS BIGINT
LANGUAGE SQL
AS $$
SELECT pgmq.send(
'webhooks',
jsonb_build_object(
'url', url,
'payload', payload,
'headers', headers,
'attempt', 1,
'max_retries', 3
)
);
$$;
-- 주문 완료 시 자동으로 웹훅 큐에 등록하는 트리거
CREATE OR REPLACE FUNCTION trigger_order_webhook()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.status = 'completed' AND OLD.status != 'completed' THEN
PERFORM queue_webhook(
'https://partner.example.com/webhooks/order',
jsonb_build_object('order_id', NEW.id, 'status', NEW.status)
);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER on_order_complete
AFTER UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION trigger_order_webhook();
모니터링
Cron Job 실행 이력
-- 최근 실패한 Cron Job 확인
SELECT
j.jobname,
r.start_time,
r.end_time,
r.status,
r.return_message
FROM cron.job_run_details r
JOIN cron.job j ON j.jobid = r.jobid
WHERE r.status = 'failed'
ORDER BY r.start_time DESC
LIMIT 20;
-- 각 작업별 평균 실행 시간
SELECT
j.jobname,
AVG(EXTRACT(EPOCH FROM (r.end_time - r.start_time))) AS avg_seconds,
COUNT(*) AS total_runs,
COUNT(*) FILTER (WHERE r.status = 'failed') AS failed_runs
FROM cron.job_run_details r
JOIN cron.job j ON j.jobid = r.jobid
GROUP BY j.jobname
ORDER BY avg_seconds DESC;
Queue 메트릭
-- 모든 큐 상태 확인
SELECT * FROM pgmq.metrics_all();
-- 특정 큐 상세 확인
SELECT * FROM pgmq.metrics('email_queue');
-- 반환: queue_name, queue_length, newest_msg_age, oldest_msg_age, ...
-- 처리되지 않고 오래된 메시지 (데드 레터 대기 중인 것들)
SELECT *
FROM pgmq.q_email_queue
WHERE enqueued_at < NOW() - INTERVAL '1 hour'
ORDER BY enqueued_at;
자주 하는 실수
1. 무거운 작업을 트리거에서 직접 처리
❌ 트리거 → 즉시 외부 API 호출 → DB 트랜잭션 블로킹
✅ 트리거 → 큐에 메시지 등록 → 워커가 비동기 처리
2. Visibility Timeout 너무 짧게 설정
❌ vt=5 (5초) → 처리가 5초 이상 걸리면 중복 처리 발생
✅ 작업의 예상 처리 시간 × 2 이상으로 설정
이메일: vt=30, AI 작업: vt=120
3. UTC/KST 혼동
-- ❌ 오전 9시(KST)에 실행하고 싶은데
SELECT cron.schedule('morning-job', '0 9 * * *', $$...$$);
-- → UTC 오전 9시 = KST 오후 6시에 실행됨!
-- ✅ KST 오전 9시 = UTC 0시
SELECT cron.schedule('morning-job', '0 0 * * *', $$...$$);
4. 오래된 cron.job_run_details 방치
-- pg_cron 실행 기록은 자동 삭제되지 않으므로 주기적으로 정리 필요
SELECT cron.schedule(
'cleanup-cron-history',
'0 3 * * 0', -- 매주 일요일 새벽 3시
$$
DELETE FROM cron.job_run_details
WHERE end_time < NOW() - INTERVAL '30 days';
$$
);
마치며
Supabase의 pg_cron + pgmq 조합은 Redis, Bull, RabbitMQ 등 별도의 인프라 없이 PostgreSQL 안에서 완전한 백그라운드 작업 시스템을 구축할 수 있게 해줍니다. 특히 Realtime 구독과 결합하면 “작업 제출 → 비동기 처리 → 완료 알림”의 훌륭한 사용자 경험을 만들 수 있습니다.
다음 편에서는 MCP(Model Context Protocol) 서버 연동을 다룹니다. AI 에이전트가 Supabase를 직접 도구로 사용하도록 연결하는 방법과, Claude 같은 LLM이 데이터베이스를 쿼리하고 수정할 수 있는 MCP 서버 구성을 살펴봅니다.