Vercel Workflows 도입 시 비용 산정과 마이그레이션 실무
서버리스 타임아웃 벽과 월간 비용 계산
서버리스 함수로 SaaS를 운영할 때 10초에서 60초라는 타임아웃 제한은 생각보다 자주 발목을 잡는다. 대용량 데이터 백업이나 멀티모달 AI 미디어 생성 파이프라인을 구현할 때 기존 서버리스 환경은 자꾸 코드를 중단시킨다.
Vercel Workflows는 함수가 대기 상태에 들어갈 때 자원 소비를 0으로 낮추면서 작업을 유지한다. 비용 예측을 위해 TypeScript 스크립트를 직접 돌려 월간 지출을 미리 계산한다. 일일 실행 건수, 스텝 수, 실행 시간, 페이로드 크기를 객체로 정의한 뒤 비용 계산 함수에 넣는다. Hobby 플랜의 월 5만 이벤트와 1GB 쓰기 한계를 넘는다면 Pro 승급을 미리 준비해야 한다. 월 1만 5000회 미만으로 돌린다면 Pro 기본 크레딧 안에서 소화할 수 있어 자체 호스팅 Redis 인프라를 유지하는 것보다 비용을 줄인다.
interface WorkflowMetrics {
dailyExecutions: number;
stepsPerExecution: number;
avgStepDurationMs: number;
payloadSizeKb: number;
}
function calculateEstimatedCost(metrics: WorkflowMetrics): { totalEvents: number; estimatedCostUsd: number } {
const totalEvents = metrics.dailyExecutions * metrics.stepsPerExecution * 30;
const freeTierEvents = 50000;
const overageEvents = Math.max(0, totalEvents - freeTierEvents);
const costPerThousand = 0.20;
const estimatedCostUsd = (overageEvents / 1000) * costPerThungk?: number;
return {
totalEvents,
estimatedCostUsd: Number(((overageEvents / 1000) * costPerThousand).toFixed(2))
};
}
메시지 큐에서 워크플로우 SDK로 전환하는 절차
BullMQ나 AWS SQS를 쓸 때는 큐 생성, 작업 발행, 수신, Redis 커넥션 관리를 전부 직접 짰다. 이 과정에서 코드가 불필요하게 비대해진다. Vercel Workflows SDK는 코드 레벨 지시어로 이 인프라 코드를 지워버린다.
마이그레이션은 3단계로 끝낸다. 첫째, next.config.ts 파일에 withWorkflow 래퍼를 씌운다. 둘째, 기존 워커 프로세스 대신 use step 지시어를 사용해 개별 작업을 비동기 함수로 작성한다. 셋째, 복잡한 Parent-Child Job 연쇄 구조를 버리고 use workflow 내부에서 자바스크립트 제어문과 await sleep 함수를 쓴다. 외부 Redis 연결 코드가 사라지면서 앱 메모리 풋프린트가 줄어들고 전환 시간이 빨라진다.
import { workflow } from "@vercel/workflows";
export const { POST } = workflow(async (context) => {
const userId = context.request.payload.userId;
const data = await context.run("fetch-user-data", async () => {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
});
await context.sleep("wait-for-processing", "10m");
await context.run("finalize-task", async () => {
await db.users.update({ where: { id: userId }, data: { status: "processed" } });
});
});
네트워크 단절 상황에서 데이터 정합성을 지키는 에러 핸들링
비결정적 장애가 터졌을 때 데이터 유실을 막으려면 에러 성격을 먼저 분리해야 한다. Vercel Workflows는 일시적 오류와 치명적 오류를 나눠서 다룬다.
스텝 안에서 에러를 잡으려면 먼저 HTTP 429나 500 계열 에러가 났을 때 RetryableError를 던져서 지수 백오프 기반 재시도를 유도한다. 그리고 데이터베이스를 수정하는 스텝 내부에는 유니크 키 검사 로직을 넣어 재시도할 때 중복 데이터가 쌓이는 것을 막는다. 마지막으로 Vercel Log Drains를 Sentry와 연결해 구조화된 JSON 로그를 실시간으로 스트리밍한다. 이 패턴을 적용하면 예외 상황에서도 중단점부터 상태가 복구되어 수동으로 데이터를 복구할 일이 사라진다.
import { RetryableError } from "@vercel/workflows";
export async function processPaymentStep(context: any, paymentData: any) {
return await context.run("charge-payment", async () => {
const exists = await db.transactions.findUnique({
where: { idempotencyKey: paymentData.key }
});
if (exists) {
return exists;
}
const response = await paymentGateway.charge(paymentData);
if (response.status === 429 || response.status >= 500) {
throw new RetryableError("Payment gateway overloaded, retrying...");
}
return await db.transactions.create({
data: { idempotencyKey: paymentData.key, amount: response.amount }
});
});
}