Deno 런타임의 동작 원리를 이해하고, 웹훅·백그라운드 작업·테스트·로깅까지 실전 서버리스 패턴을 다룬다.
Edge Functions 아키텍처와 테스트·디버깅
입문편에서는 Edge Function을 만들고 배포하여 클라이언트에서 호출하는 기본 흐름을 배웠습니다. 하지만 실제 운영 환경에서는 그보다 훨씬 많은 것을 고민해야 합니다. 콜드 스타트가 얼마나 자주 발생하는지, 웹훅 요청의 진위를 어떻게 검증하는지, 함수 코드가 복잡해질수록 어떻게 테스트하고 디버깅할 것인지, 로그를 어디서 어떻게 확인하는지가 바로 그것입니다.
이 강의에서는 Deno 런타임의 실행 모델을 깊이 이해하는 것부터 시작해, 웹훅 수신과 검증, 데이터베이스 트리거 연동, 단위·통합 테스트, 구조적 로깅과 관측(observability), 그리고 재사용 가능한 프로젝트 구조 설계까지 Edge Functions를 실무에서 안정적으로 운영하는 데 필요한 심화 패턴을 다룹니다.
학습 목표
- Deno 런타임의 실행 수명주기와 콜드 스타트 특성을 이해하고 대응 전략을 세울 수 있다.
- 웹훅 서명 검증을 구현해 외부 이벤트를 안전하게 수신할 수 있다.
- pg_cron과 데이터베이스 트리거를 이용해 함수를 비동기적으로 호출하는 패턴을 구현할 수 있다.
supabase functions serve를 활용한 로컬 디버깅과 단위/통합 테스트를 작성할 수 있다.- 구조적 로깅과 에러 핸들링으로 운영 환경에서 함수의 관측 가능성을 높일 수 있다.
Deno 런타임과 콜드 스타트 이해
Supabase Edge Functions는 Deno Deploy 위에서 실행됩니다. Deno는 V8 엔진 기반의 TypeScript 네이티브 런타임으로, Node와 달리 node_modules가 없고 URL 또는 npm: 스킴으로 모듈을 임포트합니다.
함수 실행 수명주기
함수가 처음 호출될 때 Deno 런타임이 초기화되는 과정을 **콜드 스타트(cold start)**라고 합니다. 이 단계에서는 런타임 부트, 모듈 다운로드·컴파일, 전역 스코프 초기화가 순차적으로 일어납니다. 이후 동일 인스턴스가 재사용되는 웜 스타트(warm start) 요청은 수 밀리초 내에 처리됩니다.
[콜드 스타트]
런타임 부트 (~50~200ms)
→ 모듈 컴파일 (파일 크기에 비례)
→ 전역 코드 실행 (DB 커넥션 풀 생성 등)
→ 첫 번째 요청 처리
[웜 스타트]
기존 인스턴스 재사용 → 즉시 요청 처리
⚠️ 주의 — 전역 스코프에서 무거운 초기화(외부 API 초기 연결, 대용량 데이터 로드)를 수행하면 콜드 스타트 시간이 급격히 늘어납니다. 반드시 필요한 경우에만 전역 초기화를 사용하세요.
콜드 스타트를 최소화하는 핵심 원칙은 모듈 크기 줄이기와 **지연 초기화(lazy initialization)**입니다.
// ❌ 콜드 스타트 악화: 전역에서 무거운 모듈 즉시 로드
import { createClient } from 'npm:@supabase/supabase-js@2';
import Stripe from 'npm:stripe@14';
const stripe = new Stripe(Deno.env.get('STRIPE_KEY')!); // 매 인스턴스마다 즉시 초기화
Deno.serve(async (req) => {
// ...
});
// ✅ 지연 초기화: 실제 필요한 시점에 인스턴스 생성
import { createClient } from 'npm:@supabase/supabase-js@2';
let stripeInstance: any = null;
async function getStripe() {
if (!stripeInstance) {
const { default: Stripe } = await import('npm:stripe@14');
stripeInstance = new Stripe(Deno.env.get('STRIPE_KEY')!);
}
return stripeInstance;
}
Deno.serve(async (req) => {
const stripe = await getStripe();
// ...
});
요청 격리와 상태 공유
같은 인스턴스에서 여러 요청이 연속으로 처리될 때, 전역 변수는 요청 간에 공유됩니다. 이는 캐시로 활용할 수 있는 반면, 요청별로 초기화되어야 할 상태가 오염되는 버그의 원인이 되기도 합니다.
// ❌ 요청별로 초기화되어야 하는 상태를 전역에 두면 오염됨
const requestLog: string[] = []; // 모든 요청이 같은 배열을 공유
Deno.serve(async (req) => {
requestLog.push(req.url); // 이전 요청의 로그가 남아 있음
return new Response(JSON.stringify(requestLog));
});
// ✅ 요청 스코프 내에서 상태를 관리
Deno.serve(async (req) => {
const requestLog: string[] = [];
requestLog.push(req.url);
return new Response(JSON.stringify(requestLog));
});
웹훅 수신과 서명 검증
Stripe, GitHub, Slack 등 외부 서비스는 이벤트가 발생하면 우리 엔드포인트로 HTTP POST 요청을 보냅니다. 이를 **웹훅(webhook)**이라고 하며, 누구나 이 URL로 가짜 페이로드를 보낼 수 있기 때문에 반드시 서명 검증을 구현해야 합니다.
HMAC 서명 검증 패턴
대부분의 서비스는 공유 시크릿과 HMAC-SHA256을 이용해 페이로드에 서명합니다. Deno의 Web Crypto API로 직접 검증할 수 있습니다.
// supabase/functions/stripe-webhook/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2';
const STRIPE_WEBHOOK_SECRET = Deno.env.get('STRIPE_WEBHOOK_SECRET')!;
// 두 hex 문자열을 상수시간(constant-time)으로 비교한다.
// 일반 === 비교는 첫 불일치에서 조기 종료되어 타이밍 공격에 취약하므로,
// 서명 검증에서는 반드시 입력에 무관하게 동일한 시간이 걸리는 비교를 사용한다.
function timingSafeEqualHex(a: string, b: string): boolean {
// 길이가 다르면 즉시 false (길이 자체는 비밀이 아님)
if (a.length !== b.length) {
return false;
}
let mismatch = 0;
for (let i = 0; i < a.length; i++) {
// 모든 문자를 끝까지 순회하며 XOR 결과를 누적
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return mismatch === 0;
}
async function verifyStripeSignature(
body: string,
signature: string,
secret: string,
): Promise<boolean> {
// Stripe 서명 형식: t=타임스탬프,v1=해시
const parts = Object.fromEntries(
signature.split(',').map((p) => p.split('=')),
);
const timestamp = parts['t'];
const expectedHash = parts['v1'];
// 5분 이상 지난 요청 거부 (replay attack 방지)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
const signedPayload = `${timestamp}.${body}`;
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const signatureBuffer = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signedPayload),
);
const computedHash = Array.from(new Uint8Array(signatureBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
// 상수시간 비교로 타이밍 공격 방지
return timingSafeEqualHex(computedHash, expectedHash);
}
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
const signature = req.headers.get('stripe-signature');
if (!signature) {
return new Response('Missing signature', { status: 400 });
}
// body를 텍스트로 읽어야 서명 검증이 가능 (JSON.parse 전에 수행)
const body = await req.text();
const isValid = await verifyStripeSignature(body, signature, STRIPE_WEBHOOK_SECRET);
if (!isValid) {
return new Response('Invalid signature', { status: 401 });
}
const event = JSON.parse(body);
// 이벤트 타입별 처리
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
await handleCheckoutCompleted(session);
break;
}
case 'customer.subscription.deleted': {
await handleSubscriptionDeleted(event.data.object);
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), {
headers: { 'Content-Type': 'application/json' },
});
});
async function handleCheckoutCompleted(session: any) {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
await supabase.from('orders').insert({
stripe_session_id: session.id,
customer_email: session.customer_email,
amount: session.amount_total,
status: 'completed',
});
}
💡 TIP —
req.text()로 원본 바이트를 읽은 후 서명을 검증하고, 그 다음JSON.parse(body)로 파싱하세요.req.json()을 먼저 호출하면 body 스트림이 소비되어 이후req.text()호출이 불가능합니다.
⚠️ 보안 주의 — 계산한 해시와 전달받은 해시를
===로 비교하면 첫 불일치 지점에서 조기 종료되어, 비교에 걸리는 시간이 일치하는 접두사 길이에 따라 달라집니다. 공격자는 이 미세한 시간 차이를 측정해 올바른 서명을 한 바이트씩 추측할 수 있습니다(타이밍 공격). 위 예시의timingSafeEqualHex처럼 입력에 무관하게 항상 끝까지 순회하는 상수시간 비교를 사용하세요. Deno에서는 표준 라이브러리의timingSafeEqual(https://deno.land/std@0.224.0/crypto/timing_safe_equal.ts)을 바이트 배열에 적용해도 됩니다.
시크릿 관리와 환경 분리
함수에서 사용하는 민감한 값은 모두 Supabase 시크릿으로 관리합니다. 로컬 개발과 프로덕션 환경의 시크릿을 명확히 분리하는 것이 중요합니다.
# 프로덕션 시크릿 등록
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxx
supabase secrets set SENDGRID_API_KEY=SG.xxx
supabase secrets set INTERNAL_API_SECRET=some-random-string
# 등록된 시크릿 목록 확인
supabase secrets list
로컬 개발 시에는 .env.local 파일을 사용합니다. 이 파일은 반드시 .gitignore에 추가해야 합니다.
# supabase/.env.local
STRIPE_WEBHOOK_SECRET=whsec_test_xxx
SENDGRID_API_KEY=SG.test_xxx
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# 로컬 실행 시 .env.local 자동 로드
supabase functions serve --env-file supabase/.env.local
함수 내부에서 필요한 환경변수가 없을 때 명확한 에러를 발생시키는 헬퍼 패턴을 사용하면 운영 시 디버깅이 쉬워집니다.
// supabase/functions/_shared/config.ts
function requireEnv(key: string): string {
const value = Deno.env.get(key);
if (!value) {
throw new Error(`Required environment variable "${key}" is not set`);
}
return value;
}
export const config = {
supabaseUrl: requireEnv('SUPABASE_URL'),
serviceRoleKey: requireEnv('SUPABASE_SERVICE_ROLE_KEY'),
stripeWebhookSecret: requireEnv('STRIPE_WEBHOOK_SECRET'),
};
데이터베이스 트리거와 pg_cron으로 함수 비동기 호출
Edge Functions는 HTTP 호출뿐 아니라 데이터베이스 이벤트나 스케줄에 의해서도 트리거될 수 있습니다.
pg_net으로 트리거에서 함수 호출
pg_net 확장을 사용하면 PostgreSQL 트리거 내부에서 HTTP 요청을 비동기로 발송할 수 있습니다. 이를 통해 특정 테이블에 레코드가 삽입될 때 자동으로 Edge Function을 호출하는 패턴을 구현할 수 있습니다.
-- pg_net 확장 활성화 (Supabase 대시보드 > Extensions에서도 가능)
create extension if not exists pg_net;
-- 새 주문이 생성되면 Edge Function을 비동기 호출하는 트리거 함수
create or replace function notify_order_created()
returns trigger as $$
declare
payload jsonb;
begin
payload := jsonb_build_object(
'order_id', NEW.id,
'customer_id', NEW.customer_id,
'amount', NEW.amount
);
-- pg_net으로 비동기 HTTP POST
perform net.http_post(
url := current_setting('app.edge_function_url') || '/process-order',
body := payload::text,
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.service_role_key')
)
);
return NEW;
end;
$$ language plpgsql security definer;
create trigger on_order_created
after insert on orders
for each row execute function notify_order_created();
⚠️ 주의 —
current_setting('app.edge_function_url')처럼 애플리케이션 설정을 PostgreSQL 설정으로 주입하는 방식은 Supabase 대시보드의alter system또는 마이그레이션 파일에서 설정할 수 있습니다. 프로덕션에서는 Vault 기능을 이용해 민감한 값을 안전하게 저장하세요.
pg_cron으로 주기적 함수 실행
pg_cron을 이용하면 특정 주기로 Edge Function을 호출하거나 SQL 작업을 실행하는 스케줄을 등록할 수 있습니다.
-- pg_cron 확장 활성화
create extension if not exists pg_cron;
-- 매일 자정에 통계 집계 함수 호출 (cron 표현식 사용)
select cron.schedule(
'nightly-stats-aggregation', -- 잡 이름
'0 0 * * *', -- cron 표현식: 매일 00:00 UTC
$$
select net.http_post(
url := 'https://<project-ref>.supabase.co/functions/v1/aggregate-stats',
headers := '{"Authorization": "Bearer <service-role-key>", "Content-Type": "application/json"}'::jsonb,
body := '{"triggered_by": "pg_cron"}'::jsonb
);
$$
);
-- 등록된 잡 확인
select * from cron.job;
-- 잡 실행 이력 확인
select * from cron.job_run_details order by start_time desc limit 10;
-- 잡 삭제
select cron.unschedule('nightly-stats-aggregation');
함수 테스트와 로컬 디버깅
단위 테스트: Deno.test 활용
Deno는 별도 테스트 프레임워크 없이 내장 Deno.test를 사용합니다. 순수 함수(비즈니스 로직)는 HTTP 핸들러와 분리하여 테스트하기 쉬운 형태로 작성하는 것이 중요합니다.
// supabase/functions/_shared/utils.ts
export function calculateDiscount(price: number, discountPercent: number): number {
if (discountPercent < 0 || discountPercent > 100) {
throw new RangeError('discountPercent must be between 0 and 100');
}
return Math.round(price * (1 - discountPercent / 100));
}
export function parseWebhookEvent(body: string): { type: string; data: unknown } {
const parsed = JSON.parse(body);
if (!parsed.type || !parsed.data) {
throw new TypeError('Invalid webhook payload: missing type or data');
}
return parsed;
}
// supabase/functions/_shared/utils_test.ts
import { assertEquals, assertThrows } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { calculateDiscount, parseWebhookEvent } from './utils.ts';
Deno.test('calculateDiscount: 10% 할인 적용', () => {
assertEquals(calculateDiscount(10000, 10), 9000);
});
Deno.test('calculateDiscount: 100% 할인 시 0원 반환', () => {
assertEquals(calculateDiscount(10000, 100), 0);
});
Deno.test('calculateDiscount: 범위 초과 시 에러 발생', () => {
assertThrows(
() => calculateDiscount(10000, 110),
RangeError,
'discountPercent must be between 0 and 100',
);
});
Deno.test('parseWebhookEvent: 유효한 페이로드 파싱', () => {
const result = parseWebhookEvent('{"type":"order.created","data":{"id":1}}');
assertEquals(result.type, 'order.created');
});
Deno.test('parseWebhookEvent: 잘못된 페이로드 시 에러 발생', () => {
assertThrows(
() => parseWebhookEvent('{"noType":true}'),
TypeError,
);
});
# 테스트 실행
deno test --allow-env supabase/functions/_shared/utils_test.ts
# 전체 테스트 실행
deno test --allow-all supabase/functions/
통합 테스트: supabase functions serve와 fetch
HTTP 핸들러 전체를 테스트하려면 supabase functions serve로 함수를 로컬에 띄운 후 실제 HTTP 요청을 보내는 방식을 사용합니다.
// supabase/functions/stripe-webhook/index_test.ts
// 이 테스트는 `supabase functions serve`가 실행 중인 상태에서 동작합니다.
import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts';
const BASE_URL = 'http://localhost:54321/functions/v1';
async function signPayload(body: string, secret: string): Promise<string> {
const timestamp = Math.floor(Date.now() / 1000).toString();
const signedPayload = `${timestamp}.${body}`;
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signedPayload));
const hash = Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return `t=${timestamp},v1=${hash}`;
}
Deno.test('stripe-webhook: 유효한 서명으로 200 응답', async () => {
const body = JSON.stringify({ type: 'checkout.session.completed', data: { object: { id: 'cs_test_1' } } });
const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET')!;
const signature = await signPayload(body, secret);
const response = await fetch(`${BASE_URL}/stripe-webhook`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'stripe-signature': signature,
},
body,
});
assertEquals(response.status, 200);
});
Deno.test('stripe-webhook: 서명 없이 400 응답', async () => {
const response = await fetch(`${BASE_URL}/stripe-webhook`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"type":"test"}',
});
assertEquals(response.status, 400);
});
💡 TIP —
supabase functions serve --env-file supabase/.env.local --inspect-mode brk옵션을 사용하면 Chrome DevTools(chrome://inspect)에서 중단점을 설정하고 단계별로 코드를 실행할 수 있습니다.
구조적 로깅, 에러 처리와 관측
운영 환경에서 함수가 오동작할 때 원인을 빠르게 파악하려면 구조적 로깅과 일관된 에러 처리가 필수입니다.
구조적 로깅 패턴
console.log에 평문 문자열 대신 JSON 객체를 출력하면 Supabase 대시보드의 로그 뷰어에서 필드별 필터링이 가능합니다.
// supabase/functions/_shared/logger.ts
type LogLevel = 'info' | 'warn' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
timestamp: string;
[key: string]: unknown;
}
export function log(level: LogLevel, message: string, context?: Record<string, unknown>) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
...context,
};
// Supabase 로그 뷰어는 console.log 출력을 수집합니다.
console.log(JSON.stringify(entry));
}
export const logger = {
info: (msg: string, ctx?: Record<string, unknown>) => log('info', msg, ctx),
warn: (msg: string, ctx?: Record<string, unknown>) => log('warn', msg, ctx),
error: (msg: string, ctx?: Record<string, unknown>) => log('error', msg, ctx),
};
// 함수에서 구조적 로거 사용
import { logger } from '../_shared/logger.ts';
Deno.serve(async (req) => {
const requestId = crypto.randomUUID();
const startTime = Date.now();
logger.info('Request received', {
requestId,
method: req.method,
url: req.url,
});
try {
const result = await processRequest(req);
logger.info('Request completed', {
requestId,
durationMs: Date.now() - startTime,
});
return new Response(JSON.stringify(result), {
headers: { 'Content-Type': 'application/json' },
});
} catch (err) {
logger.error('Request failed', {
requestId,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
durationMs: Date.now() - startTime,
});
// 클라이언트에게는 내부 세부사항을 노출하지 않음
return new Response(
JSON.stringify({ error: 'Internal server error', requestId }),
{ status: 500, headers: { 'Content-Type': 'application/json' } },
);
}
});
에러 분류와 HTTP 상태 코드
비즈니스 에러와 시스템 에러를 구분하면 클라이언트가 적절히 대응할 수 있습니다.
// supabase/functions/_shared/errors.ts
export class AppError extends Error {
constructor(
public readonly statusCode: number,
public readonly code: string,
message: string,
) {
super(message);
this.name = 'AppError';
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(400, 'VALIDATION_ERROR', message);
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(401, 'UNAUTHORIZED', message);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(404, 'NOT_FOUND', `${resource} not found`);
}
}
// 에러를 HTTP 응답으로 변환하는 헬퍼
export function errorResponse(err: unknown): Response {
if (err instanceof AppError) {
return new Response(
JSON.stringify({ error: err.code, message: err.message }),
{ status: err.statusCode, headers: { 'Content-Type': 'application/json' } },
);
}
// 예상치 못한 에러는 500으로 처리
console.error('Unexpected error:', err);
return new Response(
JSON.stringify({ error: 'INTERNAL_ERROR', message: 'An unexpected error occurred' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } },
);
}
프로젝트 구조와 공유 모듈 설계
함수가 늘어날수록 중복 코드가 문제가 됩니다. _shared 디렉터리 아래 공통 모듈을 배치하는 패턴이 Supabase 공식 권장 방식입니다.
supabase/
functions/
_shared/ # 공유 모듈 (배포 대상 아님)
config.ts # 환경변수 로드
supabase-client.ts # Supabase 클라이언트 팩토리
logger.ts # 구조적 로거
errors.ts # 커스텀 에러 클래스
auth.ts # JWT 검증 헬퍼
stripe-webhook/
index.ts
index_test.ts
process-order/
index.ts
index_test.ts
aggregate-stats/
index.ts
.env.local # 로컬 개발용 (gitignore)
config.toml
공유 Supabase 클라이언트 팩토리 예시입니다. 사용자 컨텍스트(RLS 적용)와 서비스 롤(RLS 우회) 클라이언트를 명확히 구분합니다.
// supabase/functions/_shared/supabase-client.ts
import { createClient, SupabaseClient } from 'npm:@supabase/supabase-js@2';
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const serviceRoleKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
// RLS를 우회하는 관리자 클라이언트 (웹훅, 배치 작업에 사용)
export function getAdminClient(): SupabaseClient {
return createClient(supabaseUrl, serviceRoleKey, {
auth: { persistSession: false },
});
}
// 사용자 JWT를 기반으로 RLS가 적용되는 클라이언트
export function getUserClient(jwt: string): SupabaseClient {
return createClient(supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY')!, {
global: { headers: { Authorization: `Bearer ${jwt}` } },
auth: { persistSession: false },
});
}
// Authorization 헤더에서 JWT를 추출하고 사용자 클라이언트 반환
export function getClientFromRequest(req: Request): SupabaseClient {
const authHeader = req.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new Error('Missing or invalid Authorization header');
}
const jwt = authHeader.slice(7);
return getUserClient(jwt);
}
| 클라이언트 종류 | 키 | RLS 적용 | 사용 시나리오 |
|---|---|---|---|
| 관리자(admin) | service_role key | 우회 | 웹훅, 배치, 관리 작업 |
| 사용자(user) | anon key + JWT | 적용 | 인증된 사용자 요청 |
| 익명(anon) | anon key | 적용 | 공개 엔드포인트 |
요약
- Deno 런타임은 콜드 스타트와 웜 스타트로 나뉘며, 모듈 크기 최소화와 지연 초기화로 콜드 스타트를 줄일 수 있습니다.
- 웹훅은 반드시 HMAC 서명 검증과 타임스탬프 검사로 진위와 재전송 공격을 방어해야 합니다.
pg_net과pg_cron을 조합하면 데이터베이스 이벤트나 스케줄에 의한 비동기 함수 호출 파이프라인을 구성할 수 있습니다.- 비즈니스 로직은 HTTP 핸들러와 분리해
Deno.test로 단위 테스트하고,supabase functions serve와fetch로 통합 테스트합니다. - 구조적 로깅(JSON 형식)과 커스텀 에러 클래스를 도입하면 로그 필터링과 에러 추적이 쉬워집니다.
_shared디렉터리에 공유 모듈을 배치하면 코드 중복을 줄이고 함수 간 일관성을 유지할 수 있습니다.
연습문제
-
아래 코드의 문제점을 설명하고 개선하세요.
import heavyModule from 'npm:some-heavy-library@3'; const instance = new heavyModule.Client(Deno.env.get('API_KEY')); Deno.serve(async (req) => { const result = await instance.process(await req.json()); return new Response(JSON.stringify(result)); });힌트 — 콜드 스타트와 전역 초기화의 관계를 떠올리세요.
-
GitHub 웹훅을 수신하는 함수를 작성하세요. GitHub은
X-Hub-Signature-256헤더에sha256=<HMAC-SHA256 hex>형식으로 서명을 보냅니다. 시크릿은GITHUB_WEBHOOK_SECRET환경변수로 관리합니다.힌트 — GitHub 서명에는 Stripe처럼 타임스탬프가 없습니다.
sha256=접두사를 분리하고 body 전체를 서명합니다. -
orders테이블에 새 행이 삽입될 때send-order-confirmationEdge Function을 pg_net으로 비동기 호출하는 트리거를 작성하세요. 페이로드에는order_id와customer_email이 포함되어야 합니다.힌트 —
jsonb_build_object로 페이로드를 구성하고net.http_post를 호출하세요. -
다음 요구사항을 만족하는
logger.ts모듈을 작성하세요. (1)requestId를 컨텍스트로 받아 모든 로그 항목에 자동 포함, (2) 로그 레벨이error일 때만console.error사용, 나머지는console.log사용.힌트 — 클로저나 팩토리 함수로
requestId를 캡처하는 구조를 설계하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“Supabase 심화” 강좌에 대한 댓글입니다.