[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,432 @@
|
||||
---
|
||||
id: backend-edge-functions
|
||||
title: Edge Functions — Cloudflare / Vercel / Deno Deploy
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, edge, serverless, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [Cloudflare Workers, Vercel Edge, Deno Deploy, edge runtime, V8 isolate, Wasm edge]
|
||||
---
|
||||
|
||||
# Edge Functions
|
||||
|
||||
> 사용자 가까이 (300+ region) 실행. **Cloudflare Workers / Vercel Edge / Deno Deploy / Fastly Compute@Edge**. V8 isolate (cold start ms), 작은 limit.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- V8 Isolate: process 안 — 매 request fast.
|
||||
- Web Standard: Request / Response / fetch.
|
||||
- Limits: CPU / memory / time 작음.
|
||||
- Storage: KV / D1 / Durable Object / R2.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Cloudflare Workers
|
||||
```ts
|
||||
// src/index.ts
|
||||
export interface Env {
|
||||
DB: D1Database;
|
||||
CACHE: KVNamespace;
|
||||
BUCKET: R2Bucket;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === '/api/users/me') {
|
||||
const userId = await getUserId(req);
|
||||
|
||||
const cached = await env.CACHE.get(`user:${userId}`, { type: 'json' });
|
||||
if (cached) return Response.json(cached);
|
||||
|
||||
const user = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first();
|
||||
ctx.waitUntil(env.CACHE.put(`user:${userId}`, JSON.stringify(user), { expirationTtl: 60 }));
|
||||
|
||||
return Response.json(user);
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
name = "my-api"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-12-01"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "my-app"
|
||||
database_id = "..."
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "CACHE"
|
||||
id = "..."
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "BUCKET"
|
||||
bucket_name = "uploads"
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
```bash
|
||||
wrangler dev
|
||||
wrangler deploy
|
||||
```
|
||||
|
||||
### Vercel Edge Function
|
||||
```ts
|
||||
// app/api/users/route.ts
|
||||
import { type NextRequest } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const id = req.nextUrl.searchParams.get('id');
|
||||
return Response.json({ id });
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// 또는 standalone
|
||||
// pages/api/edge.ts
|
||||
export const config = { runtime: 'edge' };
|
||||
|
||||
export default function handler(req: Request) {
|
||||
return new Response('Hello from edge');
|
||||
}
|
||||
```
|
||||
|
||||
### Deno Deploy
|
||||
```ts
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono();
|
||||
app.get('/', (c) => c.text('Hello from Deno Deploy'));
|
||||
|
||||
Deno.serve(app.fetch);
|
||||
```
|
||||
|
||||
```bash
|
||||
deployctl deploy --project=my-app src/index.ts
|
||||
```
|
||||
|
||||
### Bun on edge (Fly.io / Railway)
|
||||
```
|
||||
Bun = full Node API + Web Standard.
|
||||
Fly / Railway 가 Bun runtime 지원.
|
||||
Edge X but 가까운 region.
|
||||
```
|
||||
|
||||
### KV (Cloudflare)
|
||||
```ts
|
||||
// 빠른 read (eventually consistent globally)
|
||||
await env.KV.put('key', 'value', { expirationTtl: 3600 });
|
||||
const v = await env.KV.get('key');
|
||||
const json = await env.KV.get('key', { type: 'json' });
|
||||
|
||||
// List
|
||||
const list = await env.KV.list({ prefix: 'user:' });
|
||||
|
||||
// Stream large
|
||||
const stream = await env.KV.get('large-file', { type: 'stream' });
|
||||
```
|
||||
|
||||
→ Read 빠름 (각 region cache), write 글로벌 propagate (1-60s).
|
||||
|
||||
### D1 (SQLite at edge)
|
||||
```ts
|
||||
const r = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
|
||||
.bind('a@b.com')
|
||||
.first();
|
||||
|
||||
// Multi
|
||||
const all = await env.DB.prepare('SELECT * FROM users WHERE status = ?')
|
||||
.bind('active')
|
||||
.all();
|
||||
|
||||
// Batch (transaction)
|
||||
await env.DB.batch([
|
||||
env.DB.prepare('INSERT INTO users VALUES (?, ?)').bind(id1, email1),
|
||||
env.DB.prepare('INSERT INTO users VALUES (?, ?)').bind(id2, email2),
|
||||
]);
|
||||
```
|
||||
|
||||
### Durable Objects (글로벌 state)
|
||||
```ts
|
||||
// Counter — 한 instance per name, 글로벌 단일
|
||||
export class Counter {
|
||||
state: DurableObjectState;
|
||||
|
||||
constructor(state: DurableObjectState) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
let count = (await this.state.storage.get<number>('count')) ?? 0;
|
||||
count++;
|
||||
await this.state.storage.put('count', count);
|
||||
return Response.json({ count });
|
||||
}
|
||||
}
|
||||
|
||||
// Worker
|
||||
export default {
|
||||
async fetch(req: Request, env: Env) {
|
||||
const url = new URL(req.url);
|
||||
const name = url.searchParams.get('room') ?? 'default';
|
||||
const id = env.COUNTER.idFromName(name);
|
||||
const stub = env.COUNTER.get(id);
|
||||
return stub.fetch(req);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
→ Stateful — chat room, game session, rate limit.
|
||||
|
||||
### R2 (S3-compatible storage)
|
||||
```ts
|
||||
const obj = await env.BUCKET.get('photo.jpg');
|
||||
if (obj) return new Response(obj.body, { headers: { 'Content-Type': obj.httpMetadata?.contentType ?? '' } });
|
||||
|
||||
await env.BUCKET.put('upload.jpg', file, {
|
||||
httpMetadata: { contentType: 'image/jpeg' },
|
||||
});
|
||||
|
||||
await env.BUCKET.delete('old.jpg');
|
||||
```
|
||||
|
||||
→ S3-compat + free egress.
|
||||
|
||||
### Cron triggers
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[triggers]
|
||||
crons = ["0 9 * * *"] # 매일 9시
|
||||
```
|
||||
|
||||
```ts
|
||||
export default {
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
|
||||
await runDailyTask(env);
|
||||
},
|
||||
|
||||
async fetch(req: Request, env: Env) { ... },
|
||||
};
|
||||
```
|
||||
|
||||
### Queues (Cloudflare)
|
||||
```ts
|
||||
// Producer
|
||||
await env.QUEUE.send({ orderId: '...', userId: '...' });
|
||||
|
||||
// Consumer
|
||||
export default {
|
||||
async queue(batch: MessageBatch, env: Env) {
|
||||
for (const msg of batch.messages) {
|
||||
await processOrder(msg.body);
|
||||
msg.ack();
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
→ Decouple.
|
||||
|
||||
### Limits (대략)
|
||||
```
|
||||
Cloudflare Workers:
|
||||
- CPU: 30s (paid) / 10ms (free) per request
|
||||
- Memory: 128 MB
|
||||
- Subrequests: 1000
|
||||
- Bundle: 10 MB
|
||||
- Compute units / month: $5 = 10M+
|
||||
|
||||
Vercel Edge:
|
||||
- CPU: 30s
|
||||
- Memory: 128 MB
|
||||
- Bundle: 1 MB
|
||||
|
||||
Deno Deploy:
|
||||
- CPU: 50ms (per request)
|
||||
- Memory: 512 MB
|
||||
```
|
||||
|
||||
→ Long-running task = 다른 (Lambda / VM).
|
||||
|
||||
### Edge 의 함정
|
||||
```
|
||||
1. CPU limit (10ms free) — 큰 work X.
|
||||
2. Bundle size — Node module 일부 X.
|
||||
3. Cold start — 거의 0 (V8 isolate).
|
||||
4. Connection pool 어려움 (no persistent state).
|
||||
5. 일부 Node API X (fs, child_process).
|
||||
```
|
||||
|
||||
→ HTTP / KV / D1 만 사용.
|
||||
|
||||
### Use cases (적합)
|
||||
```
|
||||
- API gateway (auth, rate limit, route)
|
||||
- A/B test, geo redirect
|
||||
- Image / response transformation
|
||||
- Analytics ingestion
|
||||
- Search index 호출
|
||||
- Cache layer
|
||||
- Webhook receiver
|
||||
- Static site SSR
|
||||
```
|
||||
|
||||
### Use cases (안 적합)
|
||||
```
|
||||
- 큰 ML inference
|
||||
- Long task (1 min+)
|
||||
- Persistent connection (DB pool)
|
||||
- File system 의존
|
||||
- Large dependencies (Node-specific)
|
||||
```
|
||||
|
||||
### Multi-region database
|
||||
```
|
||||
Edge function 가 사용자 가까이.
|
||||
DB 가 single region = 큰 latency.
|
||||
|
||||
해결:
|
||||
- Read replica per region
|
||||
- Hyperdrive (CF cache)
|
||||
- Turso embedded replica
|
||||
- 분산 DB (Spanner, Yugabyte)
|
||||
```
|
||||
|
||||
### Auth at edge
|
||||
```ts
|
||||
import { jwt } from 'hono/jwt';
|
||||
|
||||
app.use('/api/*', jwt({ secret: env.JWT_SECRET }));
|
||||
|
||||
// 또는 직접
|
||||
async function verifyJwt(token: string, secret: string) {
|
||||
const [header, payload, signature] = token.split('.');
|
||||
// JWT verify (jose 같은 lib)
|
||||
return JSON.parse(atob(payload));
|
||||
}
|
||||
```
|
||||
|
||||
### Static + Edge function
|
||||
```
|
||||
Vercel / Cloudflare Pages:
|
||||
- Static assets — CDN
|
||||
- API routes — edge function
|
||||
|
||||
→ Most modern stack.
|
||||
```
|
||||
|
||||
### Streaming
|
||||
```ts
|
||||
export default {
|
||||
async fetch() {
|
||||
const { readable, writable } = new TransformStream();
|
||||
const writer = writable.getWriter();
|
||||
|
||||
(async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await writer.write(new TextEncoder().encode(`chunk ${i}\n`));
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
}
|
||||
writer.close();
|
||||
})();
|
||||
|
||||
return new Response(readable);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
→ SSE / streaming response.
|
||||
|
||||
### Test (local)
|
||||
```bash
|
||||
wrangler dev # local + miniflare (Cloudflare emulator)
|
||||
vercel dev
|
||||
deno run --watch src/index.ts
|
||||
```
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
wrangler deploy --env production
|
||||
vercel --prod
|
||||
deployctl deploy --prod
|
||||
```
|
||||
|
||||
### Cost
|
||||
```
|
||||
Cloudflare Workers:
|
||||
Free: 100K req/day
|
||||
Paid: $5/month + $0.50 per million
|
||||
|
||||
Vercel:
|
||||
Hobby: free
|
||||
Pro: $20/month + execution time
|
||||
```
|
||||
|
||||
→ 가장 cheap edge.
|
||||
|
||||
### Comparison
|
||||
```
|
||||
Cloudflare:
|
||||
+ 가장 빠름 (V8 isolate)
|
||||
+ KV / D1 / R2 통합
|
||||
+ Free tier 강
|
||||
- Node API 제한
|
||||
|
||||
Vercel:
|
||||
+ Next.js 통합 (best)
|
||||
+ Frontend / API 통합
|
||||
- 비싸 (큰 traffic)
|
||||
|
||||
Deno Deploy:
|
||||
+ Deno native
|
||||
+ Web Standard
|
||||
- Smaller ecosystem
|
||||
|
||||
Fastly Compute@Edge:
|
||||
+ Wasm 지원
|
||||
+ 큰 enterprise
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 빠른 API + 글로벌 | Cloudflare Workers |
|
||||
| Next.js | Vercel Edge |
|
||||
| Deno project | Deno Deploy |
|
||||
| Wasm | Fastly / CF Workers |
|
||||
| Long task | Lambda / VM |
|
||||
| Big data | Container / VM |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Edge 안 long task**: timeout.
|
||||
- **Big bundle (큰 dep)**: limit.
|
||||
- **Node-specific (fs, net)**: 깨짐.
|
||||
- **DB persistent connection**: HTTP driver.
|
||||
- **Edge 가 모든 답**: 가까운 user 가 critical 시만.
|
||||
- **State in memory**: cold isolate 에 잃음. KV / DO.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Cloudflare Workers + D1 + KV = 가장 강.
|
||||
- Vercel Edge + Next.js = best DX.
|
||||
- Web Standard API only.
|
||||
- Cold start 거의 0.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_Hono_Modern]]
|
||||
- [[DB_Serverless_Edge]]
|
||||
- [[Backend_Geo_Replication]]
|
||||
Reference in New Issue
Block a user