Workflow 장기 실행
Workflow를 장기 실행 에이전트의 오케스트레이션 계층으로 사용해 승인, 이벤트 재개, durable replay를 구현하는 방법을 정리합니다.
핵심 요약
- 장기 실행·외부 이벤트 재개·사람 승인 대기 작업은 timeout을 늘리지 말고 durable execution 모델인 Workflow로 옮깁니다.
- 2026년 4월 GA된 Vercel Workflows는
"use workflow"가 오케스트레이션을,"use step"이 재시도·영속화 가능한 작업 단위를 담당합니다. - step은 작게 유지하고 side effect를 뒤로 밀어 replay 시 중복을 막으며, 외부 write에는 idempotency key를 부여합니다.
- 승인이 필요한 action은 별도 step으로 분리하고 환불·권한 승격 등 업무별 기본 대기 시간(4~24시간)을 둡니다.
- 단순 fan-out 비동기는 Queue가 더 단순할 수 있으므로 모든 비동기 작업을 Workflow로 몰지 않습니다.
Workflow 5 전환 점검
2026-10-23 npm latest는 workflow@5.2.3입니다. "use workflow"·"use step" 경계를 유지하되 다음 호환성을 함께 검토합니다.
runStep은 제거됐으므로 step 함수를 직접 호출합니다.getWorld()·getWorldHandlers()·createWorld()는 비동기입니다. self-hosted worker bootstrap에서await누락을 확인합니다.getHookByToken()·resumeHook()으로 얻은 hook의metadata는 Promise이므로 읽을 때await합니다.experimental_setAttributes는 제거됐으며setAttributes를 사용합니다.- 중복 step·workflow ID는 build 오류로 처리됩니다. 번들·자체 route를 직접 연결한 앱은 v5 migration 항목도 검토합니다.
AbortSignal을 step에 전달해 실행 중인 요청을 취소할 수 있지만 signal을 무시하는 작업은 계속 실행될 수 있습니다. 취소는 이미 완료한 결제·메일을 되돌리는 동작이 아니므로 side effect의 idempotency key를 보존합니다. hook token retention·force takeover는 승인을 생략하는 기능으로 쓰지 않습니다.
에이전트가 실제 업무를 다루기 시작하면 요청-응답 구조만으로는 부족합니다.
승인 대기, 외부 웹훅 재개, 긴 분석 작업, 밤새 돌아가는 배치가 끼어들기 때문입니다. 이럴 때 필요한 건 "더 긴 timeout"이 아니라 durable execution 모델입니다.
Workflow에 올려야 하는 작업
2026년 4월 GA 이후 Vercel Workflows는 "use workflow"와 "use step" directive를 두 축으로 삼습니다. workflow 함수가 오케스트레이션을 맡고, step 함수가 재시도·영속화·관측이 가능한 작업 단위를 맡습니다.
| 작업 유형 | Workflow 적합도 | 이유 |
|---|---|---|
| 즉시 채팅 응답 | 낮음 | request path에 남기는 편이 단순 |
| 수분~수시간 배치 분석 | 높음 | 재개 가능성과 상태 추적 필요 |
| 사람 승인 대기 | 매우 높음 | external event와 pause/resume 필요 |
| 외부 API 재시도 | 높음 | step retry와 backoff 관리 필요 |
| cron성 작업 | 중간 | 큐 또는 스케줄 작업과 비교 필요 |
실행 구조
Workflow와 Queue의 경계
| 질문 | Workflow | Queue |
|---|---|---|
| 상태 재개가 필요한가 | 강점 | 약함 |
| 사람 승인/이벤트 대기가 필요한가 | 강점 | 별도 구현 필요 |
| 단순 fan-out 비동기 처리인가 | 가능 | 더 단순할 수 있음 |
| 실행 경로를 단계별로 감사해야 하는가 | 강점 | 제한적 |
step 설계 원칙
| 원칙 | 이유 | 예시 |
|---|---|---|
| step은 작게 유지 | 재시도 단위 명확화 | fetch data, run model, persist result |
| side effect는 뒤로 민다 | replay 시 중복 방지 | approval 이후 write 실행 |
| human approval은 별도 단계 | business gate 명시 | payment, message send, privilege grant |
| versioned deployment를 고려 | 장기 실행 중 코드 변경 대비 | migration-safe payload 사용 |
예시: 승인 포함 오케스트레이션
import { defineHook, sleep } from 'workflow'
const approvalHook = defineHook<{ approved: boolean; operatorId: string }>()
export async function customerEscalation(ticketId: string) {
'use workflow'
const summary = await summarizeTicket(ticketId)
const approval = approvalHook.create({ token: `ticket:${ticketId}` })
await sendApprovalRequest({ ticketId, summary, token: approval.token })
const result = await Promise.race([
approval,
sleep('24h').then(() => ({ approved: false, operatorId: 'timeout' })),
])
if (!result.approved) {
return { status: 'rejected' }
}
return executeEscalation(summary)
}
async function summarizeTicket(ticketId: string) {
'use step'
return loadAndSummarizeTicket(ticketId)
}
async function executeEscalation(summary: TicketSummary) {
'use step'
return dispatchEscalation(summary)
}승인이 필요한 업무 예시
| 업무 | 승인 이유 | 기본 대기 시간 |
|---|---|---|
| 고객에게 금전성 혜택 발송 | 비용/악용 방지 | 4시간 |
| 권한 승격 | 보안 리스크 | 24시간 |
| 법적 문안 발송 | 규제 리스크 | 8시간 |
| 대량 외부 메시지 발송 | 브랜드 리스크 | 2시간 |
추론
Vercel Workflow가 장기 실행과 이벤트 재개에 강하다고 해서 모든 비동기 작업을 여기로 몰아넣을 필요는 없습니다. 단순 큐잉과 상태 머신은 떼어 두는 편이 비용과 운영 복잡도 양쪽에서 낫습니다.
운영 기준
- 사용자가 기다리는 요청과 사람이 나중에 확인할 작업을 분리합니다.
- 승인 전 단계는 읽기 중심으로 설계하고, 쓰기는 승인 뒤에 배치합니다.
- step payload는 버전 호환성을 고려해 최소 필드만 유지합니다.
- replay 가능한 설계를 위해 외부 side effect idempotency key를 사용합니다.
ADR 스타일 결론
Decision
장기 실행, 외부 이벤트 재개, 사람 승인 대기가 있는 작업은 Workflow로 이동합니다. request timeout을 늘려 해결하지 않고, durable state machine으로 모델링합니다.
실무 체크리스트
- 30초 이상 걸리는 작업이 request path에 남아 있지 않은가
- approval 전에 irreversible side effect가 실행되지 않는가
- step payload가 버전 안전한 최소 구조로 유지되는가
- 외부 write가 idempotency key를 갖는가