effidevFlutter · Cloudflare 엣지 · 클라우드 비용 최적화
한국어

Cloudflare Agents SDK: 상태 유지형 AI 에이전트 구축

Cloudflare Agents SDK로 상태 유지형 AI 에이전트 구축하기

LLM을 API로 호출해서 답변을 받는 것까지는 누구나 한다. 문제는 그 다음이다. 에이전트가 이전 대화를 기억하고, 외부 도구를 호출하고, 중간에 사람의 승인을 기다렸다가 이어서 실행하고, 크론 스케줄에 따라 스스로 작업을 시작하는 — 이른바 **“상태 유지형(stateful) 프로덕션 에이전트”**를 만들려면 상태 관리, 영속 저장소, 실시간 통신, 장기 실행 워크플로우를 전부 직접 엮어야 한다. Redis + PostgreSQL + WebSocket 서버 + 큐 + 크론 잡을 따로따로 운영하면 인프라 자체가 에이전트보다 복잡해진다.

Cloudflare가 2026년 Agents Week에 공개한 Agents SDK는 이 문제를 Durable Objects 한 장 위에서 해결한다. 에이전트 인스턴스 하나가 곧 Durable Object 하나이고, 내장 SQLite에 상태를 영속하고, WebSocket으로 프론트엔드와 실시간 통신하며, 알람과 크론으로 자율 실행까지 처리한다. 별도 인프라 없이 wrangler deploy 한 번이면 전 세계 300개 이상 PoP에 배포된다.

이 글은 Agents SDK의 아키텍처부터 Agent 클래스 구현, @callable() 데코레이터를 쓴 타입 안전 RPC, useAgent/useAgentChat React 훅 연동, Human-in-the-Loop 승인 패턴, 그리고 프로덕션 배포 시 주의할 한도와 비용까지 실제로 운영에 올릴 수 있는 수준으로 다룬다.

핵심 요약

  • Agents SDK의 에이전트는 Durable Objects 위에서 동작하며, 인스턴스당 최대 1GB 내장 SQLite, 30초 CPU 시간(요청당 리셋), 벽시계 기준 무제한 대기가 가능하다.
  • Agent 클래스를 상속하고 @callable() 데코레이터로 메서드를 노출하면, 프론트엔드에서 useAgent 훅으로 WebSocket 기반 타입 안전 RPC를 즉시 호출할 수 있다.
  • Human-in-the-Loop은 needsApproval 플래그로 도구 호출을 게이팅하거나, Cloudflare Workflows의 waitForApproval()수일~수개월 대기까지 지원한다.
  • 에이전트는 크론 스케줄과 알람으로 자율적으로 작업을 시작할 수 있어, 요청-응답 패턴에 갇히지 않는다.
  • Workers Paid 플랜 기준 Durable Objects 요금으로 과금되며, 계정당 수천만 개의 동시 에이전트 인스턴스를 운영할 수 있다.

Agents SDK 아키텍처: 왜 Durable Objects인가

기존의 서버리스 AI 에이전트 구현 방식은 대부분 “상태 없는 함수 + 외부 저장소” 조합이다. Lambda나 Cloud Functions에서 LLM을 호출하고, 대화 이력은 Redis나 DynamoDB에, 장기 실행 상태는 Step Functions에, 실시간 통신은 별도 WebSocket 서버에 맡긴다. 에이전트 하나를 위해 서비스 4~5개를 조합해야 하고, 각 서비스 간 정합성을 맞추는 글루 코드가 에이전트 로직보다 길어진다.

Agents SDK는 이 조합을 Durable Objects 하나로 압축한다. Cloudflare 공식 아키텍처 문서에 따르면, 에이전트 인스턴스의 구조는 다음과 같다.

역할 Durable Objects가 제공하는 것 기존 구조에서 대응하는 서비스
컴퓨팅 싱글 스레드 isolate, 요청당 CPU 30초 Lambda / Cloud Run
영속 상태 내장 SQLite (인스턴스당 최대 1GB) DynamoDB / Redis
실시간 통신 네이티브 WebSocket (Hibernation 지원) Pusher / Socket.IO 서버
스케줄링 알람 API + 크론 표현식 CloudWatch Events / cron 서버
전역 라우팅 글로벌 유니크 ID로 어디서든 같은 인스턴스 접근 Route 53 + 로드밸런서
내결함성 자동 마이그레이션, 장애 시 다른 PoP에서 재시작 직접 구현

핵심은 **“에이전트 인스턴스 = Durable Object 인스턴스”**라는 1:1 매핑이다. 에이전트가 this.setState()를 호출하면 내장 SQLite에 즉시 기록되고, 연결된 모든 WebSocket 클라이언트에 상태 변경이 자동 브로드캐스트된다. 에이전트가 LLM 응답을 기다리는 동안에도 Durable Object는 벽시계 기준 무제한으로 대기할 수 있어(CPU 시간은 소모하지 않으므로), 외부 큐나 Step Functions 없이 장기 실행 워크플로우를 에이전트 내부에서 처리할 수 있다.

이 구조의 실질적 장점은 배포 복잡도의 소멸이다. wrangler deploy 한 번이면 에이전트 코드, 상태 저장소, WebSocket 엔드포인트, 스케줄러가 전부 한꺼번에 Cloudflare의 글로벌 네트워크에 올라간다. 별도의 데이터베이스 프로비저닝, WebSocket 서버 스케일링, 크론 잡 설정이 필요 없다.

Agent 클래스 구현: 상태 관리와 @callable RPC

에이전트의 기본 골격은 Agent 클래스를 상속하는 것에서 시작한다. GitHub의 agents-starter 템플릿을 기준으로 프로젝트를 생성하고 핵심 구조를 살펴보자.

# 프로젝트 생성 (2026-08 기준 최신 템플릿)
npm create cloudflare@latest -- --template cloudflare/agents-starter my-agent
cd my-agent

아래는 고객 지원 티켓을 처리하는 에이전트의 서버 사이드 구현 예시다. 이 코드가 해결하는 것은 상태 영속 + 타입 안전 RPC + 도구 호출을 하나의 클래스 안에서 처리하는 패턴이다.

// src/server.ts
import { Agent, callable } from "agents";
import { AIChatAgent } from "@cloudflare/ai-chat";

// 에이전트 상태 타입 정의
export interface TicketState {
  ticketId: string | null;
  status: "idle" | "investigating" | "waiting-approval" | "resolved";
  history: Array<{ role: string; content: string; timestamp: number }>;
  metadata: Record<string, string>;
}

export class SupportAgent extends AIChatAgent<Env, TicketState> {
  // 초기 상태 — Durable Object 생성 시 SQLite에 자동 저장
  initialState: TicketState = {
    ticketId: null,
    status: "idle",
    history: [],
    metadata: {},
  };

  // @callable()로 노출한 메서드는 프론트엔드에서 agent.stub.assignTicket() 으로 호출 가능
  @callable()
  async assignTicket(ticketId: string, priority: string): Promise<string> {
    // setState()는 SQLite에 즉시 기록 + 연결된 모든 클라이언트에 브로드캐스트
    this.setState({
      ...this.state,
      ticketId,
      status: "investigating",
      metadata: { ...this.state.metadata, priority },
    });

    // Workers AI로 티켓 분석 (에지에서 추론)
    const analysis = await this.env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", {
      messages: [
        { role: "system", content: "고객 지원 티켓을 분석하고 해결 방안을 제시하라." },
        { role: "user", content: `티켓 ${ticketId}, 우선순위: ${priority}` },
      ],
    });

    return analysis.response ?? "분석 완료";
  }

  @callable()
  async getStatus(): Promise<TicketState> {
    return this.state;
  }

  // 크론 스케줄: 매 시간 미해결 티켓 체크
  async onCronTrigger(cron: string): Promise<void> {
    if (this.state.status === "investigating") {
      const elapsed = Date.now() - (this.state.history.at(-1)?.timestamp ?? 0);
      if (elapsed > 3600_000) {
        // 1시간 이상 미해결 → 에스컬레이션 알림
        await this.escalate();
      }
    }
  }

  private async escalate(): Promise<void> {
    this.setState({ ...this.state, metadata: { ...this.state.metadata, escalated: "true" } });
    // 슬랙 웹훅, 이메일 등 알림 전송 로직
  }
}

주의할 점: @callable() 데코레이터는 WebSocket 기반 RPC 전용이다. 같은 Worker 내부나 다른 Durable Object에서 에이전트 메서드를 호출할 때는 표준 Durable Object RPC(this.env.AGENT.get(id))를 사용한다. @callable()을 내부 호출에도 쓰면 불필요한 WebSocket 오버헤드가 추가된다.

wrangler.jsonc 설정에서 에이전트를 Durable Object로 바인딩하는 부분도 빠뜨리면 안 된다.

// wrangler.jsonc
{
  "name": "support-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-28",
  "durable_objects": {
    "bindings": [
      {
        "name": "SUPPORT_AGENT",
        "class_name": "SupportAgent"
      }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }
  ],
  "ai": { "binding": "AI" }
}

React 프론트엔드 연동: useAgent와 useAgentChat 훅

Agents SDK는 React 전용 훅을 제공해서 프론트엔드-에이전트 간 연결을 선언적으로 처리한다. 공식 React 연동 가이드에서 제공하는 세 가지 훅의 역할은 명확히 다르다.

패키지 용도
useAgent agents/react 범용 에이전트 연결. 상태 동기화 + RPC 호출
useAgentChat @cloudflare/ai-chat/react 채팅 특화. 메시지 영속, 스트리밍, 도구 실행 자동 처리
useVoiceAgent @cloudflare/voice 음성 STT/TTS. 같은 WebSocket 위에서 실시간 음성 처리

아래는 useAgent로 앞서 만든 SupportAgent와 연결하는 React 컴포넌트 예시다. 이 코드가 해결하는 것은 WebSocket 연결 관리 + 상태 자동 동기화 + 타입 안전 RPC 호출을 선언적으로 처리하는 패턴이다.

// src/client.tsx
import { useAgent } from "agents/react";
import { useState, useCallback } from "react";
import type { SupportAgent, TicketState } from "./server";

function SupportDashboard() {
  const [ticket, setTicket] = useState<TicketState | null>(null);
  const [ticketId, setTicketId] = useState("");

  // useAgent는 WebSocket 연결을 자동으로 관리하고,
  // 에이전트의 setState() 호출 시 onStateUpdate가 자동으로 트리거된다
  const agent = useAgent<SupportAgent, TicketState>({
    agent: "SupportAgent",
    onStateUpdate: (state) => setTicket(state),
    onError: (error) => console.error("Agent connection error:", error),
  });

  const handleAssign = useCallback(async () => {
    if (!agent.stub || !ticketId) return;
    // 타입 안전한 RPC 호출 — @callable() 메서드만 stub에 노출됨
    const result = await agent.stub.assignTicket(ticketId, "high");
    console.log("분석 결과:", result);
  }, [agent.stub, ticketId]);

  return (
    <div>
      <input
        value={ticketId}
        onChange={(e) => setTicketId(e.target.value)}
        placeholder="티켓 ID 입력"
      />
      <button onClick={handleAssign} disabled={!agent.connected}>
        티켓 할당
      </button>
      {ticket && (
        <div>
          <p>상태: {ticket.status}</p>
          <p>우선순위: {ticket.metadata.priority ?? "미지정"}</p>
        </div>
      )}
    </div>
  );
}

useAgentChat은 더 특화된 훅으로, 메시지 이력의 SQLite 영속, LLM 스트리밍 응답 처리, 도구 호출 결과의 자동 인라이닝까지 내부적으로 처리한다. 채팅 UI를 만들 때는 useAgent 대신 useAgentChat을 쓰는 것이 코드량을 절반 이하로 줄인다.

Human-in-the-Loop: 도구 승인 게이팅과 장기 대기 패턴

프로덕션 AI 에이전트에서 가장 까다로운 부분 중 하나가 **“에이전트가 위험한 작업을 실행하기 전에 사람의 확인을 받는 것”**이다. Agents SDK는 이를 세 가지 수준으로 지원한다.

패턴 1: needsApproval 도구 게이팅. 도구 정의 시 needsApproval: true를 설정하면, 에이전트가 해당 도구를 호출하려 할 때 실행을 멈추고 approval-requested 상태로 전환한다. 프론트엔드에서 승인 또는 거부를 보내면 에이전트가 이어서 실행하거나 중단한다.

// 도구 정의 예시 — 결제 처리 도구에 승인 게이트 적용
const tools = [
  {
    name: "process_refund",
    description: "고객에게 환불을 처리한다",
    parameters: {
      type: "object",
      properties: {
        amount: { type: "number", description: "환불 금액 (USD)" },
        reason: { type: "string", description: "환불 사유" },
      },
      required: ["amount", "reason"],
    },
    // 이 플래그가 핵심 — 에이전트가 이 도구를 호출하려 하면 일시 정지
    needsApproval: true,
  },
];

주의할 점: needsApproval세션 내 실시간 승인에 적합하다. 사용자가 브라우저를 닫으면 WebSocket이 끊기므로, 승인 대기 상태가 수 시간 이상 지속될 수 있는 시나리오에는 적합하지 않다.

패턴 2: Cloudflare Workflows의 waitForApproval(). 장기 대기가 필요한 경우(예: 관리자 승인이 다음 날까지 올 수 있는 워크플로우), Cloudflare WorkflowswaitForApproval() 메서드를 사용한다. 이 메서드는 Durable Object 위에 영속적인 게이트를 만든다. 에이전트 인스턴스가 메모리에서 내려가더라도(Hibernation) 승인 이벤트가 오면 자동으로 깨어나서 워크플로우를 이어간다. 수 일에서 수 개월까지 대기가 가능하다.

패턴 3: MCP Elicitation. 에이전트가 Model Context Protocol(MCP) 서버와 통신할 때, MCP 서버 측에서 추가 정보가 필요하면 configureElicitationHandlers()를 통해 사용자에게 직접 질문을 보낼 수 있다. 에이전트가 MCP 클라이언트로서 여러 외부 도구를 오케스트레이션할 때 유용하다.

스케줄링과 자율 실행: 크론, 알람, 웹훅

전통적인 AI 챗봇은 사용자가 메시지를 보내야만 동작한다. 프로덕션 에이전트는 스스로 작업을 시작할 수 있어야 한다. Agents SDK의 에이전트는 Durable Objects의 알람 API를 상속받아 세 가지 자율 실행 패턴을 지원한다.

트리거 설정 방법 적합한 시나리오
크론 스케줄 wrangler.jsonctriggers.crons 매 시간/매일 정기 작업 (리포트, 모니터링)
알람 this.ctx.storage.setAlarm(timestamp) 특정 시점에 1회 실행 (예약 발송, 리마인더)
웹훅 수신 Worker의 fetch 핸들러에서 에이전트로 라우팅 외부 이벤트 반응 (Stripe 결제 완료, GitHub PR 머지)
이메일 수신 email() 핸들러 이메일 기반 자동 응답·분류

이 구조의 핵심 장점은 에이전트가 유휴 상태일 때 비용이 0이라는 점이다. Durable Objects의 Hibernation API 덕분에 WebSocket 연결을 유지하면서도 에이전트가 메모리에서 내려갈 수 있다. 알람이나 크론이 트리거되면 자동으로 깨어나서 작업을 수행한 뒤 다시 잠든다. 항상 떠 있는 VM 인스턴스가 필요 없으므로, 에이전트 수천 개를 동시에 배포해도 실제로 활성화된 에이전트에 대해서만 과금된다.

// 크론 트리거 설정 (wrangler.jsonc)
{
  "triggers": {
    "crons": ["0 */6 * * *"]  // 매 6시간마다 실행
  }
}

프로덕션 배포: 한도, 비용, 운영 체크리스트

Agents SDK를 프로덕션에 올릴 때 반드시 확인해야 할 한도와 비용 구조를 정리한다. 2026년 8월 기준 Cloudflare Workers 요금 페이지Durable Objects 요금 페이지의 수치다.

항목 비고
계정당 동시 에이전트 인스턴스 수천만 개 이상 하드 리밋은 요청으로 상향 가능
인스턴스당 최대 상태 크기 1GB (SQLite) Key-Value 스토리지도 별도 사용 가능
요청당 CPU 시간 30초 HTTP 요청 또는 WebSocket 메시지당 리셋
벽시계 대기 시간 무제한 LLM 응답 대기 등은 CPU를 소모하지 않음
에이전트 정의 수 ~250,000 계정 전체
Durable Objects 요금 (Paid) 요청 100만 건당 $0.15 + GB-초당 $12.50/월 Hibernation 시 벽시계 시간은 과금 안 됨
Workers AI 추론 모델별 상이 Llama 3.3 70B: 입력 1M 토큰 $0.27

운영 시 주의할 점 세 가지:

  1. SQLite 1GB 한도를 넘기지 않도록 상태를 정리한다. 대화 이력이 무한히 쌓이면 한도에 도달한다. 오래된 이력은 R2로 아카이빙하고 SQLite에는 최근 N건만 유지하는 전략이 필요하다.

  2. CPU 30초 제한을 의식한다. LLM 호출 자체는 await 대기이므로 CPU를 소모하지 않지만, 응답을 파싱하고 후처리하는 로직이 복잡하면 CPU 30초를 초과할 수 있다. 무거운 후처리는 별도 Worker로 분리하거나, Cloudflare Queues로 비동기 처리한다.

  3. Hibernation을 적극 활용한다. WebSocket 연결이 있어도 hibernateWebSocket()을 사용하면 메시지가 없는 동안 에이전트가 메모리에서 내려간다. 이렇게 하면 GB-초 과금이 활성 처리 시간에만 적용되어 비용이 크게 줄어든다. WebSocket Hibernation 가이드에서 이 패턴의 비용 절감 효과를 상세히 다뤘다.

기존 에이전트 프레임워크와의 차이: LangGraph, Mastra 비교

Agents SDK가 유일한 선택지는 아니다. 같은 시기에 활발히 사용되는 에이전트 프레임워크와 비교하면, Agents SDK의 포지셔닝이 명확해진다.

비교 항목 Cloudflare Agents SDK LangGraph Mastra
실행 환경 Cloudflare 에지 (전용) 어디서든 (클라우드, 로컬) 어디서든 (Node.js)
상태 관리 Durable Objects 내장 SQLite 외부 체크포인터 (Redis, Postgres) 외부 DB 필요
실시간 통신 네이티브 WebSocket + React 훅 직접 구현 또는 LangServe 직접 구현
장기 실행 Hibernation + Workflows AsyncIO + 외부 큐 외부 큐 필요
배포 wrangler deploy 원커맨드 컨테이너/서버리스 직접 설정 컨테이너/서버리스 직접 설정
벤더 종속 높음 (Cloudflare 전용) 낮음 낮음
강점 인프라 제로, 글로벌 에지, 비용 효율 그래프 기반 복잡 워크플로우, 유연성 간결한 API, 빠른 프로토타이핑

Agents SDK의 핵심 트레이드오프는 **“인프라 복잡도를 0으로 만드는 대신 Cloudflare에 완전히 종속된다”**는 점이다. 이미 Cloudflare Workers를 주력으로 쓰고 있는 팀이라면 Agents SDK가 가장 빠르게 프로덕션에 도달하는 경로다. 반면 멀티 클라우드 전략이 필수이거나, 에이전트의 상태 그래프가 복잡한 DAG/순환 구조를 요구하면 LangGraph가 더 적합하다.

자주 묻는 질문

Agents SDK와 Workers AI는 무엇이 다른가?

Workers AI는 모델 추론 API다. Llama, Whisper 같은 모델을 에지에서 실행하는 엔진이다. Agents SDK는 에이전트 프레임워크로, 상태 관리·도구 호출·사용자 상호작용·스케줄링 등 에이전트의 “두뇌”를 구성하는 인프라다. 보통 Agents SDK 안에서 Workers AI를 추론 백엔드로 호출하는 구조로 함께 사용한다.

에이전트 하나에 여러 사용자를 연결할 수 있는가?

가능하다. Durable Object는 여러 WebSocket 연결을 동시에 수용한다. 에이전트 ID를 채팅방 ID처럼 사용하면, 같은 에이전트에 여러 사용자가 접속해서 실시간으로 상태를 공유할 수 있다. 다만 Durable Object의 싱글 스레드 특성상, 동시 쓰기가 극단적으로 많은 시나리오(초당 수천 건)에서는 성능 한계가 있다.

기존 Workers 프로젝트에 Agents SDK를 추가할 수 있는가?

가능하다. agents 패키지를 설치하고, wrangler.jsonc에 Durable Objects 바인딩을 추가한 뒤, Worker의 fetch 핸들러에서 routeAgentRequest(request, env)를 호출하면 기존 라우팅과 에이전트 라우팅이 공존한다. 기존 API 엔드포인트는 그대로 유지하면서 에이전트 기능만 점진적으로 추가할 수 있다.

로컬 개발은 어떻게 하는가?

wrangler dev로 로컬 개발 서버를 실행하면 Durable Objects, SQLite, WebSocket이 모두 로컬에서 에뮬레이션된다. 프로덕션과 동일한 API 표면을 로컬에서 테스트할 수 있어, 외부 서비스 의존 없이 개발-테스트 사이클을 빠르게 돌릴 수 있다.