Edge Function 배포 실패? (Deno 런타임을 이해 못 해서 생긴 일)
1. "API 키 숨기려고 Edge Function 썼는데..."
OpenAI API 키를 프론트엔드에 노출할 수 없어서, Supabase Edge Function을 썼습니다.
supabase functions serve로 로컬에서 테스트할 때는 완벽했습니다.
Postman으로 찔러봐도 200 OK가 잘 떨어졌죠.
"좋아, 배포하자!"
supabase functions deploy my-function
배포 성공 메시지가 떴고, 자신만만하게 프론트엔드랑 연동했습니다. 그런데...
500 Internal Server Error
{ "error": "Worker failed to start" }
로그를 보니 Uncaught Error: Import module "..." not found 같은 에러가 떴습니다.
"아니, 로컬에선 된다니까? 왜 배포하면 모듈을 못 찾아?"
2. 처음엔 뭐가 이해가 안 갔나? (Node.js인 줄 알았지)
저는 Supabase Edge Function이 그냥 "서버리스 Node.js"인 줄 알았습니다.
그래서 npm install axios 하고 import axios from 'axios'를 썼습니다.
하지만 Supabase Edge Function은 Deno 기반입니다. 그리고 Edge Runtime이라는 아주 제한적인 환경에서 돌아갑니다.
- Node.js가 아님:
fs,path,process같은 Node 내장 모듈이 없습니다. - npm 호환성: 최근에야
npm:프리픽스로 지원하기 시작했지만, 여전히 무거운 라이브러리는 엣지 환경에서 안 돌 수 있습니다. - 파일 시스템 없음: 서버에 파일을 저장하거나 읽을 수 없습니다.
무엇보다 "로컬에 있는 node_modules를 통째로 올려주는 게 아니다"라는 걸 몰랐습니다.
3. 어떤 포인트에서 이해가 됐나? (여행 가방 비유)
이걸 "국내 여행 vs 해외 배낭여행"으로 비유해봤습니다.
- Node.js (Serverful): 내 집(서버)에서 사는 겁니다. 필요한 게 있으면 그냥 마트(npm) 가서 사 오면 되고, 냉장고(File System)에 쟁여둬도 됩니다.
- Edge Function: 해외 배낭여행입니다. 짐(코드)을 아주 가볍게 싸야 합니다. "집에 있는 냉장고 가져가야지" -> 불가능합니다. "현지에서 사지 뭐(Dynamic Import)" -> 인터넷 느리면 망합니다.
배포(Deploy)는 배낭 하나만 달랑 메고 비행기 타는 겁니다. 그런데 배낭 속에 "여권(Import Map)"이나 "필수 의약품(Dependencies)"을 안 챙기고, "집에 책상 위에 뒀는데?"라고 해봤자 소용없는 거죠.
4. 해결 과정 - Deno 환경 맞추기
1단계 - import_map.json (혹은 deno.json)
Deno는 package.json이 없습니다. 대신 deno.json이나 import_map.json에서 의존성을 관리합니다.
로컬에서 잘 된 이유는 로컬 캐시에 의존성이 있었기 때문이고, 배포 환경은 텅 빈 새 장비라서 모듈을 못 찾은 겁니다.
/supabase/functions/deno.json을 만들고 명시해줘야 합니다.
{
"imports": {
"openai": "https://deno.land/x/openai@v4.20.1/mod.ts",
"cors": "https://deno.land/x/cors/mod.ts"
}
}
그리고 코드에서는 이렇게 씁니다:
// 절대 경로 URL 쓰지 말고, 별칭(Alias) 사용!
import OpenAI from 'openai';
2단계 - 환경 변수 (.env) 설정
로컬 .env 파일은 배포되지 않습니다.
배포된 함수에도 환경 변수를 따로 심어줘야 합니다.
# 로컬 .env 파일을 비밀 값으로 설정
supabase secrets set --env-file ./supabase/.env
이걸 안 하면 Deno.env.get('OPENAI_API_KEY')가 undefined가 되어 500 에러가 납니다.
3단계 - CORS 처리
프론트엔드(브라우저)에서 Edge Function을 호출하면 99% 확률로 CORS 에러가 납니다.
엣지 함수는 기본적으로 모든 요청을 거부하거든요.
응답 헤더에 Access-Control-Allow-Origin을 넣어줘야 합니다.
가장 쉬운 방법은 OPTIONS 요청(Preflight)을 처리해주는 겁니다.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
// Preflight 요청 대응
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// 본 로직...
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
5. 깊이 파고들기 - 왜 Deno인가?
"왜 익숙한 Node.js 안 쓰고 Deno를 쓰냐?" 불만이 생길 수 있습니다. 핵심은 Cold Start 시간과 보안입니다.
- Cold Start: Node.js는 무겁습니다. 함수 한 번 실행하려고 가상 머신 띄우는데 1초가 걸립니다. Deno(V8 Isolate)는 브라우저 탭 하나 띄우는 것만큼 가볍습니다. (수 밀리초)
- Web Standard: Deno는 브라우저 API(
fetch,Request,Response)를 그대로 씁니다. 즉, 프론트엔드 개발자가 서버 코드를 짤 때 위화감이 적습니다.axios대신fetch쓰면 됩니다.
6. 데이터베이스 연결 (Supabase Direct vs Pooler) 제대로 파보기
Edge Function에서 Supabase DB에 postgres.js 등으로 직접 붙으려고 하면 에러가 날 수 있습니다.
Edge Function은 수천 개가 동시에 뜰 수 있는데, DB 연결(Connection) 개수는 한정되어 있기 때문입니다(Connection Exhaustion).
해결책: Supaver(Connection Pooler) 사용
DIRECT_URL (포트 5432) 대신 DATABASE_URL (포트 6543, Transaction Mode)을 사용해야 합니다.
Supabase 프로젝트 설정에서 Connection Pooling이 켜져 있는지 확인하세요.
Cold Start와 런타임 제한 한 걸음 더
Edge Function은 CPU 제한과 메모리 제한이 빡빡합니다.
- CPU: 100ms ~ 500ms 이상 돌면 강제 종료될 수 있습니다. (Heavy Computation 금지)
- Memory: 128MB ~ 256MB. 큰 이미지를 처리하려고 하면 터집니다.
- Timeout: 기본 60초 (설정 변경 가능).
만약 비디오 인코딩 같은 무거운 작업을 해야 한다면? Edge Function은 틀렸습니다. 이건 AWS Lambda나 Google Cloud Run 같은 Serverless Containers로 가야 합니다. Edge Function은 "API 라우팅, 인증, 간단한 DB 조작" 같은 가벼운 작업용입니다.
8. Pro Tip: 로컬 환경 변수 관리 (Secret Management)
배포할 때마다 supabase secrets set을 치는 건 고역입니다.
특히 팀원들과 작업할 때는 실수하기 딱 좋습니다.
비법: .env 파일을 로컬과 운영으로 분리하세요.
.env.local: 로컬 테스트용 (Git Ignore).env.production: 배포용 (CI/CD 파이프라인에서 주입)
그리고 supabase/config.toml이나 배포 스크립트를 만들어서 관리해야 합니다.
Supabase CLI는 아직 .env의 자동 배포를 완벽히 지원하지 않으므로, 아래와 같은 스크립트를 package.json에 넣어두면 편합니다.
"scripts": {
"deploy:secrets": "supabase secrets set --env-file ./supabase/.env.production",
"deploy": "npm run deploy:secrets && supabase functions deploy"
}
사소하지만 배포 사고를 막는 중요한 습관입니다.
9. 디버깅 팁
"배포했는데 500 에러만 뜨고 로그를 모르겠어요!"
- Dashboard Logs: Supabase 대시보드의 'Edge Functions' -> 'Logs' 탭을 보세요. 실시간 로그가 나옵니다.
- Local Debugging:
supabase functions serve --debug이렇게 띄우면 Chrome DevTools(chrome://inspect)를 붙여서 브레이크포인트를 걸고 디버깅할 수 있습니다.
핵심 용어 정리
- Runtime: 코드가 실행되는 환경. Node.js Runtime, Deno Runtime, Edge Runtime 등이 있음.
- Isolate: V8 엔진의 격리된 인스턴스. 프로세스보다 훨씬 가볍고 빠름.
- WebAssembly (Wasm): 브라우저나 엣지에서 고성능(C++, Rust) 코드를 돌리기 위한 바이너리 포맷.
- Polyfill: 특정 환경에 없는 기능을 구현해서 채워 넣는 코드(예: Edge에 없는 Node API를 구현).
11. Application: AI Wrapper 만들기
Edge Function의 가장 흔한 용도는 OpenAI API Proxy입니다.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
const { query } = await req.json();
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: query }],
}),
});
const data = await response.json();
return new Response(JSON.stringify(data), { ... });
});
이렇게 하면 클라이언트는 내 API 키를 몰라도 AI 기능을 쓸 수 있습니다.
12. 한 줄 요약
Edge Function 배포 실패의 주범은 1. Import Map 누락, 2. 환경 변수 누락, 3. CORS 미처리다. Node.js가 아니라 '브라우저 같은 서버'라고 생각하고 짜야 한다.
Supabase Edge Function Failed? (Understanding Deno Runtime)
1. "Tried to Hide API Keys, Function Crashed"
I needed to hide my OpenAI API key, so I used a Supabase Edge Function to proxy the request.
Running supabase functions serve locally worked perfectly.
Postman returned 200 OK.
"Great, let's deploy!"
supabase functions deploy my-function
It said "Deployed successfully." I connected my frontend confidently. But then...
500 Internal Server Error
{ "error": "Worker failed to start" }
Logs showed Uncaught Error: Import module "..." not found.
"It works locally! Why can't it find the module in the cloud?"
2. What Confused Me Initially? (Thought it was Node.js)
I assumed Supabase Edge Function was just a "Serverless Node.js."
So I did npm install axios and import axios from 'axios'.
But Supabase Edge Function runs on Deno. And specifically, on the Edge Runtime, which is very restrictive.
- Not Node.js: No
fs,path,processmodules. - Semi-npm support:
npm:prefix support is recent, and heavy libraries might fail. - No File System: You can't read/write files to the server disk.
I didn't realize "It doesn't upload my node_modules folder to the cloud."
3. The 'Aha!' Moment (The Backpacking Analogy)
I compared Node.js vs. Edge to "Living at Home vs. Backpacking Abroad."
- Node.js (Serverful): Living in your own house. Need something? Go to the mart (npm) or get it from your fridge (File System).
- Edge Function: Backpacking trip. You must pack extremely light. "I'll bring my fridge" -> Impossible. "I'll buy it there (Dynamic Import)" -> Risky if internet is slow.
Deploying is jumping on a plane with just one backpack. If you forgot your passport (Import Map) or meds (Dependencies) and say "But I left them on my desk at home?", it's useless.
4. The Fix: Adapting to Deno
Step 1: import_map.json (or deno.json)
Deno doesn't use package.json. It uses deno.json or import_map.json.
Local works because of local cache. Cloud fails because it's a fresh machine without that cache.
Create /supabase/functions/deno.json:
{
"imports": {
"openai": "https://deno.land/x/openai@v4.20.1/mod.ts",
"cors": "https://deno.land/x/cors/mod.ts"
}
}
In code:
// Don't use raw URLs. Use the alias!
import OpenAI from 'openai';
Step 2: Environment Variables (.env)
Your local .env is NOT deployed automatically.
You must manually set secrets for the deployed function.
# Push local .env vars to production secrets
supabase secrets set --env-file ./supabase/.env
Without this, Deno.env.get('OPENAI_API_KEY') returns undefined, causing 500 errors.
Step 3: Handling CORS
Calling Edge Functions from a browser triggers CORS errors 99% of the time.
Edge functions block all requests by default.
You must insert Access-Control-Allow-Origin headers manually.
Handle the OPTIONS (Preflight) request explicitly:
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
// Handle Preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Logic...
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})
5. Deep Dive: Why Deno?
"Why Deno instead of the familiar Node.js?" The answer is Cold Start Time and Security.
- Cold Start: Node.js is heavy. Spinning up a VM takes ~1s. Deno (V8 Isolate) is as light as opening a browser tab (milliseconds).
- Web Standard: Deno uses browser APIs (
fetch,Request,Response). Frontend devs feel right at home. No need foraxios; just usefetch.
6. Deep Dive: Database Connection Pooling
Trying to connect to Postgres directly (postgres.js)?
You will hit the Connection Limit quickly because Edge Functions scale to thousands.
Solution: Use the Supabase Pooler (Supaver).
Use the DATABASE_URL (Port 6543) instead of DIRECT_URL (Port 5432).
Ensure Transaction Mode is enabled in Supabase Project Settings.
7. Deep Dive: Cold Start and Limitations
Edge Functions are strict.
- CPU: Tasks > 100ms-500ms might be killed. No heavy computing!
- Memory: 128MB ~ 256MB. Processing 4K images? OOM Crash.
- Timeout: 60s max.
If you need video encoding or ML inference, use Serverless Containers (AWS Lambda / Google Cloud Run). Edge is for "Routing, Auth, Simple Logic."
8. Pro Tip: Local Secrets Management
Updating secrets with supabase secrets set every time is painful and error-prone.
Secret: Split .env files.
.env.local: Local testing (Git Ignored).env.production: Deployment (Injected via CI/CD)
Use a script in package.json:
"scripts": {
"deploy:secrets": "supabase secrets set --env-file ./supabase/.env.production",
"deploy": "npm run deploy:secrets && supabase functions deploy"
}
This prevents deployment accidents.
9. Debugging Tips
"Deployed, but getting 500 error!"
- Dashboard Logs: Check 'Edge Functions' -> 'Logs' in Supabase.
- Local Debugging:
supabase functions serve --debugUse Chrome DevTools (chrome://inspect) to attach to the Deno runtime and set breakpoints.
10. Deep Dive Glossary
- Runtime: Environment where code runs. Node, Deno, Edge.
- Isolate: V8 instance. Lighter than a process (Node.js).
- WebAssembly (Wasm): Binary format for running C++/Rust code in browser/edge.
- Polyfill: Code that implements missing features.
11. Application: AI Wrapper
The most common use case is an OpenAI API Proxy:
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
const { query } = await req.json();
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: query }],
}),
});
const data = await response.json();
return new Response(JSON.stringify(data), { ... });
});
This lets clients use AI features without exposing your private keys.