10. Edge Functions
함수 URL은 공개되어 있다. “Edge Function이니까 안전하다”는 착각이 가장 위험하다
Edge Functions란
섹션 제목: “Edge Functions란”Deno 런타임 기반의 서버리스 함수다. TypeScript를 그대로 실행하고,
전 세계 엣지 로케이션에 배포되어 사용자와 가까운 곳에서 실행된다.
https://<ref>.supabase.co/functions/v1/<함수명>으로 호출된다.
핵심은 secret key를 안전하게 쓸 수 있는 자리라는 것이다. 브라우저에 둘 수 없는 것들이 여기 온다 —
- 외부 API 호출 (결제, 이메일, LLM) — API 키가 필요한 것
- 웹훅 수신 (Stripe, GitHub 등) — 서명 검증 후 DB 반영
- 관리자 작업 — RLS를 우회해야 하는 일괄 처리
- Auth Hook의 HTTP 구현체
첫 함수
섹션 제목: “첫 함수”supabase functions new hello-world디렉터리supabase/functions/
디렉터리hello-world/
- index.ts
디렉터리_shared/ 여러 함수가 공유하는 코드 (관례)
- cors.ts
- deno.json import map
- .env
.gitignore에 넣을 것
import { withSupabase } from 'npm:@supabase/server'
export default { fetch: withSupabase({ auth: 'user' }, async (req, ctx) => { const { name } = await req.json() return Response.json({ message: `안녕하세요 ${name}님!`, userId: ctx.userClaims?.id, }) }),}withSupabase는 자격 증명 검증과 Supabase 클라이언트 생성을 대신 해준다.
auth: 'user'는 유효한 사용자 JWT만 받고, ctx.supabase에는 그 사용자의 RLS가 적용된다.
기존 예제에서 흔히 보이는 Deno.serve(async (req) => {...}) 형태도 여전히 동작한다.
로컬 실행과 배포
섹션 제목: “로컬 실행과 배포”# 로컬 실행 (Docker 필요, 핫 리로드 지원)supabase functions serve hello-world
# 호출해 보기curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/hello-world' \ --header 'apikey: <로컬 anon key>' \ --header 'Authorization: Bearer <로그인 사용자의 access token>' \ --header 'Content-Type: application/json' \ --data '{"name":"앨리스"}'
# 배포supabase functions deploy hello-world
# 전체 배포 / 목록 / 삭제supabase functions deploysupabase functions listsupabase functions delete hello-world로그는 대시보드 Edge Functions → 함수 선택 → Logs에서 본다.
console.log가 여기로 나오고, 로컬에서는 터미널에 바로 찍힌다.
호출하기
섹션 제목: “호출하기”// supabase-js — 현재 사용자의 JWT가 자동으로 실린다const { data, error } = await supabase.functions.invoke('hello-world', { body: { name: 'JavaScript' },})
// 헤더 추가 / 메서드 지정await supabase.functions.invoke('report', { method: 'POST', headers: { 'x-trace-id': traceId }, body: { month: '2026-08' },})functions.invoke는 HTTP 에러도 error로 돌려준다.
error.context에 응답 객체가 들어 있으니 디버깅할 때 여기를 본다.
시크릿 관리
섹션 제목: “시크릿 관리”# .env 파일로 한 번에 등록supabase secrets set --env-file ./supabase/functions/.env
# 개별 등록supabase secrets set OPENAI_API_KEY=sk-xxxx STRIPE_SECRET=sk_live_xxxx
# 목록 확인 (값은 해시로만 보인다)supabase secrets listconst apiKey = Deno.env.get('OPENAI_API_KEY')기본으로 주입되는 값들 —
| 변수 | 설명 |
|---|---|
SUPABASE_URL | 프로젝트 URL |
SUPABASE_PUBLISHABLE_KEYS | 구: SUPABASE_ANON_KEY. 키 회전 중엔 여러 개일 수 있어 복수형 |
SUPABASE_SECRET_KEYS | 구: SUPABASE_SERVICE_ROLE_KEY |
SUPABASE_JWKS | JWT 서명 검증용 공개키 세트 |
SUPABASE_DB_URL | 직접 연결 문자열 |
함수 안에서 DB 접근
섹션 제목: “함수 안에서 DB 접근”withSupabase가 만들어 준 두 클라이언트를 구분한다. 기본은 A다.
export default { fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { const { data, error } = await ctx.supabase.from('documents').select() // ctx.supabase에는 호출자의 JWT가 전달되어 RLS가 적용된다 return Response.json({ data, error }) }),}호출자의 JWT를 그대로 넘겨 DB가 판정하게 한다. 권한 로직을 함수에 복제하지 않아도 되는 게 장점이다.
export default { fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => { await ctx.supabaseAdmin.from('audit_logs').insert({ action: 'nightly-export' }) return Response.json({ ok: true }) }),}감사 로그 기록, 관리자 일괄 처리처럼 정말 우회해야 하는 작업에만 쓴다.
secret key는 JWT가 아니므로 이 모드는 해당 함수의 verify_jwt = false와 함께 쓰고,
withSupabase가 apikey 헤더의 secret key를 검증하게 한다.
JWT 검증과 인가
섹션 제목: “JWT 검증과 인가”플랫폼의 verify_jwt는 함수 코드가 실행되기 전에 Authorization 헤더의 사용자 JWT를 검증한다.
사용자 전용 함수(auth: 'user')에는 켜 두고, secret/publishable API 키나 외부 웹훅처럼
사용자 JWT 없이 호출하는 함수는 끈 뒤 withSupabase 또는 제공자 서명으로 검증한다.
[functions.stripe-webhook]verify_jwt = false# 또는 배포 시 플래그로supabase functions deploy stripe-webhook --no-verify-jwt의존성과 CORS
섹션 제목: “의존성과 CORS”// npm 패키지 — npm: 접두사import Stripe from 'npm:stripe@17'import { Resend } from 'npm:resend'
// JSR (Deno의 표준 레지스트리)import { createClient } from 'jsr:@supabase/supabase-js@2'
// Deno 표준 라이브러리import { encodeBase64 } from 'jsr:@std/encoding/base64'// supabase/functions/deno.json — import map으로 정리하면 관리가 편하다{ "imports": { "@supabase/supabase-js": "jsr:@supabase/supabase-js@2", "stripe": "npm:stripe@17" }}- Node 전용 API에 의존하는 패키지는 동작하지 않을 수 있다 (
fs,child_process등) - 버전을 반드시 고정한다. 고정하지 않으면 배포마다 다른 버전이 올라갈 수 있다
- 번들 크기가 크면 콜드 스타트가 느려진다
브라우저에서 직접 호출한다면 CORS(Cross-Origin Resource Sharing) 처리가 반드시 필요하다 — 다른 출처(도메인)로의 요청을 브라우저가 기본 차단하기 때문에, 함수가 허용 헤더로 풀어줘야 한다. 동작 원리는 web 덱 11장에서 깊게 다룬다.
export const corsHeaders = { 'Access-Control-Allow-Origin': '*', // 프로덕션에서는 실제 도메인으로 좁힐 것 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS',}Deno.serve(async (req) => { // preflight 요청 처리 — 빠뜨리면 브라우저에서만 호출이 실패한다 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
return new Response(JSON.stringify({ message: 'hello' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, })})Database Webhooks와 연동
섹션 제목: “Database Webhooks와 연동”DB 변경을 계기로 함수를 자동 실행한다.
- 대시보드 Database → Webhooks에서 테이블·이벤트·대상 URL을 설정한다
- 내부적으로는
pg_net확장을 쓰는 트리거다 — 비동기 HTTP 호출이라 DB 트랜잭션을 막지 않는다 - 재시도 정책이 제한적이므로 중요한 작업은 큐(pgmq)를 거치게 설계한다 (11장)
언제 Edge Function을 쓰나
섹션 제목: “언제 Edge Function을 쓰나”| 쓰기 좋은 경우 | Vercel 쪽이 나은 경우 |
|---|---|
| DB 이벤트에 반응하는 로직 (웹훅 수신자) | 프론트엔드와 강하게 결합된 로직 |
| Auth Hook의 HTTP 구현 | 렌더링과 함께 실행되는 데이터 조회 |
| 여러 클라이언트(웹·모바일·서버)가 공유하는 로직 | Node 전용 패키지가 필요한 작업 |
| 프론트엔드 배포와 무관하게 살아야 하는 로직 | 프레임워크 기능(스트리밍, 캐시 태그)을 쓰는 경우 |
| Supabase 시크릿만 필요한 작업 | 팀의 배포 파이프라인이 이미 Vercel 중심일 때 |
12장에서 이 판단 기준을 훨씬 자세히 다룬다. 지금은 “둘 다 서버 코드를 둘 수 있다”만 기억하면 된다.
제약과 함정
섹션 제목: “제약과 함정”- 콜드 스타트 — 오래 호출이 없으면 첫 요청이 느리다. 무거운 import를 줄인다
- 자원 제한 — 메모리 256MB, 요청당 CPU 시간 2초, 응답 대기 150초가 상한이다. 워커의 wall-clock 상한은 Free 150초, 유료 400초다. 장시간·CPU 집약 작업은 외부 워커로 넘긴다
- JWT 검증을 끄고 자체 검증을 잊음 — 가장 흔한 보안 사고
- secret key를 무비판적으로 사용 — 함수 URL은 공개되어 있다는 전제로 설계한다
- CORS preflight 미처리 — 브라우저에서만 실패한다
- Node 전용 패키지 사용 — 로컬에서는 되고 배포 후 깨지는 경우가 있다
- 로컬과 배포 환경의 시크릿 불일치 —
secrets set을 잊으면 배포본만 실패한다 - 함수가 비멱등 — 웹훅 재시도 시 중복 처리된다
10장 요약
섹션 제목: “10장 요약”- Edge Functions = Deno 서버리스. 시크릿이 필요한 코드의 자리
functions new→functions serve(로컬) →functions deploy- DB 접근은 호출자 권한(RLS 적용)이 기본, secret key는 검증 후에만
verify_jwt = false로 열었다면 반드시 자체 검증을 넣는다- 웹훅 연동 시 함수는 멱등하게 만든다
- Vercel Route Handler와 역할이 겹친다 → 12장에서 정리
참고 자료
섹션 제목: “참고 자료”- Edge Function 인증 —
@supabase/server,withSupabase, auth mode별 클라이언트 - Authorization 헤더 — 사용자 JWT와 API 키 헤더,
verify_jwt의 경계 - Edge Function 한도 — 메모리, CPU, wall-clock, 번들 크기
- 환경 변수 — 복수 키 JSON 맵과 기본 주입 변수