Claude Computer Use Tool로 데스크톱 자동화 에이전트 만들기
Claude Computer Use Tool의 API 구조와 에이전트 루프 구현, Docker 샌드박스·보안 검증까지 실무 관점으로 정리한 가이드
2026-08-12 · 최초 발행 2026-04-26
Computer Use Tool의 동작 원리
Computer Use Tool은 Claude가 데스크톱 환경과 상호작용할 수 있게 해주는 Anthropic의 베타 기능이다. 웹 자동화, GUI 테스트, 반복 업무 자동화 시나리오에 쓸 수 있고, Docker 기반 격리 환경과 에이전트 루프를 결합하면 안전한 자동화 파이프라인으로 확장된다.
┌─────────────────────────────────────────────────────────────────────────┐
│ Claude Computer Use 개념도 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ 사용자 │ "고양이 사진을 데스크톱에 저장해줘" │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Claude API │ │
│ │ - 화면 스크린샷 분석 │ │
│ │ - 다음 작업 결정 (클릭, 타이핑, 스크롤 등) │ │
│ │ - tool_use 응답 반환 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 개발자 애플리케이션 (에이전트 루프) │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. Claude 응답에서 tool_use 추출 │ │ │
│ │ │ 2. 가상 환경에서 작업 실행 │ │ │
│ │ │ 3. 스크린샷 캡처 │ │ │
│ │ │ 4. tool_result로 Claude에 반환 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 컴퓨팅 환경 (Docker/VM) │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 가상 디스플레이 (Xvfb) │ │ │
│ │ │ - 마우스/키보드 제어 │ │ │
│ │ │ - 스크린샷 캡처 │ │ │
│ │ │ - 애플리케이션 실행 (Firefox, LibreOffice 등) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
핵심은 Claude가 화면을 직접 만지지 않는다는 점이다. Claude는 스크린샷 이미지만 보고 다음 작업을 판단해 tool_use로 응답하고, 실제 클릭·타이핑은 개발자 코드가 가상 환경에서 대신 실행한 뒤 결과를 다시 Claude에 넘긴다. 이 왕복이 작업 완료까지 반복되는 것이 에이전트 루프이며, 보안을 위해 반드시 샌드박스 안에서 돌려야 한다.
| 기능 | 설명 |
|---|---|
| 스크린샷 캡처 | 현재 화면 상태 확인 |
| 마우스 제어 | 클릭, 드래그, 이동 |
| 키보드 입력 | 텍스트 입력, 단축키 |
| 데스크톱 자동화 | 모든 GUI 애플리케이션 제어 |
모델별 설정과 기본 호출
도구 버전과 베타 플래그는 모델마다 다르게 지정해야 한다.
| 모델 | 도구 버전 | 베타 플래그 |
|---|---|---|
| Claude Opus 4.5 | computer_20251124 |
computer-use-2025-11-24 |
| Claude Sonnet 4.5 | computer_20250124 |
computer-use-2025-01-24 |
| Claude Sonnet 4 | computer_20250124 |
computer-use-2025-01-24 |
필요한 패키지는 다음과 같다.
pip install anthropic
pip install pillow # 스크린샷 처리
pip install pyautogui # 마우스/키보드 제어 (로컬 테스트용)
기본 호출 구조는 tools에 computer·text_editor·bash 세 도구를 함께 등록하고, betas에 베타 플래그를 빠뜨리지 않는 것이 핵심이다.
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
betas=["computer-use-2025-01-24"], # 베타 헤더 필수!
tools=[
{
"type": "computer_20250124",
"name": "computer",
"display_width_px": 1024,
"display_height_px": 768,
"display_number": 1,
},
{
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool"
},
{
"type": "bash_20250124",
"name": "bash"
}
],
messages=[{
"role": "user",
"content": "고양이 사진을 데스크톱에 저장해줘"
}]
)
전체 아키텍처와 처리 흐름
사용자 인터페이스, Claude API, 에이전트 루프, 컴퓨팅 환경(Docker 컨테이너) 네 층이 순환한다. 컨테이너 안에서는 Xvfb(가상 디스플레이)와 Mutter(창 관리자)가 Firefox·LibreOffice 같은 애플리케이션을 띄우고, 에이전트 루프는 while not done 형태로 Claude 응답의 tool_use를 실행하고 결과를 다시 메시지에 붙여 넣는다.
┌─────────────────────────────────────────────────────────────────────────┐
│ 전체 아키텍처 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ 사용자 인터페이스 │ │ Claude API │ │
│ │ (Web UI/CLI) │◄──►│ - 스크린샷 분석 (Vision) │ │
│ └─────────────────────┘ │ - 작업 결정 │ │
│ │ - tool_use 응답 │ │
│ └─────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 에이전트 루프 │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ while not done: │ │ │
│ │ │ response = claude.messages.create(...) │ │ │
│ │ │ for tool_use in response: │ │ │
│ │ │ result = execute_tool(tool_use) │ │ │
│ │ │ messages.append(tool_result) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 컴퓨팅 환경 (Docker Container) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Xvfb │ │ Mutter │ │ Applications │ │ │
│ │ │ (가상 디스플레이)│ │ (창 관리자) │ │ Firefox, LibreOffice 등 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
한 번의 왕복을 단계별로 펼치면 다음과 같다.
실무 예제: 호출과 액션 정의
가장 단순한 형태는 스크린샷을 이미지 블록으로 messages에 함께 넣어 호출하는 함수다.
import anthropic
import base64
client = anthropic.Anthropic()
def call_computer_use(user_message: str, screenshot_base64: str = None):
"""Computer Use API 호출"""
messages = [{"role": "user", "content": user_message}]
# 스크린샷이 있으면 이미지로 추가
if screenshot_base64:
messages = [{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot_base64
}
},
{
"type": "text",
"text": user_message
}
]
}]
response = client.beta.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
betas=["computer-use-2025-01-24"],
tools=[
{
"type": "computer_20250124",
"name": "computer",
"display_width_px": 1024,
"display_height_px": 768,
"display_number": 1,
}
],
messages=messages
)
return response
Claude가 tool_use로 요청할 수 있는 액션 종류는 스크린샷·클릭·타이핑·키 입력·마우스 이동뿐 아니라, Sonnet 4 이상에서는 스크롤과 드래그도 포함한다. 줌(zoom)은 Opus 4.5 전용이다.
# 스크린샷 촬영
screenshot_action = {
"action": "screenshot"
}
# 마우스 클릭
click_action = {
"action": "left_click",
"coordinate": [500, 300]
}
# 텍스트 입력
type_action = {
"action": "type",
"text": "Hello, World!"
}
# 키보드 단축키
key_action = {
"action": "key",
"text": "ctrl+s" # Ctrl+S (저장)
}
# 마우스 이동
move_action = {
"action": "mouse_move",
"coordinate": [800, 600]
}
# 스크롤 (Claude Sonnet 4+)
scroll_action = {
"action": "scroll",
"coordinate": [500, 400],
"scroll_direction": "down",
"scroll_amount": 3
}
# 드래그 (Claude Sonnet 4+)
drag_action = {
"action": "left_click_drag",
"start_coordinate": [100, 100],
"end_coordinate": [300, 300]
}
# 더블 클릭
double_click_action = {
"action": "double_click",
"coordinate": [500, 300]
}
# 우클릭
right_click_action = {
"action": "right_click",
"coordinate": [500, 300]
}
# 대기
wait_action = {
"action": "wait",
"duration": 2 # 2초 대기
}
# 줌 (Claude Opus 4.5만)
zoom_action = {
"action": "zoom",
"region": [100, 200, 400, 350] # [x1, y1, x2, y2]
}
에이전트 루프 구현
실제 운영에 쓸 에이전트 루프는 최대 반복 횟수로 무한 루프를 막고, computer·bash·str_replace_based_edit_tool 세 도구를 묶어 처리한다. 모델 이름에 opus-4-5가 포함되는지로 도구 버전과 베타 플래그를 자동 분기하는 부분도 눈여겨볼 만하다.
import anthropic
import json
from typing import Optional
from abc import ABC, abstractmethod
class ComputerEnvironment(ABC):
"""컴퓨팅 환경 추상 클래스"""
@abstractmethod
def screenshot(self) -> str:
"""스크린샷을 base64로 반환"""
pass
@abstractmethod
def click(self, x: int, y: int) -> None:
"""좌표에서 클릭"""
pass
@abstractmethod
def type_text(self, text: str) -> None:
"""텍스트 입력"""
pass
@abstractmethod
def key_press(self, key: str) -> None:
"""키 입력"""
pass
@abstractmethod
def mouse_move(self, x: int, y: int) -> None:
"""마우스 이동"""
pass
class ComputerUseAgent:
"""Computer Use 에이전트"""
def __init__(
self,
environment: ComputerEnvironment,
model: str = "claude-sonnet-4-5",
display_width: int = 1024,
display_height: int = 768
):
self.client = anthropic.Anthropic()
self.env = environment
self.model = model
self.display_width = display_width
self.display_height = display_height
# 도구 버전 결정
if "opus-4-5" in model:
self.tool_version = "computer_20251124"
self.beta_flag = "computer-use-2025-11-24"
else:
self.tool_version = "computer_20250124"
self.beta_flag = "computer-use-2025-01-24"
def get_tools(self):
"""도구 정의 반환"""
return [
{
"type": self.tool_version,
"name": "computer",
"display_width_px": self.display_width,
"display_height_px": self.display_height,
"display_number": 1,
},
{
"type": "bash_20250124",
"name": "bash"
},
{
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool"
}
]
def execute_tool(self, tool_name: str, tool_input: dict) -> dict:
"""도구 실행 및 결과 반환"""
if tool_name == "computer":
return self._execute_computer_action(tool_input)
elif tool_name == "bash":
return self._execute_bash(tool_input)
elif tool_name == "str_replace_based_edit_tool":
return self._execute_text_editor(tool_input)
else:
return {"error": f"Unknown tool: {tool_name}"}
def _execute_computer_action(self, tool_input: dict) -> dict:
"""컴퓨터 작업 실행"""
action = tool_input.get("action")
try:
if action == "screenshot":
screenshot = self.env.screenshot()
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot
}
}
elif action == "left_click":
x, y = tool_input["coordinate"]
self.env.click(x, y)
screenshot = self.env.screenshot()
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot
}
}
elif action == "type":
text = tool_input["text"]
self.env.type_text(text)
screenshot = self.env.screenshot()
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot
}
}
elif action == "key":
key = tool_input["text"]
self.env.key_press(key)
screenshot = self.env.screenshot()
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot
}
}
elif action == "mouse_move":
x, y = tool_input["coordinate"]
self.env.mouse_move(x, y)
return {"result": f"Moved mouse to ({x}, {y})"}
elif action == "scroll":
direction = tool_input.get("scroll_direction", "down")
amount = tool_input.get("scroll_amount", 3)
screenshot = self.env.screenshot()
return {
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot
}
}
else:
return {"error": f"Unknown action: {action}"}
except Exception as e:
return {"error": str(e), "is_error": True}
def _execute_bash(self, tool_input: dict) -> dict:
"""Bash 명령 실행"""
import subprocess
command = tool_input.get("command", "")
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=30
)
output = result.stdout or result.stderr
return {"output": output, "return_code": result.returncode}
except Exception as e:
return {"error": str(e), "is_error": True}
def _execute_text_editor(self, tool_input: dict) -> dict:
"""텍스트 편집기 작업 실행"""
return {"result": "Text editor action executed"}
def run(self, user_message: str, max_iterations: int = 20) -> str:
"""
에이전트 루프 실행
Args:
user_message: 사용자 요청
max_iterations: 최대 반복 횟수 (무한 루프 방지)
Returns:
최종 응답 텍스트
"""
messages = [{"role": "user", "content": user_message}]
for iteration in range(max_iterations):
print(f"\n=== Iteration {iteration + 1} ===")
response = self.client.beta.messages.create(
model=self.model,
max_tokens=4096,
betas=[self.beta_flag],
tools=self.get_tools(),
messages=messages
)
print(f"Stop Reason: {response.stop_reason}")
messages.append({
"role": "assistant",
"content": [block.model_dump() for block in response.content]
})
tool_uses = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_uses:
for block in response.content:
if hasattr(block, 'text'):
return block.text
return "작업 완료"
tool_results = []
for tool_use in tool_uses:
print(f"Tool: {tool_use.name}")
print(f"Input: {json.dumps(tool_use.input, indent=2)}")
result = self.execute_tool(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": [result] if isinstance(result, dict) and result.get("type") == "image" else json.dumps(result)
})
messages.append({
"role": "user",
"content": tool_results
})
return "최대 반복 횟수 도달"
도구 핸들러: 로컬 테스트와 Docker 운영
로컬에서 빠르게 검증할 때는 PyAutoGUI로 실제 화면을 직접 제어하는 환경이 편하다. scale_factor로 좌표를 보정하고, 스크린샷은 API 최대 크기(1568px)를 넘지 않도록 리사이즈한다.
import pyautogui
import base64
from io import BytesIO
from PIL import Image
class LocalComputerEnvironment(ComputerEnvironment):
"""로컬 컴퓨터 환경 (테스트용 - 실제 화면 제어)"""
def __init__(self, scale_factor: float = 1.0):
self.scale_factor = scale_factor
pyautogui.FAILSAFE = True
pyautogui.PAUSE = 0.1
def screenshot(self) -> str:
"""스크린샷 캡처 및 base64 인코딩"""
screenshot = pyautogui.screenshot()
max_size = 1568
if max(screenshot.size) > max_size:
ratio = max_size / max(screenshot.size)
new_size = (int(screenshot.width * ratio), int(screenshot.height * ratio))
screenshot = screenshot.resize(new_size, Image.LANCZOS)
buffer = BytesIO()
screenshot.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode()
def click(self, x: int, y: int) -> None:
"""좌표에서 클릭"""
scaled_x = int(x / self.scale_factor)
scaled_y = int(y / self.scale_factor)
pyautogui.click(scaled_x, scaled_y)
def type_text(self, text: str) -> None:
"""텍스트 입력"""
pyautogui.typewrite(text, interval=0.05)
def key_press(self, key: str) -> None:
"""키 입력 (예: ctrl+s)"""
if '+' in key:
keys = key.split('+')
pyautogui.hotkey(*keys)
else:
pyautogui.press(key)
def mouse_move(self, x: int, y: int) -> None:
"""마우스 이동"""
scaled_x = int(x / self.scale_factor)
scaled_y = int(y / self.scale_factor)
pyautogui.moveTo(scaled_x, scaled_y)
운영 환경에서는 실제 데스크톱을 건드리지 않는 Docker 컨테이너가 기본이다. 컨테이너 내부에서 xdotool로 클릭·타이핑·키 입력을 실행하고, ImageMagick의 import로 Xvfb 디스플레이를 캡처한다.
import subprocess
import base64
class DockerComputerEnvironment(ComputerEnvironment):
"""Docker 컨테이너 기반 가상 환경"""
def __init__(self, container_name: str = "claude-computer-use"):
self.container = container_name
def _run_in_container(self, command: str) -> str:
"""컨테이너 내에서 명령 실행"""
result = subprocess.run(
["docker", "exec", self.container, "bash", "-c", command],
capture_output=True,
text=True
)
return result.stdout
def screenshot(self) -> str:
"""Xvfb 디스플레이 스크린샷"""
self._run_in_container(
"DISPLAY=:1 import -window root /tmp/screenshot.png"
)
result = self._run_in_container("base64 -w 0 /tmp/screenshot.png")
return result.strip()
def click(self, x: int, y: int) -> None:
"""xdotool로 클릭"""
self._run_in_container(
f"DISPLAY=:1 xdotool mousemove {x} {y} click 1"
)
def type_text(self, text: str) -> None:
"""xdotool로 텍스트 입력"""
escaped = text.replace("'", "'\\''")
self._run_in_container(
f"DISPLAY=:1 xdotool type '{escaped}'"
)
def key_press(self, key: str) -> None:
"""xdotool로 키 입력"""
xdo_key = key.replace('+', '+')
self._run_in_container(
f"DISPLAY=:1 xdotool key {xdo_key}"
)
def mouse_move(self, x: int, y: int) -> None:
"""xdotool로 마우스 이동"""
self._run_in_container(
f"DISPLAY=:1 xdotool mousemove {x} {y}"
)
화면 해상도가 API 제한(긴 변 1568px, 총 픽셀 약 1,150,000px)을 넘으면 스케일 팩터를 계산해 좌표를 양방향으로 변환해야 한다.
import math
def calculate_scale_factor(screen_width: int, screen_height: int) -> float:
"""
API 제한에 맞는 스케일 팩터 계산
API 제한:
- 가장 긴 가장자리: 1568px
- 총 픽셀: ~1,150,000px
"""
MAX_LONG_EDGE = 1568
MAX_PIXELS = 1_150_000
long_edge = max(screen_width, screen_height)
total_pixels = screen_width * screen_height
long_edge_scale = MAX_LONG_EDGE / long_edge
total_pixels_scale = math.sqrt(MAX_PIXELS / total_pixels)
return min(1.0, long_edge_scale, total_pixels_scale)
class ScaledComputerEnvironment:
"""좌표 스케일링을 처리하는 래퍼"""
def __init__(self, env: ComputerEnvironment, screen_width: int, screen_height: int):
self.env = env
self.screen_width = screen_width
self.screen_height = screen_height
self.scale = calculate_scale_factor(screen_width, screen_height)
self.scaled_width = int(screen_width * self.scale)
self.scaled_height = int(screen_height * self.scale)
def click(self, x: int, y: int) -> None:
"""스케일된 좌표를 원본 좌표로 변환 후 클릭"""
original_x = int(x / self.scale)
original_y = int(y / self.scale)
self.env.click(original_x, original_y)
보안 고려사항
Claude가 화면을 대신 조작한다는 것은 곧 프롬프트 주입 공격 표면이 넓어진다는 뜻이다. 웹페이지나 이미지에 숨겨진 지시에 Claude가 반응해 예상치 못한 작업을 수행할 가능성이 있으므로, 최소 권한 가상 환경·인터넷 접근 허용 목록·중요 작업 사람 확인·전체 작업 로깅을 기본으로 갖춰야 한다. 금융 거래, 법적 동의, 실계정 소셜 미디어 활동, 민감 개인정보 처리는 자동화 대상에서 제외한다.
┌─────────────────────────────────────────────────────────────────────────┐
│ 보안 체크리스트 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 필수 조치 │
│ ───────────────────────────────────────────────────────────────── │
│ □ 최소 권한 가상 머신/컨테이너 사용 │
│ □ 민감한 데이터 접근 차단 (로그인 정보, 개인 파일 등) │
│ □ 인터넷 접근 도메인 허용 목록 설정 │
│ □ 중요 작업에 대한 사람 확인 절차 │
│ □ 모든 작업 로깅 │
│ │
│ ⚠️ 주의사항 │
│ ───────────────────────────────────────────────────────────────── │
│ - 프롬프트 주입 공격 가능성 (웹페이지/이미지에 숨겨진 지시) │
│ - Claude가 예상치 못한 작업 수행 가능 │
│ - 금융 거래, 법적 동의 등은 자동화 금지 │
│ │
│ 🚫 금지 사항 │
│ ───────────────────────────────────────────────────────────────── │
│ - 실제 계정으로 소셜 미디어 활동 │
│ - 실제 결제/금융 거래 │
│ - 법적 동의가 필요한 작업 │
│ - 민감한 개인정보 처리 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
이 원칙을 코드로 옮기면 위험 키워드 차단, 허용 도메인 화이트리스트, 작업 로그, 중요 작업 확인 프롬프트 네 요소로 구성된다.
class SecureComputerEnvironment:
"""보안이 강화된 컴퓨터 환경"""
BLOCKED_ACTIONS = [
"delete",
"format",
"sudo",
"rm -rf",
]
ALLOWED_DOMAINS = [
"google.com",
"wikipedia.org",
]
def __init__(self, env: ComputerEnvironment):
self.env = env
self.action_log = []
def validate_action(self, action: str, params: dict) -> tuple[bool, str]:
"""작업 유효성 검증"""
if action == "key":
key = params.get("text", "").lower()
if any(blocked in key for blocked in ["delete", "format"]):
return False, "Blocked dangerous key combination"
if action == "type":
text = params.get("text", "").lower()
if any(blocked in text for blocked in self.BLOCKED_ACTIONS):
return False, "Blocked dangerous text input"
return True, "OK"
def log_action(self, action: str, params: dict, result: any):
"""모든 작업 로깅"""
import datetime
self.action_log.append({
"timestamp": datetime.datetime.now().isoformat(),
"action": action,
"params": params,
"result": str(result)[:100]
})
def execute_with_validation(self, action: str, params: dict) -> dict:
"""검증 후 실행"""
is_valid, message = self.validate_action(action, params)
if not is_valid:
self.log_action(action, params, f"BLOCKED: {message}")
return {"error": message, "is_error": True}
result = self._execute(action, params)
self.log_action(action, params, result)
return result
def require_human_confirmation(self, action: str, params: dict) -> bool:
"""중요 작업에 대한 사람 확인"""
CRITICAL_ACTIONS = ["key", "type"]
if action in CRITICAL_ACTIONS:
print(f"\n⚠️ 확인 필요: {action}")
print(f"파라미터: {params}")
confirm = input("실행하시겠습니까? (y/n): ")
return confirm.lower() == 'y'
return True
제한사항과 실무 팁
인간보다 상호작용이 느리고, 좌표를 가끔 잘못 짚거나 환각을 일으키며, 스크롤 작업이 불안정하고, 스프레드시트 셀 선택도 잘 미끄러진다. 프롬프트 주입 위험까지 더하면 아래 표의 대응 방안이 곧 최소 방어선이다.
| 제한사항 | 설명 | 대응 방안 |
|---|---|---|
| 지연 시간 | 인간보다 느린 상호작용 | 배경 작업, 테스트 자동화에 적합 |
| 좌표 정확도 | 가끔 좌표 실수/환각 | 각 단계 후 스크린샷으로 확인 |
| 스크롤 신뢰성 | 스크롤 작업 불안정 | scroll 작업 명시적 사용 |
| 스프레드시트 | 셀 선택 불안정 | 키보드 단축키 활용 |
| 프롬프트 주입 | 웹페이지 내 악성 지시 | 도메인 제한, 사람 확인 |
프롬프트는 순서가 명확한 단계별 지시로 써야 한다. "구글에서 뭔가 검색해줘" 같은 모호한 지시는 좌표 실수와 잘못된 판단을 늘린다.
# 좋은 예
prompt = """
다음 단계를 순서대로 수행하세요:
1. Firefox를 엽니다
2. google.com으로 이동합니다
3. 검색창에 "Claude AI"를 입력합니다
4. 검색 버튼을 클릭합니다
5. 각 단계 후 스크린샷을 촬영하여 결과를 확인하세요
"""
# 나쁜 예
prompt = "구글에서 뭔가 검색해줘"
각 단계 뒤에 결과를 명시적으로 검증하게 만드는 프롬프트를 더하면 오류가 누적되기 전에 잡아낼 수 있다.
verification_prompt = """
각 단계 후에 스크린샷을 촬영하고 올바른 결과를 달성했는지 신중하게 평가하세요.
명시적으로 생각을 보여주세요: "나는 X 단계를 평가했습니다..."
올바르지 않으면 다시 시도하세요.
단계가 올바르게 실행되었음을 확인한 후에만 다음 단계로 이동하세요.
"""
마우스 클릭보다 키보드 단축키가 더 안정적으로 동작한다. Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), Tab(다음 필드), Enter(확인) 등을 우선 활용한다. 해상도는 무작정 높이지 말고 용도에 맞춰 고른다 — 1920x1080 이상에서는 성능 문제가 나타날 수 있다.
RECOMMENDED_RESOLUTIONS = {
"general": (1024, 768), # 일반 데스크톱
"web": (1280, 800), # 웹 애플리케이션
"wide": (1366, 768), # 와이드 스크린
}
# 1920x1080 이상은 성능 문제 발생 가능
에러 처리도 재시도 로직으로 감싸는 편이 안전하다.
def execute_with_retry(self, action: str, params: dict, max_retries: int = 3):
"""재시도 로직이 포함된 실행"""
for attempt in range(max_retries):
try:
result = self.execute(action, params)
if not result.get("is_error"):
return result
except Exception as e:
if attempt == max_retries - 1:
return {"error": str(e), "is_error": True}
time.sleep(1)
비용 구조와 도입 원칙
스크린샷 기반으로 동작하는 특성상 이미지 처리 비용이 누적되기 쉽다. 시스템 프롬프트 오버헤드가 466~499 토큰, computer 도구 정의 자체가 735 토큰을 차지하고, 여기에 스크린샷마다 Vision 가격이 더해진다. max_iterations로 루프 반복을 제한하고 불필요한 스크린샷 촬영을 줄이는 것이 비용 관리의 핵심이다.
| 항목 | 토큰 수 |
|---|---|
| 시스템 프롬프트 오버헤드 | 466-499 토큰 |
| computer 도구 정의 | 735 토큰 |
| 스크린샷 이미지 | Vision 가격 적용 |
실무에 들일 때 가장 중요한 원칙은 샌드박스 격리와 최소 권한이다. Docker 컨테이너 기반 가상 환경을 기본으로 삼고, 허용 도메인 목록과 위험 작업 차단 로직을 반드시 구현하며, 금융 거래나 법적 동의처럼 되돌리기 어려운 작업에는 사람의 확인 단계를 끼워 넣어야 한다. 아직 베타 단계이지만 GUI 테스트 자동화, 레거시 시스템 인터페이스 통합, 반복 업무 자동화 같은 에이전트 워크플로우의 구성 요소로 빠르게 자리 잡고 있다.