Supabase Edge Functions 실전: Deno 런타임과 TypeScript 개발 가이드

휴대전화가 쉴 새 없이 진동했습니다. 프로덕션 환경의 Stripe Webhook에서 500 오류가 쏟아졌고, 고객의 결제는 성공했지만 주문은 생성되지 않았습니다.
일어나 로그를 살펴보니 기존 serverless 함수가 문제였습니다. 콜드 스타트가 너무 오래 걸려 Stripe가 기다리지 못하고 타임아웃된 것입니다. 더 골치 아픈 점은 서명 검증과 CORS 처리를 위해 API 게이트웨이까지 별도로 구축해야 한다는 것이었습니다.
그날 밤 이후 Supabase Edge Functions를 본격적으로 알아보기 시작했습니다. 처음에는 ‘Deno 런타임’이라는 말을 보고 조금 망설였습니다. 오랫동안 Node.js로 개발해 왔기 때문에 런타임을 바꾸면 새로운 API 체계를 다시 배워야 한다고 생각했기 때문입니다. 하지만 직접 다뤄 보니 Edge Functions의 설계 철학은 완전히 달랐습니다. 기존 시스템을 억지로 ‘마이그레이션’하게 하는 것이 아니라, 무거운 의존성이 필요하지 않은 상황을 위한 더 가벼운 선택지를 제공합니다.
이 글에서는 직접 겪은 시행착오와 배운 내용을 공유합니다. Edge Functions의 아키텍처 원리, Deno와 Node.js의 차이, 로컬 개발 및 디버깅 과정, Hono 프레임워크로 API를 깔끔하게 작성하는 실전 경험을 차례로 살펴보겠습니다.
Edge Functions란 무엇인가 — 아키텍처와 기술 선택
먼저 Edge Functions가 무엇인지, 그리고 Supabase가 Node.js 대신 Deno를 선택한 이유부터 짚어 보겠습니다.
클라우드 호스팅이 아닌 엣지 실행
Edge Functions는 엣지 노드에서 실행되는 TypeScript 함수입니다. 기존 Lambda나 Vercel Functions와 달리 몇몇 대형 리전의 서버에 집중적으로 배포되지 않고, 전 세계 수백 개 엣지 노드에 분산됩니다.
이것이 의미하는 바는 무엇일까요? 상하이에 있는 사용자가 요청을 보내면 도쿄의 엣지 노드에서 함수가 실행될 수 있고, 지연 시간이 수백 밀리초에서 수십 밀리초로 줄어듭니다.
하지만 엣지 실행에도 대가는 있습니다. 함수가 지나치게 무거워서는 안 됩니다. 각 함수는 독립적인 V8 isolate에서 자체 메모리 힙과 실행 스레드를 사용합니다. 시작 속도는 밀리초 단위로 빠르지만 메모리와 실행 시간에 제한이 있습니다. 따라서 Webhook 처리, OG 이미지 생성, 서드파티 API 호출, 이메일 전송처럼 수명이 짧은 작업에 적합합니다.
반대로 오래 실행되는 작업, Node.js 네이티브 모듈에 크게 의존하는 라이브러리, 파일 시스템 접근이 필요한 작업에는 적합하지 않습니다.
왜 Deno인가
이 질문의 답을 찾으려고 Supabase의 GitHub Discussion을 오랫동안 뒤졌습니다. 공식 설명을 대략 정리하면 다음과 같습니다.
- 빠른 시작 속도: Deno는 코드를 ESZip 형식으로 묶어 함수의 콜드 스타트를 0
5ms까지 줄일 수 있습니다. 반면 Node.js 기반 Lambda의 콜드 스타트는 보통 100500ms입니다. - 보안 모델: Deno는 파일 시스템 및 네트워크 접근을 기본적으로 차단하며 명시적인 권한을 요구합니다. 멀티 테넌트 엣지 환경에서는 특히 중요합니다. 다른 사람의 함수가 내 데이터를 읽는 일은 없어야 하니까요.
- TypeScript 기본 지원: tsconfig를 설정하거나 ts-node를 설치하지 않아도
.ts파일을 바로 실행할 수 있습니다. 이미 TypeScript로 백엔드를 개발해 온 사람이라면 설정 시간을 상당히 절약할 수 있습니다. - 이식성: Deno는 다른 애플리케이션에 임베드할 수 있습니다. Supabase는 임베디드 환경을 위해 개선한 자체 Deno 포크인
deno_core를 사용합니다.
물론 장점만 있는 것은 아닙니다. Deno 생태계는 Node.js보다 훨씬 작고, 일부 npm 패키지는 바로 사용할 수 없습니다. 그래도 현재 Deno는 npm specifiers를 지원하므로 import { xxx } from 'npm:lodash'처럼 가져올 수 있고 호환성도 크게 좋아졌습니다.
아키텍처 한눈에 보기
요청이 들어오면 대략 다음 흐름으로 처리됩니다.
클라이언트 → CDN/엣지 게이트웨이 → JWT 검증 → V8 isolate에서 함수 실행 → 응답 반환
여기서 중요한 부분은 JWT 검증입니다. Edge Functions는 기본적으로 요청의 Authorization header를 검증해 권한이 있는 사용자만 호출할 수 있도록 합니다. 공개 접근을 허용하려면 배포할 때 --no-verify-jwt 플래그를 추가해야 합니다.
개발 환경 구성과 CLI 명령 자세히 보기
개념은 여기까지입니다. 이제 직접 시작해 보겠습니다.
Supabase CLI 설치
저는 macOS를 사용하므로 Homebrew로 바로 설치했습니다.
brew install supabase/tap/supabase
Linux와 Windows에도 각각 설치 방법이 있습니다. 공식 문서에 잘 정리되어 있으므로 여기서는 반복하지 않겠습니다.
설치가 끝나면 로그인합니다.
supabase login
이 단계에서 브라우저가 열리고 CLI의 Supabase 계정 접근 권한을 승인하게 됩니다.
프로젝트 초기화
프로젝트 디렉터리에서 다음 명령을 실행합니다.
supabase init
그러면 설정 파일 config.toml과 functions/ 하위 디렉터리가 들어 있는 supabase/ 디렉터리가 생성됩니다. functions/가 없다면 자동으로 만들어집니다.
첫 번째 Edge Function 만들기
supabase functions new hello-world
이 명령은 supabase/functions/ 아래에 hello-world/ 디렉터리를 만들고 그 안에 다음과 같은 index.ts 파일을 생성합니다.
Deno.serve(async (req: Request) => {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Connection': 'keep-alive',
},
})
})
정말 이게 전부입니다. Deno.serve()는 요청 처리 함수를 받는 Deno의 기본 API입니다. Request와 Response는 모두 표준 Web API이므로 브라우저의 fetch와 같은 방식으로 사용할 수 있습니다.
로컬 개발 서버
로컬 개발 환경을 시작합니다.
supabase functions serve --env-file supabase/.env.local
이 명령은 기본 주소가 http://localhost:54321인 로컬 서버를 시작합니다. 함수에는 http://localhost:54321/functions/v1/hello-world로 접근할 수 있습니다.
솔직히 처음 실행했을 때 한 가지 실수를 했습니다. 로컬 PostgreSQL을 포함하는 Supabase 로컬 서비스 스택을 먼저 시작하는 것을 깜빡한 것입니다. 올바른 순서는 다음과 같습니다.
# 먼저 로컬 Supabase 스택 시작
supabase start
# 그다음 함수 서비스 시작
supabase functions serve
요청 테스트
curl이나 HTTPie로 요청을 보내 테스트해 봅니다.
curl -i --location --request POST 'http://localhost:54321/functions/v1/hello-world' \
--header 'Authorization: Bearer <your-anon-key>' \
--header 'Content-Type: application/json' \
--data '{"name":"World"}'
응답은 다음과 같습니다.
{
"message": "Hello World!"
}
성공했습니다.
핫 리로드는 자동으로 활성화되어 있습니다. 코드를 수정하고 저장하면 서비스를 다시 시작하지 않아도 바로 반영됩니다. 이 부분의 개발 경험은 꽤 좋았습니다.
환경 변수
민감한 정보를 코드에 작성하지 마세요. Supabase는 .env 파일로 환경 변수를 관리할 수 있습니다.
# .env 파일 생성
echo "MY_SECRET=super_secret_value" > supabase/.env.local
# 함수에서 읽기
const mySecret = Deno.env.get('MY_SECRET')
프로덕션 환경에 배포할 때는 supabase secrets set 명령을 사용합니다.
supabase secrets set MY_SECRET=super_secret_value
실전: Hono 프레임워크로 RESTful API 구축하기
기본 Deno.serve()만으로도 충분할 수 있지만, 라우팅·미들웨어·매개변수 검증 등이 필요해져 함수 로직이 복잡해지면 직접 구현하기가 매우 번거롭습니다.
이럴 때 Hono가 유용합니다.
Hono란 무엇인가
Hono는 엣지 런타임을 위해 설계된 초경량 Web 프레임워크입니다. Deno, Cloudflare Workers, Bun을 비롯한 여러 런타임을 지원하고 라우팅 성능이 뛰어나며 TypeScript 지원도 훌륭합니다.
공식 설명은 ‘small, simple, and ultrafast’입니다. 직접 사용해 보니 정말 그 표현과 잘 맞았습니다.
Edge Functions에 통합하기
먼저 새 함수를 만듭니다.
supabase functions new user-api
그다음 index.ts를 수정합니다.
import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
import { logger } from 'jsr:@hono/hono/logger'
const app = new Hono().basePath('/api')
// 미들웨어
app.use('*', cors())
app.use('*', logger())
// 라우트 정의
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ user: { id, name: 'Demo User', email: '[email protected]' } })
})
app.post('/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
// 여기에 Supabase 데이터베이스를 연결할 수 있습니다
return c.json({ created: body }, 201)
})
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json<{ name?: string; email?: string }>()
return c.json({ updated: { id, ...body } })
})
app.delete('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: id })
})
// 서비스 시작
Deno.serve(app.fetch)
핵심 내용은 다음과 같습니다.
jsr:@hono/hono는 npm이 아니라 Deno의 JSR 패키지 관리 형식입니다. JSR은 Deno의 공식 패키지 저장소입니다.basePath('/api')를 사용하면 라우트 접두사가/api로 설정됩니다.c는 요청, 응답, 각종 유틸리티 메서드를 담고 있는 Hono의 context 객체입니다.c.json()은 Content-Type header를 자동으로 설정하며 null과 undefined도 처리할 수 있습니다.
Supabase 데이터베이스 연결
Hono는 Web 프레임워크일 뿐이므로 데이터베이스를 다루려면 Supabase 클라이언트가 필요합니다. 다음은 전체 예제입니다.
import { Hono } from 'jsr:@hono/hono'
import { createClient } from 'jsr:@supabase/supabase-js@2'
const app = new Hono().basePath('/api')
// Supabase 클라이언트 초기화
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
})
// GET /api/users - 목록
app.get('/users', async (c) => {
const { data, error } = await supabase
.from('users')
.select('id, name, email, created_at')
if (error) {
return c.json({ error: error.message }, 500)
}
return c.json({ users: data })
})
// POST /api/users - 생성
app.post('/users', async (c) => {
const body = await c.req.json<{ name: string; email: string }>()
const { data, error } = await supabase
.from('users')
.insert(body)
.select()
.single()
if (error) {
return c.json({ error: error.message }, 400)
}
return c.json({ user: data }, 201)
})
Deno.serve(app.fetch)
여기서는 SUPABASE_SERVICE_ROLE_KEY를 사용했습니다. 이 key는 전체 데이터베이스 권한을 가지며 RLS를 우회하므로 프로덕션 환경에서는 신중하게 사용해야 합니다.
오류 처리 및 검증
Hono에는 기본 검증기가 없지만 Zod와 함께 사용할 수 있습니다.
import { z } from 'npm:zod'
import { zValidator } from 'jsr:@hono/zod-validator'
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
})
app.post(
'/users',
zValidator('json', userSchema),
async (c) => {
const validated = c.req.valid('json')
// validated는 이미 타입 안전성이 보장된 객체입니다
return c.json({ received: validated })
}
)
검증에 실패하면 상세한 오류 정보가 담긴 응답 본문과 함께 400 오류가 자동으로 반환됩니다.
배포 및 프로덕션 환경 모범 사례
로컬에서 정상적으로 실행되는 것을 확인했다면 이제 프로덕션 환경에 올릴 차례입니다.
배포 명령
supabase functions deploy user-api
처음 배포할 때는 연결할 Supabase 프로젝트를 묻습니다. 이후에는 코드 업로드, 빌드, 배포가 자동으로 진행됩니다.
배포가 성공하면 함수의 URL 형식은 다음과 같습니다.
https://[PROJECT_ID].supabase.co/functions/v1/user-api
환경 변수와 Secrets
프로덕션 환경의 환경 변수는 별도로 설정해야 합니다.
supabase secrets set SUPABASE_URL=https://xxx.supabase.co
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
이 Secrets는 암호화되어 저장되며 함수 실행 중 Deno.env.get()으로 읽을 수 있습니다.
JWT 검증 전략
앞서 설명했듯 Edge Functions는 기본적으로 JWT를 검증합니다. 이는 다음을 의미합니다.
- 유효한
Authorization: Bearer <token>이 포함된 요청만 통과합니다. - Token에 포함된 사용자 정보는
req.headers에서 파싱할 수 있습니다.
서드파티 Webhook처럼 공개 API가 필요하다면 배포할 때 --no-verify-jwt를 추가합니다.
supabase functions deploy user-api --no-verify-jwt
다만 이렇게 하면 누구나 함수를 호출할 수 있으므로 코드에서 직접 검증해야 합니다.
콜드 스타트 지연 줄이기
Deno의 콜드 스타트는 매우 빠르지만 더 개선할 수 있는 방법이 있습니다.
- 의존성 크기 줄이기: 가능한 한 Deno/JSR 기본 패키지를 사용하고 npm 패키지는 적게 사용합니다.
- 지연 로딩: 큰 모듈은 필요할 때
import()합니다. - 함수를 가볍게 유지하기: 함수 하나는 한 가지 일만 처리하도록 하고 백엔드 전체를 한 함수에 넣지 않습니다.
Supabase는 함수 하나의 실행 시간을 2초 이하로 유지할 것을 공식적으로 권장하며 콜드 스타트 시간은 0~5ms입니다. 위 방법을 적용하면 응답 속도를 더 높일 수 있습니다.
모니터링과 로그
Dashboard에서 함수 호출 로그와 오류 보고서를 확인할 수 있습니다. Sentry나 다른 모니터링 서비스와 연동할 수도 있습니다.
또한 EdgeRuntime.waitUntil() API를 사용하면 응답을 반환한 뒤에도 함수가 백그라운드 작업을 계속 실행하도록 할 수 있습니다.
EdgeRuntime.waitUntil(
fetch('https://analytics.example.com/track', { method: 'POST', body: '...' })
)
return new Response('OK')
이렇게 하면 백그라운드 작업이 끝날 때까지 기다리지 않고도 클라이언트가 응답을 받을 수 있습니다.
결론
지금까지 살펴본 Edge Functions는 어떤 상황에 적합할까요?
적합한 작업:
- Webhook 처리(Stripe, GitHub, Slack)
- OG 이미지 생성
- AI 추론(LLM API 호출)
- 이메일 및 메시지 알림
- 수명이 짧은 데이터 처리
적합하지 않은 작업:
- 동영상 트랜스코딩처럼 오래 실행되는 작업
- 무거운 Node.js 네이티브 모듈에 의존하는 라이브러리
- 파일 시스템 접근이 필요한 작업
이미 Supabase의 데이터베이스와 인증을 사용하고 있다면 Edge Functions는 매우 자연스러운 확장 방식입니다. 별도 서버를 구축하거나 운영을 걱정할 필요 없이 비즈니스 로직만 작성하면 됩니다.
Cloudflare Workers나 Vercel Functions와 비교하면 어떨까요? 각각 장점이 있습니다. Cloudflare Workers는 더 성숙하고 생태계도 크며, Vercel Functions는 Next.js 생태계와 더 긴밀하게 결합되어 있습니다. 하지만 이미 Supabase를 사용한다면 데이터베이스 클라이언트, 인증, 스토리지가 모두 준비된 Edge Functions가 가장 매끄러운 통합 경험을 제공합니다.
직접 사용해 보고 싶다면 공식 예제 저장소에서 시작할 수 있습니다: github.com/supabase/supabase/tree/master/examples/edge-functions
궁금한 점이 있다면 댓글로 남기거나 Supabase Discord에서 커뮤니티에 직접 도움을 요청해 보세요.
Supabase Edge Functions 개발 및 배포 전체 과정
환경 구성부터 프로덕션 배포까지 이어지는 전체 작업 가이드
⏱️ Estimated time: 45 min
- 1
Step 1: Supabase CLI 설치 및 로그인
Homebrew로 CLI를 설치합니다(macOS):
```bash
brew install supabase/tap/supabase
supabase login
```
로그인하면 브라우저가 열리고 CLI의 Supabase 계정 접근 권한을 승인할 수 있습니다. - 2
Step 2: 프로젝트 초기화 및 함수 생성
프로젝트 디렉터리에서 초기화 명령을 실행한 다음 첫 번째 함수를 생성합니다:
```bash
supabase init
supabase functions new hello-world
```
그러면 `supabase/functions/` 디렉터리에 함수 템플릿이 생성됩니다. - 3
Step 3: 로컬 개발 환경 시작
먼저 PostgreSQL을 포함한 로컬 Supabase 스택을 시작한 다음 함수 서비스를 실행합니다:
```bash
supabase start
supabase functions serve --env-file supabase/.env.local
```
로컬 함수 주소: `http://localhost:54321/functions/v1/{function-name}` - 4
Step 4: Hono 프레임워크로 API 구축
Hono를 설치하고 RESTful API를 만듭니다:
```typescript
import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
const app = new Hono().basePath('/api')
app.use('*', cors())
app.get('/users/:id', (c) => {
return c.json({ user: { id: c.req.param('id') } })
})
Deno.serve(app.fetch)
```
Hono는 라우팅, 미들웨어, 매개변수 검증을 지원합니다. - 5
Step 5: 환경 변수와 Secrets 설정
로컬 개발에서는 `.env` 파일을, 프로덕션 환경에서는 Secrets를 사용합니다:
```bash
# 로컬
echo "MY_SECRET=value" > supabase/.env.local
# 프로덕션
supabase secrets set MY_SECRET=value
```
함수 안에서는 `Deno.env.get('MY_SECRET')`으로 값을 읽습니다. - 6
Step 6: 프로덕션 환경에 배포
함수를 배포하고 필요한 경우 공개 접근을 설정합니다:
```bash
# 표준 배포(JWT 검증 필요)
supabase functions deploy user-api
# 공개 API(JWT 검증 없음)
supabase functions deploy user-api --no-verify-jwt
```
프로덕션 URL 형식: `https://[PROJECT_ID].supabase.co/functions/v1/user-api`
FAQ
Supabase Edge Functions와 Cloudflare Workers는 무엇이 다른가요?
Edge Functions는 Webhook 처리에 적합한가요?
Deno와 Node.js의 패키지 관리는 어떻게 다른가요?
Edge Functions에서 Supabase 데이터베이스에 연결하려면 어떻게 해야 하나요?
Edge Functions에 실행 시간 제한이 있나요?
Edge Functions를 어떻게 디버깅하나요?
3분 읽기 · 게시일: 2026년 4월 19일 · 수정일: 2026년 9월 4일
Supabase 실전
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
Supabase Storage 실전 가이드: 파일 업로드, CDN, 액세스 제어
Supabase Storage 완전 실전 가이드입니다. 세 가지 액세스 제어 방식 비교, TUS 청크 업로드, Smart CDN 최적화 팁, R2/S3 비용 비교를 다루며 React 코드 예제와 문제 해결 방법도 제공합니다.
8편 중 6편
다음
Supabase Auth 심층 설정: OAuth, SSO 및 권한 제어
Supabase Auth 고급 설정을 자세히 설명합니다. OAuth 다중 Provider 연동, SAML SSO 기업 인증, RLS 멀티테넌트 권한 격리를 통해 소비자용 앱부터 기업용 SaaS까지 아우르는 완전한 인증 솔루션을 구성합니다.
8편 중 8편



댓글
GitHub로 로그인하여 댓글을 남기세요