글

라벨이 자동화인 게시물 표시

브라우저 자동화 대기: 90초 타임아웃이 나도 화면부터 확인하는 이유

브라우저 자동화는 몇 초가 지났는지가 아니라 필요한 상태가 생겼는지를 기다려야 한다. 감시를 행동보다 먼저 등록하고, 완료 조건을 만족하면 진행하되 제한 시간을 넘으면 현재 화면을 확인한다. 이 방식은 글 작성·예약 발행을 자동화하는 개발자가 입력 반영과 저장 결과를 구분하는 데 쓸 수 있다. 2026년 10월 13일 실행 로그에는 NavigationReadinessTimeoutError 가 30초 대기에서, 이어 90초로 늘린 대기에서도 남아 있다. 그 뒤 현재 화면 스냅숏을 읽고 진행해 8편을 예약했고, 각 글을 다시 열어 확인했다. 같은 실행 안에서도 30초 준비 대기 타임아웃은 여덟 번 더 났지만, 매번 화면을 읽고 다음 단계로 갔다. 30초를 90초로 바꾸면 해결될까? 준비 완료를 기다리다가 타임아웃이 나면 제한 시간부터 늘리고 싶어진다. 그런데 대기 시간이 부족한지, 기다리는 신호가 작업의 완료 조건과 다른지 먼저 나눠야 한다. 같은 종류의 대기가 90초에서도 끝나지 않았다는 보고만으로 브라우저 내부 원인을 단정할 수는 없다. 오류 이름이 말해 주는 것은 준비 상태 대기가 제한 시간 안에 끝나지 않았다는 사실이다. 입력이 실패했다거나 저장이 취소됐다는 뜻까지 담고 있지는 않다. 타임아웃 뒤 화면을 읽지 않고 클릭을 반복하면 현재 상태와 다른 행동을 할 수 있다. 붙여넣기에서도 비슷한 구분이 필요했다. 9월 27일 SmartEditor 조사에서는 합성 paste 이벤트가 처리됐지만 직후 본문은 한 문단으로 보였다. 나중에는 시험용 두 문단이 들어왔다. 이벤트 반환과 본문 반영을 같은 순간으로 취급한 검사가 잘못된 실패 판정을 만든 것이다. 최소 재현은 단순하다. 시험용 편집기에서 붙여넣기 직후 본문을 한 번 읽고, 변경 감시가 확인한 본문과 비교한다. 앞의 읽기만으로 실패를 판정하는 순서와, 감시를 먼저 붙이는 순서를 대조하면 된다. 실제 글을 발행할 필요는 없다. 먼저 구독하고 나서 행동한다 완료 조건은 행동 전에 정한다. 붙여넣기...

발행 상태 검증, 재발행 버튼을 누르기 전에 확인할 4가지

AI에게 발행을 맡겼다면 보고를 읽은 뒤 실제 공개 상태를 따로 확인해야 한다. 공개 URL, 화면의 공개 상태, 게시 시각, 중복 URL이 맞는지 보고 나서 재시도를 결정한다. 2026년 10월 13일에는 중간에 멈춘 발행 작업이 발행 진행 중이라고 남긴 글이 후속 확인에서 이미 공개돼 있었다. 이 확인은 블로그 제작과 발행을 자동화하는 개발자를 위한 절차다. 목표는 완료 문구를 받는 데 있지 않다. 의도한 글이 의도한 상태로 남아 있고, 같은 글을 다시 올릴 필요가 없는지 판단하는 데 있다. 아직 발행하지 않았다는 보고를 받으면 바로 다시 올려도 될까? 당시 앞 작업은 실행 중간에 중단됐다. 마지막 보고는 앞선 시도가 발행에 도달하지 않았고, 발행은 진행 중이라는 내용이었다. 몇 분 뒤 시작한 후속 작업은 중복 확인부터 했다. 그 글은 이미 공개돼 있었고, 후속 작업은 공개 페이지에서 정확한 제목을 확인한 뒤 새 글을 만들지 않고 멈췄다. 여기서 확인한 결과는 단순하다. 재발행이 필요하다는 전제로 시작해도 실제 글이 공개돼 있으면 멈춰야 한다. 보고를 기준으로 새 글을 만들었다면 필요 없는 실행으로 이어질 상황이었다. 발행 버튼이 언제 눌렸는지, 어떤 시도가 공개를 끝냈는지는 남은 영수증만으로 확정할 수 없다. 그래도 다음 행동을 정하는 데 필요한 사실은 남아 있다. 마지막 보고는 완료가 아니라 진행 중이었고, 실제 상태는 공개였다. 최소 증거는 네 가지로 나눈다 공개 URL은 결과물을 찾는 기준으로 남긴다. 주소만 복사하지 말고 그 주소에서 의도한 제목과 본문이 보이는지 확인한다. 다른 글을 열어 놓은 채 성공으로 처리하지 않도록 대상 원고와 함께 대조한다. 화면의 상태는 별도 항목이다. 관리 목록에서 공개인지 예약인지 초안인지 적고, 공개가 목표라면 독자가 보는 페이지도 확인한다. 편집 화면에서 원고가 보이는 것과 공개 페이지에서 글이 보이는 것을 같은 증거로 취급하지 않는다. 게시 시각에는 날짜와 시간대를 같이 남긴다. 예약이 목표...

Blogger 피드 max-results=150인데 35개만 왔다: 끝까지 받는 페이지네이션

이미지
답부터 말하면 Blogger 공개 피드의 max-results 는 전체 글 수를 보장하지 않았다. 2026년 9월 28일 측정에서 이 블로그는 totalResults=86 을 알렸지만, max-results=150 응답에는 JSON과 Atom 모두 35개 entry만 들어 있었다. 해결은 totalResults 와 실제 entry 수를 따로 읽고 start-index 를 넘기는 것이다. 10개씩 순회한 최종 측정에서는 86개 고유 글을 모두 회수했다. 150을 요청했는데 왜 35개만 왔나? 처음에는 150이 전체 86개보다 크니 한 번에 끝날 것으로 봤다. 그런데 JSON 응답의 openSearch$totalResults 는 86, entry 배열 길이는 35였다. Atom도 같은 결과였다. max-results 값을 바꿔 보니 경계가 더 분명했다. 35까지는 요청한 수만큼 왔지만, 36부터는 35개에 멈췄다. cd4761 공개 피드 측정 — 2026-09-28 15:51 KST 형식 max-results totalResults 실제 entry JSON 1 86 1 JSON 10 86 10 JSON 25 86 25 JSON 35 86 35 JSON 36 86 35 JSON 50 86 35 JSON 100 86 35 JSON 150 86 35 Atom 150 86 35 문제는 itemsPerPage 도 요청값 150으로 표시된다는 점이다. 이 값만 믿으면 실제 배열이 짧다는 사실을 놓친다. 결국 검증 기준은 요청값이 아니라 len(entry) 다. 주문서에 150개라고 적혀 있어도 상자에 35개만 들었다면, 세어 볼 것은 주문서가 아니라 상자다. 다만 피드는 이유를 적은 송장을 함께 주지 않는다. 35개가 Blogger의 공통 상한인가? 아니다. 같은 날 공개 Blogspot 두 곳의 JSON 피드에 max-results=150 을 보냈다. Blogger 공식 블로그는 전체 540개 중 150개, Google Blog는 전체 3,37...

텔레그램 한 줄로 블로그 작업을 넘길 때 중복 실행을 막는 구조

이미지
답부터 말하면 텔레그램 메시지 ID를 요청 파일 이름과 처리 영수증의 공통 키로 써야 한다. 같은 메시지가 다시 들어와도 새 파일로 덮어쓰지 않고, 상태 파일이 있으면 에이전트에 다시 넣지 않는 구조다. 이 결론은 2026년 9월 28일 로컬 테스트 기준이다. 테스트 41개가 통과했고 실패는 0개였다. 다만 실제 텔레그램 사진 왕복과 장애 복구 시간은 아직 측정하지 않았다. 왜 명령 한 줄이 두 번 실행될 수 있나? 텔레그램에서 /blog 명령을 한 번 보냈다고 실행도 한 번이라고 볼 수 있을까. 수신 플러그인, 파일 감시, 에이전트 세션, 답장 전송 사이에는 각각 재시작과 재호출이 들어올 수 있다. 문제는 요청 내용이 아니라 경계다. 같은 업데이트를 플러그인이 다시 받거나, 파일 감시 이벤트가 반복되거나, 답장 전송 중 프로세스가 멈출 수 있다. 그래서 “이미 본 요청인가”를 각 단계가 같은 기준으로 판단해야 한다. 이 브리지는 메시지 본문 대신 텔레그램 메시지 ID를 기준으로 삼는다. 내용이 같은 두 요청은 서로 다른 작업일 수 있지만, ID가 같은 요청은 같은 전달 건이기 때문이다. 요청 파일은 어떻게 한 번만 만들어지나? 수신 단계는 먼저 임시 파일에 요청 전체를 쓴다. 파일을 동기화하고 닫은 뒤, requests/<메시지ID>.json 이라는 최종 이름에 배타적으로 연결한다. 최종 파일이 이미 있으면 덮어쓰지 않는다. 기존 파일 자체가 중복 수신 영수증이 된다. 소비자는 완성된 이름만 보므로 절반만 써진 JSON을 집어 들지 않는다. 임시 파일에 요청 전체를 기록한다. 파일 동기화와 닫기를 끝낸다. 메시지 ID 이름으로 배타적 링크를 만든다. 같은 이름이 있으면 기존 요청을 유지한다. 요청 파일은 처리 뒤에도 삭제하지 않는다. 2026년 9월 28일 테스트에는 같은 명령을 8개 동시에 넣어도 최종 요청 파일이 하나만 남는 검사가 있다. 파일 감시자가 처음 본 시점에 JSON 전체를 읽을 수 있는지도 별도로 확인한다...

네이버 SmartEditor 붙여넣기: 성공 이벤트보다 본문 일치가 먼저다

이미지
답부터 말하면 붙여넣기 이벤트가 받아들여졌는지가 아니라, 편집기 본문이 넣으려던 전체 글과 같아졌는지를 완료 조건으로 삼아야 한다. 이벤트를 보내기 전에 변경 감시를 시작하고, 제한 시간 안에 정규화한 누적 본문이 정확히 일치할 때만 다음 단계로 넘어가는 방식이다. 2026년 9월 27일 한 번의 SmartEditor 자동화 기록에서 첫 구간은 48ms, 두 번째 구간은 239ms 뒤 반영됐다. 이후 다시 연 예약 글에서는 원고·주입 대상·편집기 본문이 각각 2800자로 일치했다. 그 예약은 이 실행이 아니라 동시에 돌던 다른 작업이 저장한 것이다. 48ms와 239ms는 속도 기준이나 성공률이 아니라 한 실행의 관찰값이다. 왜 paste 이벤트 성공만으로는 부족했나? 조사 중 합성 paste 이벤트는 defaultPrevented:true 를 반환했다. 그런데 직후 본문은 여전히 한 문단이었고, 조금 뒤에는 시험용 두 문단이 들어와 있었다. 문제는 이벤트 수락과 화면 반영이 같은 순간이 아니라는 점이다. dispatchEvent() 반환값, iframe 포커스, 즉시 읽은 문단 수는 모두 완료 조건이 아니었다. 선택 영역이 iframe 루트에 잡힌 관찰도 과거 60초 지연의 원인으로 확정할 수 없었다. 그렇다면 무엇을 기다려야 할까. 편집기가 실제로 만들어 낸 최종 상태를 기다려야 한다. 고정 sleep 대신 무엇을 기다렸나? 성공한 경로는 MutationObserver 를 붙여넣기 전에 등록했다. 그 다음 clipboard 이벤트를 보낸 뒤, 공백과 제로폭 문자를 정규화한 전체 본문을 매 변경마다 비교했다. 아래는 실제 코드를 줄인 형태다. const done = new Promise((resolve, reject) => { const timer = setTimeout(() => { observer.disconnect(); reject(new Error("paste timeout")); ...

봇이라고 밝히니 통과했다 — 브라우저인 척해서 차단당한 18일

이미지
건물 하나를 떠올려 보자. 택배 기사는 그냥 들여보내는데 방문객처럼 굴면 신분증을 내고 방문 목적을 적어야 한다. 그 건물에서는 제복을 벗는 쪽이 빨리 들어간다. 내 수집기는 18일 동안 정문 앞에 서 있었다. 브라우저인 척하는 User-Agent를 달고 있었기 때문이다. 그 한 줄을 지우자 차단이 풀렸다. 결론부터 적는다. 대응책은 자기가 언제부터 쓸모없어졌는지 말해 주지 않는다. 차단을 피하려고 넣어둔 UA 위장이 그랬고, 실패를 놓치지 않으려고 넣어둔 장치도 그랬다. 신호는 28번 다 울렸고 그걸 18일 동안 읽지 않은 사람은 나다. 누가 읽으면 좋은가 외부 사이트를 긁는 코드를 굴려본 사람. 차단을 피하려고 헤더를 손봐본 적이 있는 사람. 그리고 “실패하면 에러가 나니까 알게 되겠지”라고 생각해 본 사람. 수집기가 조용히 0을 반환하는 걸 본 적이 있다면 절반은 아는 이야기다. 나머지 절반은 조용하지 않았는데도 못 봤다는 쪽이다. 위장을 지우니 403이 200이 됐다 먼저 증상부터 살펴보자. 트렌드 수집기의 소스 두 곳이 죽어 있었다. Threads와 Product Hunt다. 둘 다 r.jina.ai 라는 같은 경로를 지나간다. 마지막으로 정상 수집된 실행은 7월 29일이다. Threads 135건, Product Hunt 16건이 들어왔다. 다음 날부터 둘 다 0이 됐다. 8월 16일까지 28회 연속이다. 원인을 좁히려고 같은 URL에 헤더만 바꿔 던져 봤다. 같은 시각, 같은 주소다. 보낸 User-Agent 응답 Mozilla/5.0 ... Chrome/131.0.0.0 Safari/537.36 403 curl 기본 UA 200 빈 UA 200 trend-scout/0.2 200 403 응답 본문의 첫 줄은 이랬다. <title>Just a moment...</title> — Cloudflare가 자바스크립트 챌린지를 걸 때 내보내는 화면이다. 즉 브...

테스트 113개 다 통과했는데 실제론 0개를 긁었다 — AI가 짠 수집기의 함정

이미지
AI에게 수집기(웹에서 데이터를 긁어오는 코드) 두 개를 맡겼더니, 테스트 113개를 전부 통과시켰다. 초록불. 그런데 실제로 돌려보니 한쪽은 0개를 긁었다. 전에 이 블로그에서 이 트렌드 수집 도구의 데이터 소스가 어떻게 죽는지 썼다. 이번엔 거기에 기능을 붙이다가 더 불편한 걸 배웠다. 테스트는 통과했는데 코드는 작동하지 않았다 — 이게 어떻게 동시에 참일 수 있나. 한 줄이면 이렇다. 추정한 입력에 맞춰 짠 테스트는 그 추정이 맞다는 것만 증명한다. 실제 세계가 추정과 같은지는 아무도 검증하지 않았다. 누가 읽으면 좋은가 AI(Claude·Cursor 등)에게 코드를 맡기고 테스트 초록불을 받아본 사람 특히 외부 API나 웹페이지를 긁는 코드를 AI에 시켜본 사람 “테스트 다 통과했으니 됐겠지”로 넘어간 적 있는 사람 113개 초록불, 라이브에선 0개 무슨 일이었나부터. 트렌드 도구에 “해외에서 잘나가는 신제품”을 자동으로 잡아낼 수집기 두 개를 붙이려 했다. 하나는 Show HN(해커뉴스 신제품 게시판), 하나는 Product Hunt(신제품 등록 사이트). 작업은 서브에이전트에 맡겼다. 테스트를 먼저 쓰고 구현하는 방식으로, 규칙도 빡빡하게 줬다. 에이전트는 코드와 테스트를 짜서 “113개 통과”를 보고했다. 여기서 멈췄으면 끝이었다. 초록불이니까. 그런데 안 믿고 실제로 돌렸다. Show HN은 실제 런칭 5건을 정확히 긁어왔다. Product Hunt는 0개였다. 대신 에러 하나 — “제품을 하나도 파싱하지 못함”. 테스트는 왜 통과했나 이게 핵심이다. 0개를 긁는 코드의 테스트가 어떻게 113개나 통과했나. 답은 단순했다. 에이전트는 Product Hunt의 실제 응답을 한 번도 본 적이 없다. 줄 수도 없었다. 그 응답은 실제로 한 번 긁어봐야 나온다. 그래서 “이렇게 생겼겠지” 하고 가짜 샘플을 만들었고, 그 샘플에 맞춰 파싱 코드를 짰고, 다시 그 샘플로 테스트를 돌렸다. 테스트는 “추정이 추정과 일치...