웹 스크래퍼가 계속 고장 나는 이유 (LLM과 Playwright를 사용하여 자가 치유(Self-Healing) TypeScript 에이전트를
요약
웹 사이트의 DOM 변경으로 인해 발생하는 스크래핑 오류를 해결하기 위해 LLM과 Playwright를 활용한 자가 치유(Self-Healing) TypeScript 에이전트 구축 방법을 소개합니다. 시각적 접지와 멀티모달 분석을 통해 동적인 웹 환경에서도 탄력적으로 작동하는 아키텍처를 다룹니다.
핵심 포인트
- 전통적인 CSS/XPath 기반 스크래핑의 구조적 취약성 분석
- LLM의 시각적 접지 및 멀티모달 분석을 통한 자가 치유 메커니즘
- 모놀리식 구조에서 에이전트형 마이크로서비스로의 아키텍처 전환
- TypeScript와 Playwright를 이용한 프로덕션급 에이전트 구현
만약 여러분이 프로덕션 환경의 웹 스크래핑 파이프라인(web scraping pipeline)이나 자동 양식 채우기 어시스턴트(automated form-filling assistant)를 유지 관리해 본 적이 있다면, 월요일 아침 로그를 확인했을 때 빨간색 오류 메시지가 가득한 것을 보며 느끼는 그 절망감을 알고 있을 것입니다. 프론트엔드 엔지니어가 클래스 속성(class attribute)을 btn-primary에서 btn-action-primary로 변경했거나, A/B 테스트 프레임워크가 DOM 트리 계층 구조를 변경했거나, 혹은 사소한 React 컴포넌트 업데이트가 CSS 셀렉터(CSS selectors)를 무작위로 바꾸어 버렸을 수도 있습니다.
순식간에 여러분의 자동화 스크립트는 산산조각이 납니다. 셀렉터(selector)는 해결되지 않고, 런타임 예외(runtime exception)가 발생하며, 전체 데이터 파이프라인(data pipeline)이 멈춰버립니다.
엄격한 CSS 셀렉터(CSS selectors), XPath 표현식, 또는 경직된 좌표 기반 클릭(coordinate-based clicks)을 기반으로 구축된 전통적인 자동화 아키텍처는 웹을 결정론적 상태 머신(deterministic state machine)으로 취급합니다. 하지만 현대의 웹은 결코 결정론적이지 않습니다. 웹은 유동적이고, 동적이며, 끊임없이 변화합니다.
이러한 구조적 취약성을 극복하기 위해, 현대적인 에이전트 시스템(agentic systems)은 패러다임의 전환을 요구합니다. 대규모 언어 모델(LLM)의 시각적 접지(visual grounding), 모델 컨텍스트 프로토콜(MCP) 도구 표준화, 그리고 WebGPU 컴퓨트 셰이더(WebGPU Compute Shaders)를 통한 로컬 하드웨어 가속을 결합함으로써, 우리는 의미론적 탄력성(semantic resilience)을 가진 TypeScript 에이전트를 구축할 수 있습니다. DOM 변형(mutation)으로 인해 셀렉터가 깨지더라도 에이전트는 충돌하지 않습니다. 대신 시각적 스냅샷을 캡처하고, 멀티모달 분석(multimodal analysis)을 통해 공간 레이아웃을 처리하며, 실행 경로를 동적으로 자가 치유(self-heals)합니다.
자가 치유형 웹 스크래퍼의 아키텍처를 깊이 있게 살펴보고, 깨진 셀렉터 앞에서도 당당하게 작동하는 프로덕션급 TypeScript 양식 채우기 어시스턴트를 구축해 보겠습니다.
아키텍처의 진화: 모놀리스에서 에이전트형 마이크로서비스로
자가 치유형 웹 에이전트가 어떻게 작동하는지 진정으로 이해하려면, 백엔드 시스템에서의 평행한 아키텍처 진화 과정을 살펴보는 것이 도움이 됩니다. 즉, 모놀리식 애플리케이션(monolithic applications)에서 지능형 API 게이트웨이(API Gateways)에 의해 관리되는 마이크로서비스(microservices)로의 전환을 살펴보는 것입니다.
모든 내부 모듈이 다른 모듈의 정확한 메모리 주소와 내부 메서드 시그니처(method signatures)를 직접 참조하는 레거시 모놀리식(monolithic) 웹 애플리케이션을 상상해 보십시오. 만약 모듈 A가 사용자 인증 함수의 시그니처를 업데이트한다면, 의존성이 있는 모든 모듈은 동시에 수동으로 리팩터링(refactoring)되고 재컴파일(recompiled)되어야 합니다. 이는 특정 CSS 선택자(selectors)에 하드코딩된 전통적인 웹 스크래퍼의 아키텍처적 상황과 정확히 일치합니다. 스크래퍼가 대상 웹사이트의 특정 DOM 구현 세부 사항에 모놀리식하게 결합되어 있는 것입니다.
이제 이를 서비스 디스커버리(service discovery)와 스키마 협상(schema negotiation)을 활용하는 API 게이트웨이(API Gateway)에 의해 중재되는 현대적인 마이크로서비스(microservices) 아키텍처와 대조해 보십시오. 업스트림(upstream) 마이크로서비스가 내부 라우팅이나 데이터 직렬화(serialization) 형식을 변경할 때, API 게이트웨이는 요청을 가로채고, 동적 계약(dynamic contract)을 평가하며, 의도 라우팅(intent routing)을 위해 의미론적 변환 계층(semantic transformation layers)을 사용하고, 다운스트림(downstream) 소비자에게 영향을 주지 않으면서 즉석에서 페이로드(payload)를 조정합니다.
브라우저 자동화 영역에서 모델 컨텍스트 프로토콜(Model Context Protocol, MCP)은 이러한 지능형 API 게이트웨이 역할을 합니다. 자율 에이전트(autonomous agent)는 하드코딩된 메모리 포인터나 취약한 선택자를 통해 DOM과 상호작용하지 않습니다. 대신, 표준화된 도구 계약(tool contracts)을 통해 통신합니다. UI가 변이(mutate)될 때, 에이전트의 시각 기반 인지 계층(vision-driven perception layer)은 동적 스키마 어댑터(dynamic schema adapter) 역할을 하여, 웹 페이지의 새로운 시각적 및 구조적 현실을 실행 가능한 의미론적 의도(semantic intents)로 번역합니다. 회복 탄력성이 있는 마이크로서비스 아키텍처가 백엔드 변경 사항을 클라이언트 애플리케이션으로부터 격리하는 것처럼, MCP 기반의 시각 에이전트는 구조적인 웹 변경 사항을 핵심 추출 로직으로부터 격리합니다.
클라이언트 측 가속: WebGPU 및 컴퓨트 셰이더 (Compute Shaders)
에이전트가 더욱 자율적으로 변함에 따라, 사소한 DOM 조정마다 원격 LLM API로 왕복(round-trips)해야 하는 빈도가 심각한 지연 시간(latency) 병목 현상을 초래합니다. 실시간의 유연한 브라우저 자동화를 달성하기 위해, 현대적인 TypeScript 아키텍처는 WebGPU를 통한 브라우저 네이티브 하드웨어 가속을 활용합니다.
WebGPU는 클라이언트의 GPU에 대해 오버헤드가 적고 고성능인 접근을 제공하여, 이전의 WebGL 구현에 내재된 CPU 병목 현상을 우회합니다. 자가 치유 (Self-healing) 스크래퍼의 경우, WebGPU는 클라이언트 측 임베딩 생성 (Embedding generation) 및 경량 시각-언어 모델 (Vision-language model) 추론을 위한 실행 계층 역할을 합니다. WebGPU 컴퓨트 셰이더 (Compute Shader)를 활용하면, 브라우저는 로컬 하드웨어에서 대규모로 병렬화된 텐서 연산 (Tensor operations)을 직접 실행할 수 있습니다.
양식 채우기 (Form-filling) 어시스턴트가 새로 발견된 입력 필드가 "Billing Address Line 1"에 해당하는지 평가해야 할 때, 어시스턴트는 주변의 DOM 컨텍스트와 시각적 크롭 (Visual crop) 영역을 로컬 벡터 공간 (Vector space)으로 투영할 수 있습니다. 저지연 유사도 검색 (Low-latency similarity search)에 최적화된 모델을 채택함으로써, 에이전트는 밀리초 단위로 알려진 스키마 레지스트리 (Schema registry)와 코사인 유사도 (Cosine similarities)를 계산합니다. 이러한 로컬 실행 루프는 의미론적 복구 (Semantic recovery)가 상호작용 가능한 속도로 이루어지도록 보장하며, 네트워크 지연 시간과 클라우드 API 속도 제한 (Rate limits)으로부터 자동화 파이프라인을 보호합니다.
RAG 확장하기: 정적 PDF에서 살아있는 사용자 인터페이스로
이전의 아키텍처 패턴에서 검색 증강 생성 (RAG, Retrieval-Augmented Generation)은 비정형 문서가 어떻게 청킹 (Chunking)되고, 고차원 벡터 공간에 임베딩되며, 벡터 데이터베이스에 저장되고, 유사도 지표를 통해 검색되어 LLM의 응답을 사실적 컨텍스트에 근거하게 만드는지를 정의했습니다.
우리는 이 정확한 기초 모델을 정적인 문서 청크에서 동적이고 살아있는 사용자 인터페이스 (User Interfaces)로 확장할 수 있습니다.
표준 RAG 파이프라인에서 코퍼스 (Corpus)는 텍스트 파일이나 PDF로 구성됩니다. 반면 자가 치유 웹 스크래퍼에서 코퍼스는 웹 페이지 그 자체이며, 이는 DOM 트리 (구조적 텍스트)와 렌더링된 뷰포트 (시각적 픽셀)로 구성된 이중 표현 방식입니다.
- UI 청킹 (Chunking the UI): 텍스트를 단락별로 나누는 대신, 에이전트는 DOM을 상호작용 가능한 원자적 단위(버튼, 입력창, 레이블, 컨테이너)로 파싱하고 해당 요소들의 경계 상자 (Bounding Boxes)를 캡처합니다.
- 상태 임베딩 (Embedding the State): 각 상호작용 요소는 의미론적 역할 (Semantic Role), 접근성 속성 (ARIA labels), 주변 텍스트 문맥 및 시각적 크롭 (Visual Crop) 정보로 풍부해집니다. 이러한 요소들은 공유 벡터 공간 (Shared Vector Space)에 임베딩됩니다.
- 검색 기반 상호작용 (Retrieval-Based Interaction): 전통적인 셀렉터 (Selector)가 실패할 때, 에이전트는 에러를 발생시키지 않습니다. 대신 누락된 요소의 의도 (Intent) (예: "결제 제출")를 사용하여 새롭게 변형된 DOM 요소들에 대해 내부 벡터 공간을 쿼리합니다. 벡터 데이터베이스는 의미론적 및 시각적 근접성을 기반으로 가장 높은 점수를 가진 후보를 반환하며, 이를 통해 에이전트는 중단 없이 동작을 수행할 수 있습니다.
인간은 가공되지 않은 HTML 소스 코드를 읽거나 DOM 자식 인덱스를 세면서 웹사이트를 탐색하지 않습니다. 인간 사용자는 렌더링된 뷰포트를 보고, 시각적 어포던스 (Visual Affordances) (예: "결제하기"라고 적힌 흰색 텍스트가 있는 파란색 직사각형 버튼)를 식별하며, 그러한 시각적 인식을 바탕으로 행동합니다. 자가 치유 (Self-healing) 스크래퍼는 멀티모달 인지 루프 (Multimodal Perception Loops)를 통해 이러한 인간 중심적 패러다임을 복원합니다.
회복 탄력성이 있는 TypeScript 양식 채우기 어시스턴트 구축하기
TypeScript, Playwright, 그리고 Google GenAI SDK를 사용하여 회복 탄력성이 있는 양식 채우기 어시스턴트를 구축하는 실질적인 엔드 투 엔드 (End-to-End) 구현 사례를 살펴보겠습니다. 이 패턴은 기업용 SaaS 환경에서 자동화된 사용자 온보딩 (Onboarding), 경쟁사 가격 정보 수집, 또는 다단계 결제 확인 등을 위해 흔히 사용됩니다.
아래는 동적인 웹 페이지의 스크린샷을 찍고, 해당 시각적 문맥을 DOM 폴백 (Fallback) 상태와 함께 퓨샷 프롬프팅 (Few-Shot Prompting)을 사용하여 LLM에 전달하며, 반환된 구조화된 좌표 또는 CSS 셀렉터를 파싱하고, 헤드리스 브라우저 래퍼를 통해 자가 치유 클릭 동작을 실행하는 과정을 시뮬레이션하는 완전한 실행 가능 TypeScript 구현 코드입니다.
import { chromium, Page } from 'playwright';
import { GoogleGenAI } from '@google/genai';
...
{% endraw %}
json/g, '').replace(/\n{% raw %}.*?`\n/g, '').trim();
const target: ElementTarget = JSON.parse(cleanedJsonString);
console.log(`[LLM Self-Healed] Found target via vision at coordinates (${target.x}, ${target.y}) using new selector: ${target.selector}`);
...
## 코드 구현 상세 분석 (Detailed Breakdown of the Code Implementation)
1. **`interface ElementTarget`**: 멀티모달 LLM으로부터 반환되는 예상 JSON 응답 페이로드에 대한 엄격한 TypeScript 계약(contract)을 정의합니다. 이를 통해 좌표와 CSS 선택자 추출 시 타입 안정성(type safety)을 보장합니다.
2. **`interface FewShotExample`**: Few-Shot Prompting의 구조적 패턴을 확립하며, 원시 HTML DOM 스니펫과 사용자 의도 문자열, 그리고 이상적인 JSON 타겟 출력을 쌍으로 묶습니다.
3. **`class SelfHealingFormAssistant`**: 브라우저 자동화 세션 전체 수명 주기(lifecycle)를 캡슐화하여 Playwright와 Google GenAI SDK 간에 깨끗한 상태 경계(state boundaries)를 유지합니다.
4. **`constructor()`**: 환경 기반 자격 증명 로딩(`process.env.GEMINI_API_KEY`)을 사용하여 `GoogleGenAI` 클라이언트를 인스턴스화하며, 하드코딩된 키 없이 안전한 런타임 구성을 보장합니다.
5. **`public async initialize()`**: Playwright를 통해 헤드리스 Chromium 브라우저 인스턴스를 실행하고, 일관된 스크린샷 생성 및 레이아웃 렌더링을 위해 표준 데스크톱 뷰포트(`1280x800`)로 구성합니다.
6. **`private getFewShotContext()`**: 하드코딩된 몇 가지 예시(few-shot examples) 배열을 반환합니다. 이는 LLM에게 사용자 프롬프트 컨텍스트 내에서 직접 학습시켜, 혼란스러운 엔터프라이즈 HTML로부터 정확한 좌표 매핑 및 대체 선택자(fallback selectors)를 추출하는 방법을 시연합니다.
7. **`public async locateAndAct(...)`**: 핵심 작동 메서드입니다. 고수준의 사용자 의도 문자열과 원하는 DOM 요소를 가리킨다고 가정되는 표준 CSS 선택자를 인수로 받습니다.
8.
**`const element = await this.page.$(standardSelector)`**: Playwright의 표준 DOM 쿼리 엔진을 사용하여 비용이 들지 않는 빠른 초기 확인을 수행합니다. 요소가 존재하면 스크립트는 비용이 많이 드는 LLM 처리를 완전히 건너뜁니다.
9. **`console.warn(...)`**: 표준 CSS 선택자가 실패할 때 명확한 운영 경고를 로그로 남깁니다. 이는 SaaS UI가 변경되었거나 리팩토링되었을 가능성을 나타내며, 자가 치유 (Self-healing) 파이프라인을 트리거합니다.
10. **`const screenshotBuffer = await this.page.screenshot(...)`**: 현재 브라우저 뷰포트의 실시간 바이너리 PNG 버퍼를 캡처합니다. 이 시각적 데이터는 시각 기능이 있는 LLM을 위한 기초 입력값 역할을 합니다.
11. **`const pageHtml = await this.page.content()`**: 시각적 스크린샷과 함께 구조적 맥락을 제공하기 위해 현재 DOM 트리의 전체 HTML 문자열을 가져옵니다.
12. **`const domSnippet = pageHtml.slice(0, 4000)`**: 원본 HTML 문서를 4,000자로 자릅니다. 이는 토큰 폭발을 방지하고, 중요한 구조적 앵커를 유지하면서 프롬프트 컨텍스트 창 (Prompt context window) 내에 안전하게 머물도록 합니다.
13. **`const fewShots = this.getFewShotContext()`**: 모델의 출력 형식 동작을 준비하기 위해 퓨샷 (Few-shot) 학습 배열을 가져옵니다.
14. **`const prompt = `...\u0060\u0060`**: 시스템 지침, 퓨샷 (Few-shot) 예시, 현재 사용자 의도, 대상 요소 설명 및 잘라낸 DOM 스니펫을 포함하는 포괄적인 템플릿 문자열을 구성합니다.
15. **`const response = await this.ai.models.generateContent(...)`**: 바이너리 이미지 버퍼(`inlineData`로 래핑됨)와 텍스트 프롬프트를 모두 포함하는 배열을 전달하여 멀티모달 Gemini API(`gemini-2.5-flash`)를 호출합니다.
16. **`const responseText = response.text()`**: JSON 페이로드가 포함된 모델의 원시 텍스트 문자열을 추출합니다.
17. **`const cleanedJsonString = ...`**: 마크다운 코드 블록 래퍼(예: `\u0060json ... `\u0060)를 제거하여 모델 출력을 정화함으로써 JSON 파싱 오류를 방지합니다.
18.
**`const target: ElementTarget = JSON.parse(cleanedJsonString)`**: 정제된 문자열을 강력한 타입(strongly-typed)의 `ElementTarget` JavaScript 객체로 역직렬화(Deserializes)합니다.
19. **`if (target.confidence > 0.75)`**: 엄격한 에이전트 거버넌스(Governance)를 강제합니다. 만약 모델이 요소의 시각적 위치에 대해 확신하지 못할 경우, 시스템은 클릭 실행을 거부하여 프로덕션 SaaS 플랫폼에서 발생할 수 있는 의도치 않은 부작용을 방지합니다.
20. **`await this.page.mouse.click(target.x, target.y)`**: 시각 모델(Vision model)에 의해 계산된 정확한 픽셀 좌표에서 물리적인 마우스 클릭을 실행하며, 고장 난 DOM 셀렉터(Selectors)를 완전히 우회합니다.
21. **`catch (error)`**: 실행 이상, 런타임 타임아웃(Runtime timeouts) 또는 JSON 파싱 실패를 포착하여, 전체 Node.js 프로세스를 중단시키지 않고 치명적인 오류를 로깅합니다.
22. **`public async close()`**: Playwright 브라우저 컨텍스트(Browser context)를 종료하고 메모리를 해제하여 리소스를 정리합니다.
23. **`(async () => { ... })()`**: 클래스 인스턴스 테스트를 위한 진입점 역할을 하는 즉시 실행 비동기 함수 표현식(IIFE, Immediately Invoked Async Function Expression)입니다.
## 거버넌스(Governance), 보안(Security) 및 샌드박싱(Sandboxing) 고려 사항
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기