
【AI 에이전트】 25건을 가져왔습니다. 실제로는 432건이 있었습니다
요약
AI 에이전트가 조작 API의 반환값(return value)만으로 실제 상태를 오판하는 사례를 분석합니다. 가상 스크롤이나 검색 결과 누락 등, 성공 응답이 실제 데이터의 완전성을 보장하지 않는 기술적 한계와 주의점을 다룹니다.
핵심 포인트
- API의 성공 반환값이 실제 작업의 완전성을 의미하지 않음
- 가상 스크롤 환경에서 DOM 기반 데이터 수집 시 누락 위험 존재
- 검색 결과가 0건이라도 실제 데이터가 존재할 수 있는 예외 상황 주의
- 데이터 검증 시 단순 건수가 아닌 고유 식별자(URL 등)를 활용할 것

25건밖에 보이지 않는 목록, 1건밖에 반환되지 않는 Accessibility Tree (액세스빌리티 트리), 실제로 존재하는 문자열을 0건이라고 반환하는 검색. 이 세 가지에서는 에러가 발생하지 않았습니다. 네 번째는 타임아웃입니다. 에러는 반환되었지만, 그럼에도 무엇이 일어났는지는 알 수 없었습니다.
이 시리즈에서는 자동화의 반환값(return value)만으로는 실제 상태를 판단할 수 없는 상황을 다룹니다. 첫 번째는 배포(deploy), 두 번째는 exit 0과 HTTP 200, 세 번째가 브라우저입니다.
성공이 반환되어도, 에러가 반환되어도, 그것만으로는 실제 상태를 확정할 수 없는 경우가 있습니다. 문제는 조작의 성패만이 아닙니다. 그 후에 에이전트가 확정한 '결과' 쪽이었습니다.
| 관측된 결과 | 실제로 확인해야 할 것 |
|---|---|
| 가져오기에 성공했다 | 전건을 가져왔는가 |
| 클릭할 수 있었다 | 올바른 대상을 클릭했는가 |
| 입력할 수 있었다 | 올바른 문자열이 들어갔는가 |
| 타임아웃되었다 | 처리가 실행되었는지 여부 |
이하는 2026년 7월 22일부터 8월 1일까지 실제로 겪은 사례입니다. 조작 API의 반환값만으로는 실제 상태를 확정할 수 없었던 예이며, 망라적이거나 배타적인 것은 아닙니다. 여러 가지가 동시에 일어날 수도 있습니다.
건수, 화면 크기, 타임아웃 시간은 실제 업무에서 기록한 값입니다. 비율은 거기서 계산된 값입니다. Claude의 상한값과 리사이즈 사양은 공식 문서에 기반합니다. 대상은 로그인이 필수이므로 재현 절차는 제시할 수 없습니다. 대신 검산·측정 코드를 덧붙입니다. 코드 자체에도 함정이 있기 때문에, 성립 조건과 무너지는 방식까지 작성합니다.
증상 1: 획득 성공 ≠ 전건 획득
구인 사이트의 목록을 Accessibility Tree (액세스빌리티 트리)를 통해 읽게 했습니다. 카테고리를 전환할 때마다 '해당 1건'이라고 반환됩니다. 에러도 경고도 없습니다. 실제로는 1페이지 최대 20건으로, 6페이지를 순회하니 합계 102건이 있었습니다.
그 목록은 Virtual Scroll (가상 스크롤)이었습니다. 확인 시점에는 화면 밖에 있는 행이 DOM에서 벗어나 있었습니다. 반환된 1건 자체는 실재하는 항목입니다. 문제는 그것을 목록 전체로서 다루는 순간 발생합니다.
다른 Web App (웹 앱)에서도 동일한 유형을 겪었습니다. 채팅 목록의 최초 DOM은 25건입니다. 스크롤을 반복하자 432건까지 늘어났습니다. 25건으로 확정했다면, 실재하는 상대의 94%를 '없음'이라고 판정했을 것입니다.
어떤 구인 사이트에서는 '기사'라고 검색해도 SEO 기사 집필을 포함하는 안건이 나오지 않았습니다. 적어도 문자열의 포함 여부만으로 '없다'고 판단할 수 있는 검색은 아닙니다. Google Sheets (구글 스프레드시트)에서도 Ctrl+F가 0/0을 반환했는데, 그 문자열은 실제로 존재했습니다.
완전성을 확인하지 않은 획득 결과만으로는 부재를 판단할 수 없습니다. 이번 검색에서는 해당 사항 없음이 빈 결과로 취급되었고, 예외(exception)가 발생하지 않았습니다.
검산: 모집단의 측면에서 세기
DOM에서 수집한 URL의 총수가 사이트 표시의 전체 102건과 일치하여, 겨우 '셀 수 있게 되었다'고 판단했습니다. 페이지별 건수를 더해서는 안 됩니다. 동일한 안건이 여러 페이지에 나타나면 중복이 사라지지 않기 때문입니다.
세는 것은 건수가 아니라 URL입니다. 순회 중에 발견한 URL을 Set (집합)에 저장합니다.
sessionStorage.removeItem('__ew_jobAudit_v1');
이것은 감사를 시작할 때 한 번만 실행합니다. 아래의 수집 코드를 반복할 때는 실행하지 않습니다.
(() => {
const key = '__ew_jobAudit_v1';
const urls = new Set(JSON.parse(sessionStorage.getItem(key) ?? '[]'));
...
- 전제: 동일 Origin (오리진)·동일 탭에서 순회합니다 (
sessionStorage는 리로드(reload)를 거쳐도 남습니다). Virtual Scroll (가상 스크롤)에서는 행이 DOM에서 사라지기 전에 중간에 몇 번이고 실행합니다. 마지막까지 보낸 후에 한 번 세면, 이 글의 증상 1을 겪게 됩니다. 동일 안건에 여러 URL 표현이 있다면 안건 ID로 중복을 제거합니다. Selector (셀렉터)는 대상 사이트에 맞춰 변경합니다 (예시는href가/jobs/로 시작하는 사이트용입니다). - 한계: 순회 중에 게재 종료나 신규 추가가 있는 사이트에서는 동일 시점의 스냅샷이 아니라, 순회 기간 중에 관측한 URL의 합집합이 됩니다.
- 무너지는 방식: 놓치면 예외 없이 과소하게, 리셋을 잊으면 예외 없이 과대하게 됩니다. Storage API (스토리지 API)를 사용할 수 없거나 용량 초과 시에는 예외를 반환하며, 저장된 값이 잘못된 JSON이라면
JSON.parse()에서 멈춥니다.
일치는 방증일 뿐 증명이 아닙니다. 사이트 표시 캐시나 집계 누락은 별도로 해결해야 합니다. 해당 사이트는 게시물 16,267건이라고 표시하면서도, 모집 중인 항목은 102건뿐이었습니다. 비율은 0.63%입니다. 큰 숫자가 반드시 재고를 의미하는 것은 아닙니다.
증상 2: 클릭 성공 ≠ 대상 선택 성공
Google 스프레드시트(Google Sheets)를 좌표 클릭으로 조작하게 했더니, 한 행이 밀려 옆 행을 선택했습니다. 그대로 Delete 입력이 이어지면서 기존 데이터를 덮어썼습니다.
확인된 결과, 좌표계에 스케일(Scale) 차이가 있었습니다. 스크린샷은 1568×744였고, 실제 뷰포트(Viewport)는 2231×1059였습니다. 가로세로 모두 약 1.423배 차이가 납니다.

가로 방향으로도 4090px, 최대로는 약 1열 정도 밀렸습니다. 폭이 300px을 넘는 열에서는 같은 열 안에 들어오는 경우도 있습니다. 이번 세션에서는 폭이 8090px 정도인 열에서 옆 열까지 침범하는 사례가 있었습니다.
Canvas 드로잉이 주를 이루는 앱에서는 개별 표시물을 DOM 요소로 참조할 수 없는 경우가 있습니다. 필자의 환경에서는 셀 그룹이 접근성 트리(Accessibility Tree) 상에서 하나의 콤보 박스(Combobox)로 보입니다. 그 때문에 좌표 클릭이 유일한 수단처럼 보였으나, 그 수단 자체가 어긋나 있었습니다.
Name Box(셀 참조 입력란)에서도 동일한 현상이 발생합니다. 클릭이 약간 빗나가면서 직전 셀이 선택된 상태로 W26이라고 타이핑했습니다. 이 경우 키 입력이 멈추지 않고 해당 문자열이 직전 셀에 입력되었습니다.
축소가 어디서 일어났는지는 분리하여 검증하지 않음
먼저 결론부터 말씀드리면, Claude에게 2231×1059 이미지가 전달되었다면 표준 티어(Standard Tier)의 리사이징(Resizing)으로 관측값을 설명할 수 있습니다. 고해상도 티어(High-resolution Tier)로는 설명할 수 없습니다. 다만, 전달되기 직전의 이미지 치수와 모델의 티어를 기록하지 않았기에 원인을 확정 지을 수는 없는 상태입니다.
Claude의 좌표 사양에 따르면, 상한을 초과하는 이미지는 종횡비(Aspect Ratio)를 유지하며 축소합니다. 반환되는 좌표는 축소된 이미지를 기준으로 합니다. 표준 티어는 장변 1568px·시각 토큰(Visual Token) 1568이며, 고해상도 티어는 2576px·4784입니다.
공식 레퍼런스 구현(Reference Implementation)에 2231×1059를 표준 티어로 통과시키면 1568×744가 됩니다. 이는 관측값과 일치합니다. 고해상도 티어라면 3040 토큰으로 상한 이내이므로 축소되지 않습니다.
하지만 2231×1059는 실제 뷰포트의 값입니다. 이것이 그대로 Claude에게 전달되었는지는 확인하지 않았습니다. 에이전트 측에서 먼저 축소되었을 가능성도 남아 있습니다. 이 일치만으로는 원인을 단정할 수 없습니다.
계측: 페이지 측의 좌표계를 기록하기
축소율을 기대값만으로 결정하지 말고, 실제로 반환된 이미지 치수와 대조해야 합니다.
// 페이지 측의 값. 스크린샷 이미지의 실제 픽셀 너비·높이와 별도로 비교
JSON.stringify({
innerWidth: window.innerWidth,
...
- 전제: 이 코드 단독으로는 축소율을 확정할 수 없습니다.
innerWidth × devicePixelRatio가 이미지의 너비와 일치한다는 보장이 없기 때문입니다(OS의 디스플레이 확대율이나 전달 측의 축소가 개입될 수 있습니다). 이미지의 실제 치수를 별도로 기록하여 가로세로 각각의 비율을 구해야 합니다. - 붕괴 방식: 예외는 발생하지 않습니다. 기대값만 믿고 진행하면 잘못된 축소율을 기반으로 좌표를 계속 전송하게 됩니다.
운용 시에는 좌표에 의존하지 않는 방법을 우선시했습니다. Google 스프레드시트라면 URL의 프래그먼트(Fragment)에 범위를 작성하여 이동할 수 있습니다.
형식이 틀려도 에러가 발생하지 않고 시트의 맨 처음이 열렸습니다. 이동 후에 현재 위치를 다시 읽어옵니다. 셀 이동도 화살표 키나 Tab 키를 우선적으로 사용합니다. 첫 번째 셀을 실수로 편집하는 사고를 방지하기 위해서입니다.
증상 3: 입력 성공 ≠ 값의 일치
지원 양식에 일본어 장문을 타이핑하게 했더니, 4곳에서 누락, 치환, 혼입을 확인했습니다. 에러는 전혀 발생하지 않았습니다. 전송 전에 육안으로 확인하지 않았다면 알아차릴 수 없었을 것입니다.
「24세」 → 「7726세」
「곧(もうすぐ)」 → 「모우스에(もうすぇ)」
「학생(学生)」 → 「자생(字生)」
...
동일한 내용을 값 직접 설정(Direct Setting) 방식으로 다시 입력했을 때는 이번 세션에서 결손이 재현되지 않았습니다.
스프레드시트의 셀에서도 유사한 결손이 나타났습니다. 일본어를 타이핑하면 앞의 두 글자가 사라지는 현상이 3회 연속 재현되었습니다. 셀을 선택하고 Delete를 누른 뒤, F2를 눌러 편집 모드로 들어갑니다. 그 후에 타이핑하고 Enter로 확정하면 글자가 사라지지 않았습니다.
하지만 F2를 누른 직후에 지체 없이 타이핑하면 재발했습니다. 편집 모드로 진입하기 전에 입력이 시작되었기 때문입니다.
어떤 서비스는 「Shift + Enter로 전송 / Enter로 줄바꿈」이라고 표시하고 있었습니다. 일반적인 채팅과는 반대입니다. 줄을 바꾸려는 의도로 누르면, 거기까지의 첫 번째 줄만 전송되었습니다. 전송 후에는 입력란이 초기화되었고, 후속 입력은 무시되었습니다. 게시물은 편집도 취소도 할 수 없습니다.
검산: 입력한 후에 다시 읽어오기
전송 전에 입력란의 값을 원문과 대조합니다.
// 전송 버튼을 누르기 전에, 입력란의 값을 원문과 대조한다
const expected = `여기에 전송 예정인 원문을 넣는다`;
const el = document.querySelector('textarea[name="message[body]"]');
...
- 전제: 셀렉터(Selector)가 어긋나면
el은null이 되므로, 비교 전에 판정합니다. textarea의value는 CRLF가 LF로 정규화됩니다. Windows에서 만든 원고와 비교하려면 줄바꿈을 맞춰야 합니다. - 어긋나는 방식: null 체크가 없으면 예외(Exception)가 발생하며 중단됩니다 (이것은 알아챌 수 있는 분들입니다). 줄바꿈을 맞추는 것을 잊으면, 올바른 입력을 「불일치」라고 보고합니다.
값의 직접 설정도 만능은 아닙니다. React 등의 제어된 입력란(Controlled Input)에서는 DOM의 value를 바꿔도 앱 측의 상태(State)에 반영되지 않을 수 있습니다. 이 경우, DOM의 값을 다시 읽어와도 일치하게 됩니다.
다시 읽어와서 알 수 있는 것은 DOM의 값까지입니다. 재렌더링(Re-rendering) 후에도 값이 남아있는지, 실제로 전송된 내용까지 확인합니다. 수단을 정답으로 삼지 말고, 사후 확인을 정답으로 삼으세요.
모든 입력을 다시 읽어오는 것은 현실적이지 않습니다. 되돌릴 수 없는 것, 공유 대상이 있는 것, 일괄 처리의 기점이 되는 것에 한정합니다. 그곳에서는 다시 읽어올 때까지 완료된 것으로 간주하지 않는 운용을 합니다.
증상 4: 타임아웃 ≠ 미실행
채팅으로 전송하는 도중에 브라우저 측의 실행이 45초 만에 타임아웃(Timeout)되었습니다. 사용한 것은 Chrome DevTools Protocol (CDP)입니다. 돌아온 응답은 The renderer may be frozen or unresponsive였습니다.
이를 「전송되지 않았다」고 해석하고 다음 조작을 던졌습니다. 하지만 타임아웃 후에도 전송 처리는 완료되었고, 메시지가 도착해 있었습니다. 「실패했으니까 재실행」이라며 한 번 더 보냈다면, 중복 전송이 되었을 것입니다.
전송·저장·게시·결제와 같이 부작용(Side Effect)이 있는 조작에서는 타임아웃을 「실패」가 아니라 「불명(Unknown)」으로 취급합니다. 성공과 실패라는 2진법이 아니라, unknown을 가진다는 취급입니다.
다음 조작을 하기 전에 화면이나 목록을 다시 가져와서 실측합니다. 자동 재시도(Retry)로 인해 중복 실행이 발생하는 전형적인 사례가, 처리는 성공했는데 응답만 돌아오지 않은 케이스입니다.
집필 중에 겪은, 상태가 어긋나는 예
최소 재현 페이지를 로컬에 만들고, 에이전트 측의 브라우저로 엽니다. file://로의 네비게이션은 300초 만에 타임아웃되었습니다.
직후, 탭 상태를 반환하는 API는 origin에 목적지 URL을 표시하고 있습니다. 그런데 같은 탭에서 JavaScript를 실행하면, 다른 결과가 반환되었습니다.
tabs_context → origin: file:///.../index.html
javascript_tool → No site is open in this tab.
이 두 가지만으로는 페이지가 열려 있었는지 확정할 수 없습니다. 스크린샷도 DOM도 찍지 않았기 때문입니다.
확정할 수 있었던 것은, 탭 상태를 반환하는 API와 JavaScript 실행 API에서 인식이 어긋났다는 사실뿐입니다. 어느 쪽이 맞는지 결정하지 않고 진행하면, 이후의 측정값이 무엇에 대한 것인지 알 수 없게 됩니다.
왜 알아채지 못하는가
이 네 가지는 「반환값(Return Value)만으로는 실제 상태를 확정할 수 없었던 것」이라는 기준으로 선별한 사례입니다. 공통점이 있는 것은 당연하며, 그 자체는 발견이 아닙니다.
이유는 두 가지입니다. 앞의 세 가지는 성공과 같은 형태의 결과를 반환했습니다. 이번 검색에서는 해당 없음도 빈 결과(Empty Result)로 취급되고 있습니다. 클릭은 맞든 틀리든 같은 반환값이었습니다.
네 번째는 반대로 타임아웃이라는 이상이 반환되었습니다. 그럼에도 「미실행」까지 의미하지는 않습니다. 조작 그 자체의 반환값만으로는 검증할 수 없으며, 별도의 경로로 결과의 상태를 다시 읽어야 합니다.
또 하나는 에이전트에서는 오류가 증폭되기 쉽다는 점입니다. 잘못된 관측이 자연스러운 단정문으로 변하고, 그 문장이 다음 조작의 전제로 재사용됩니다.
「25건이 보인다」가 「25건이 존재한다」로 바뀌면, 그 오차는 다음 검색·연락·삭제 단계까지 전파됩니다.
운영에 적용한 4가지 규칙
| 증상 | 규칙 |
|---|---|
| 건수 | 취득 건수를 존재 건수로 취급하지 않는다. 모집단 측의 숫자와 대조하기 전까지는 확정하지 않는다 (일치는 방증일 뿐 증명이 아니다) |
| ... | |
| 이 4가지와는 별개로, 실질적인 피해를 억제하는 데 가장 효과적이었던 것은 「일괄 처리 전에 1건만 실행하기」 운영이었습니다. 중복 연락 사고를 1건으로 한정할 수 있었던 이유는, 첫 1건을 보낸 뒤에 화면을 확인했기 때문입니다. 20건을 동일한 절차로 실행했다면 전 건에서 발생했을 것이라 생각하지만, 실제로는 실행하지 않았기에 확인하지 않았습니다. |
4가지 규칙은 오류를 검출하는 방법이며, 1건만 실행하는 운영은 오류가 있었을 때의 피해 범위를 억제하는 방법입니다. 역할이 다르기 때문에 둘 다 필요합니다.
환경
측정 기간: 2026-07-22 〜 2026-08-01
대상: Chrome 확장(CDP) 경유 브라우저 조작 / 에이전트 내장 브라우저
OS: Windows 11 및 macOS (복수 머신·복수 세션에 걸침)
...
측정 당시의 Chrome 본체·확장 버전, OS의 디스플레이 확대율은 기록하지 않았습니다. 위의 수치는 해당 환경에서 관측된 값으로 읽어주시기 바랍니다. 기록하지 않은 것을 나중에 덧붙이지 않는 것 또한 이 글의 주제입니다.
저자에 대하여: 유마(Effortless Works). 수탁 개발과 개인 개발을 혼자서 운영하며, 실작업의 대부분을 AI 에이전트에 맡기고 있습니다. 이 글은 AI를 사용하여 집필하였으며, AI를 통한 다중 계통의 반증 체크와 다른 모델을 통한 교정 과정을 거쳐 지적 사항을 반영했습니다. 사례의 실측값은 본인의 세션 기록에 기반합니다. 사양값은 공식 자료이며, 비율이나 토큰 수는 그로부터 계산한 값입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기