시스템 아키텍처
레이니의 자동화 시스템은 데이터베이스 이벤트를 감지하여 메시지 발송, AI 텍스트 생성, 브라우저 자동화까지 하나의 내구성 있는(durable) 워크플로우로 실행합니다. 서버가 재시작되어도 워크플로우가 이어서 실행되고, 클라이언트가 주기적으로 폴링하지 않아도 이벤트를 실시간으로 수신할 수 있는 구조입니다.
이 아키텍처를 택한 이유
기존 자동화 시스템에는 두 가지 문제가 있었습니다. 서버 크래시 시 워크플로우가 유실되는 문제, 그리고 클라이언트가 주기적으로 폴링해야 하는 비효율입니다.
| 항목 | 기존 (폴링 기반) | 현재 (Hook + 이벤트 기반) |
|---|---|---|
| 이벤트 감지 | 클라이언트가 10초마다 DB 폴링 | PostgreSQL 트리거가 즉시 감지 |
| 실행 보장 | 서버 크래시 시 워크플로우 유실 | 내구성 스텝이 마지막 완료 지점부터 재개 |
| 브라우저 제어 | WebSocket 양방향 통신 | Hook suspend/resume 패턴 |
| 실시간 알림 | 폴링 + WebSocket 혼합 | SSE 단방향 스트림 + Supabase Realtime |
| 리소스 소비 | 대기 중에도 연결 유지 | sleep/Hook 중 컴퓨팅 비용 0 |
| 재연결 | 상태 유실 가능 | startIndex로 중간부터 이어받기 |
핵심 설계 원칙 4가지
내구성 (Durable Execution)
워크플로우는 "use workflow" 디렉티브로 선언됩니다. Vercel Workflow SDK가 각 스텝의 결과를 자동으로 저장하므로, 서버 재배포나 크래시 후에도 마지막 완료된 스텝부터 자동으로 재개됩니다. 이미 완료된 스텝은 캐시된 결과를 반환합니다(결정론적 재실행). LLM 호출이나 메시지 발송 같은 부수 효과가 중복 실행되는 것을 막아줍니다.
재개 가능 (Resumable)
sleep() 호출은 컴퓨팅 리소스를 소비하지 않고 워크플로우를 일시 정지합니다. 브라우저 에이전트의 createHook() 역시 외부 이벤트(클라이언트 스냅샷)를 기다리는 동안 워크플로우를 중단합니다. 두 경우 모두 대기 중에는 비용이 발생하지 않습니다.
관찰 가능 (Observable)
모든 스텝 시작/완료 이벤트가 내구성 스트림에 기록됩니다. 클라이언트는 SSE를 통해 실시간으로 이 스트림을 소비하며, startIndex 파라미터로 중간부터 재연결할 수 있습니다.
폴링 없음 (Zero-Polling)
이벤트 감지는 PostgreSQL 트리거가 담당하고, 실시간 알림은 Supabase Realtime(postgres_changes)이 처리합니다. 클라이언트가 주기적으로 폴링할 필요가 없습니다.
전체 아키텍처
시스템은 크게 4개 계층으로 구성됩니다.
- 이벤트 감지: PostgreSQL 트리거가 데이터 변경을 감지하고,
dispatch_org_workflow()함수가 필터 조건을 평가합니다 - 워크플로우 엔진: Vercel Workflow SDK 기반의 내구성 실행 엔진이 스텝을 순차적으로 실행합니다
- 스텝 실행: 각 스텝 타입(sleep, send_message, ai_generate 등)이 독립적으로 실행됩니다
- 결과 출력: 메시지 발송, 텍스트 생성, 브라우저 자동화 등 실제 작업이 수행됩니다
핵심 구성 요소
| 구성 요소 | 기술 | 역할 |
|---|---|---|
| 워크플로우 엔진 | Vercel Workflow SDK | 내구성 있는 실행, 재시작/재배포 후에도 이어서 진행 |
| 데이터베이스 | Supabase (PostgreSQL + Realtime) | 트리거 감지, 실행 기록 저장, 실시간 변경 알림 |
| AI SDK | Vercel AI SDK + Google Gemini | 텍스트 생성(ai_generate), 브라우저 에이전트 의사결정 |
| 브라우저 클라이언트 | Python (laney-integrate) | 로컬 Chrome 제어, 스냅샷 전송, 커맨드 실행 |
| 스트림 계층 | Durable Stream + SSE | 실시간 이벤트 전달, 재연결 지원 |
| 대시보드 | Next.js + Supabase Realtime | 워크플로우 실행 상태 실시간 모니터링 |
실행 흐름 예시: 예약 확인 시나리오
고객이 예약을 확인(confirmed)하면 확인 메시지를 발송하는 워크플로우의 전체 흐름입니다.
| 단계 | 설명 | 기술 |
|---|---|---|
| 1. 데이터 변경 | 예약 상태가 confirmed로 변경됨 | PostgreSQL UPDATE |
| 2. 트리거 발동 | AFTER UPDATE 트리거가 dispatch_org_workflow() 호출 | PL/pgSQL |
| 3. 필터 평가 | status 필드가 confirmed로 변경되었는지 확인 | evaluate_trigger_filters() |
| 4. 실행 기록 | org_workflow_runs 테이블에 running 상태로 INSERT | Supabase |
| 5. HTTP 호출 | pg_net.http_post()로 워크플로우 엔진 API 호출 | pg_net |
| 6. 워크플로우 시작 | start(executeWorkflow, payload)로 내구성 워크플로우 시작 | Vercel Workflow SDK |
| 7. 대기 | sleep(30분) — 30분간 컴퓨팅 비용 없이 일시정지 | SDK sleep |
| 8. 메시지 발송 | Bizgo OMNI API로 알림톡 발송 | HTTP API |
| 9. 결과 저장 | 최종 상태와 실행 시간을 DB에 기록 | Supabase |
워크플로우 엔진 핵심
워크플로우 엔진은 두 가지 디렉티브로 내구성을 선언합니다.
"use workflow": 함수 본문 첫 줄에 선언하면 해당 함수가 내구성 워크플로우로 변환됩니다. 내부의 모든"use step"함수 호출은 결과가 자동으로 저장됩니다."use step": 함수 본문 첫 줄에 선언하면 해당 함수가 내구성 스텝으로 변환됩니다. 스텝의 반환값은 저장되어, 워크플로우가 재실행(replay)될 때 실제 로직을 다시 실행하지 않고 캐시된 결과를 반환합니다.
결정론적 재실행의 효과를 구체적으로 보면 이렇습니다.
| 시나리오 | 재실행 없이 | 재실행 (캐시) |
|---|---|---|
| LLM 텍스트 생성 | 매번 다른 텍스트 생성, 비용 발생 | 동일한 텍스트 반환, 비용 없음 |
| 메시지 발송 | 중복 발송 위험 | 발송하지 않고 성공 반환 |
| DB 업데이트 | 중복 업데이트 | 업데이트하지 않고 성공 반환 |
스텝에 에러가 발생하면 이후 스텝은 실행되지 않습니다. 부분 실행 결과는 DB에 그대로 보존되어 어느 스텝까지 성공했는지 확인할 수 있습니다. 최종 상태는 success, error, partial 세 가지입니다.
AI 스텝 파이프라인
워크플로우 엔진은 Vercel AI SDK와 Google Gemini를 사용하여 두 가지 유형의 AI 스텝을 제공합니다.
ai_generate: 프롬프트를 기반으로 LLM에서 텍스트를 생성하는 범용 스텝입니다. 워크플로우 컨텍스트의 데이터를 {{data.field}} 형식의 템플릿 변수로 주입할 수 있어, 고객 데이터에 기반한 개인화된 텍스트를 생성합니다. "use step" 디렉티브가 적용되어 재실행 시 LLM을 다시 호출하지 않습니다.
| 모델 | 속도 | 적합한 용도 |
|---|---|---|
gemini-2.0-flash (기본) | 매우 빠름 | 짧은 메시지 생성, 단순 분류 |
gemini-2.0-flash-lite | 가장 빠름 | 템플릿 기반 단순 변환 |
gemini-2.5-flash-preview-04-17 | 빠름 | 복잡한 분석, 긴 텍스트 생성 |
템플릿 변수 문법:
| 형식 | 설명 | 예시 출력 |
|---|---|---|
{{data.field}} | 현재 행의 필드 값 | 홍길동 |
{{previous_data.field}} | 변경 전 필드 값 | pending |
{{steps.stepId.field}} | 이전 스텝 결과 | (생성된 텍스트) |
{{data.field|date}} | 날짜 포매터 적용 | 2026년 4월 2일 (수) 14:30 |
decideNextAction: 브라우저 에이전트 루프 내부에서 동작하는 AI 스텝입니다. generateText() 대신 generateObject()를 사용하여 Zod 스키마로 구조화된 JSON(done, reasoning, commands)을 출력합니다. 현재 페이지의 접근성 트리를 분석하여 다음 브라우저 명령을 결정합니다.
브라우저 에이전트 역할
브라우저 에이전트는 서버(워크플로우 엔진)와 클라이언트(Python + Chrome)가 Hook을 통해 협업하는 AI 기반 브라우저 자동화 시스템입니다.
워크플로우가 createHook()에서 완전히 일시정지(suspend)되고, 클라이언트가 스냅샷을 보내면 정확히 그 지점에서 재개(resume)됩니다. 이 사이에 서버는 어떤 컴퓨팅 리소스도 소비하지 않습니다.
LLM은 시각적 스크린샷이 아닌 접근성 트리(텍스트)를 분석하여 다음 행동을 결정합니다. Hook 토큰은 agent:{workflowRunId}:{stepId}:{stepNumber} 형식의 결정론적 구조를 가지므로, 서버 재시작 후에도 동일한 Hook을 정확히 매칭할 수 있습니다.
스트림 프로토콜 구조
워크플로우 엔진의 모든 이벤트는 내구성 스트림(durable stream)에 기록됩니다. 클라이언트는 SSE(Server-Sent Events)를 통해 실시간으로 소비하며, 연결이 끊어져도 startIndex로 중간부터 재연결할 수 있습니다.
WebSocket이 아닌 SSE를 선택한 이유는 워크플로우 이벤트가 서버에서 클라이언트로의 단방향 흐름이기 때문입니다. SSE는 HTTP/2 위에서 동작하여 CDN과 로드밸런서와 호환되고, startIndex로 상태 복원이 가능합니다.
청크 타입 목록:
| 타입 | 발생 시점 | 주요 필드 |
|---|---|---|
step:started | 스텝 실행 시작 | stepId, stepName, stepType |
step:completed | 스텝 실행 완료 | stepId, status, output, durationMs |
agent:ready | 브라우저 에이전트 루프 시작 | stepId, goal, maxSteps |
agent:waiting | Hook 생성, 스냅샷 대기 | stepNumber, hookToken, deadlineAt |
agent:action | LLM이 결정한 다음 행동 | actionId, commands, reasoning, done |
agent:completed | 에이전트 루프 종료 | stepId, stepsExecuted |
workflow:completed | 전체 워크플로우 완료 | status, durationMs |
재연결 흐름은 단순합니다. 클라이언트가 마지막으로 수신한 인덱스를 lastIndex에 저장해두었다가, 재연결 시 GET /api/agent/{runId}/stream?startIndex={lastIndex}로 이미 받은 청크를 건너뜁니다.
클라이언트 통합 방식
워크플로우 엔진의 이벤트를 소비하는 클라이언트는 두 종류입니다.
- Python 클라이언트 (laney-integrate): 로컬 환경에서 Chrome을 제어하며 워크플로우 엔진과 실시간으로 통신합니다. 새로운 워크플로우 실행은 Supabase Realtime(
postgres_changes)으로 즉시 감지하고, Realtime 연결이 불안정한 환경을 위해 60초 폴백 폴링도 함께 동작합니다. SSE 스트림에서agent:waiting이벤트를 수신하면 Chrome에서 접근성 트리를 캡처하고 서버로 전송합니다. - 대시보드: Next.js로 구현된 실시간 모니터링 인터페이스입니다.
org_workflow_runs테이블의 변경을 Supabase Realtime으로 구독하여 실행 목록을 자동 갱신하고,useAgentStreamReact Hook이 SSE 스트림을 소비하여 스텝별 실행 상태와 AI의 의사결정 근거(reasoning)를 실시간으로 표시합니다.
두 클라이언트 모두 Supabase JWT 인증을 사용하며, 조직 수준의 접근 제어가 적용됩니다. 사용자가 속하지 않은 조직의 워크플로우 스트림에는 접근할 수 없습니다.
스텝 타입 요약
| 스텝 타입 | 설명 | 실행 위치 | 대표 용도 |
|---|---|---|---|
sleep | 지정 시간 대기 (컴퓨팅 소비 없음) | 서버 | 예약 30분 전 알림 |
send_message | 알림톡/친구톡/RCS/SMS 메시지 발송 | 서버 | 예약 확인 메시지 |
notification | 정보성 메시지 발송 (send_message 래퍼) | 서버 | 예약 리마인더 |
advertisement | 광고성 메시지 발송 (send_message 래퍼) | 서버 | 프로모션 안내 |
update_field | 데이터베이스 필드 값 수정 | 서버 | 고객 등급 변경 |
ai_generate | LLM으로 텍스트 생성 | 서버 | 개인화된 메시지 작성 |
browser_agent | Hook 기반 AI 브라우저 자동화 루프 | 서버 + 클라이언트 | 외부 사이트 자동화 |
browser_agent 스텝은 다른 스텝과 달리 서버와 클라이언트가 협업합니다. 서버(워크플로우 엔진)가 LLM으로 다음 행동을 결정하고, 클라이언트(Python + Chrome)가 실제 브라우저를 조작합니다.
다음 단계
각 구성 요소의 상세 내용은 아래 문서에서 확인하세요.