IceBlogation 개발일지 — 예약인 척하던 발행, 링크 미스터리 3부작, 사라진 글 65개
2주간 기능 대신 신뢰를 고쳤다. 다른 PC로 이사하는 .iceblog 데이터 이전, 링크풀 폐기와 실시간 발급, 예약을 안 걸고 걸었다던 네이버 발행, 범인이 셋이던 쿠팡 링크대기(레이스·차단·죽은 트릭), 설치 후 갈라진 데이터에서 글 65개 복구, 이미지 엔진 4종 선택, 그리고 유령 터미널 창까지 — 버전 1.1.0에서 1.4.2로.
6월 말 이후 2주는 기능을 늘린 기간이 아니라 신뢰를 회복한 기간이었다. 자동화 제품은 "했다"고 말해놓고 안 했을 때 끝난다. 예약했다면서 예약을 안 걸고, 링크를 딴다면서 빈손으로 오고, 설치했더니 글이 사라지는 — 그런 것들을 하나씩 잡았다. 버전은 1.1.0에서 1.4.2가 됐다.

데이터는 들고 다닐 수 있어야 한다
첫 숙제는 이사였다. 계정 금고와 설정은 그 PC의 OS 키링 키로 암호화돼 있어서, 파일만 복사하면 다른 PC에서 못 연다. 집 열쇠를 벽에 붙박이로 박아 둔 셈이라, 이사를 가면 문을 못 여는 구조였다.
그래서 이동용 통로를 만들었다. 금고·설정을 풀어 사용자가 정한 이동용 암호로 다시 잠근 .iceblog 번들(계정+설정+DB+콘텐츠)로 내보내고, 새 PC에서 그 암호로 풀어 그 PC의 키링 키로 재보관한다. 처음엔 CLI만 있었는데, exe 사용자는 파이썬이 없으니 GUI 버튼(⚙설정 → 🔄 데이터 이전)까지 완성했다. 언인스톨러에도 "데이터도 삭제할까요?"를 물어보되 기본값은 보존으로 두었다 — 지우는 쪽이 실수하기 쉬운 방향이니까.
링크풀을 버리고, 실시간으로
쿠팡 제휴 링크는 원래 미리 만들어 둔 정적 풀에서 키워드 매칭으로 꺼내 쓰는 구조였다. 그런데 매칭이 거의 항상 0개로 떨어졌다. 창고에 물건을 쌓아 두고 목록표로 찾는데 목록표가 늘 안 맞는 격. 그래서 창고를 통째로 버렸다 — 글을 쓸 때 그 주제로 쿠팡을 실시간 소싱해 링크 2개를 즉석 발급하는 방식으로 바꾸고, 풀 관련 코드·UI·설정을 전부 삭제했다. 이 과정에서 상품 2개가 같은 딥링크로 나오던 버그(입력칸 잔류값)도 잡았다.
네이버의 방해꾼들
네이버 글쓰기 화면엔 복병이 둘 있었다.
하나는 사용자가 지목한 우측 도움말 사이드패널 — 계정에 따라 자동으로 펼쳐져 작성·발행을 가릴 수 있다. 라이브로 셀렉터를 확정해 뜨면 닫는 처리를 넣었다.
다른 하나가 진짜 수확이었다. 가끔 제목 입력에서 타임아웃이 나던 미스터리의 원인이, 페이지 진입 직전 블로그 홈의 잔재 iframe(이벤트 광고)을 에디터 프레임으로 잘못 잡아서였던 것. 신형 에디터는 iframe이 아니라 페이지 최상위에 있다. 제목 셀렉터가 실제로 존재하는 프레임을 고르도록 바꿨다. 덤으로, 계정에 적힌 블로그 주소가 실제와 달라 글쓰기 진입에 실패하면 리다이렉트된 주소에서 진짜 블로그 ID를 읽어 재진입하는 폴백도 넣었다 — 설정이 틀려도 발행이 되는 쪽으로.
예약 발행이, 예약을 안 하고 있었다
사용자 보고는 한 줄이었다. "네이버 글을 올리다가 예약 발행을 하지 않고 끝내는 경우가 있다."
파보니 아찔했다. 네이버 글쓰기 화면은 React로 만들어져 있는데, 코드가 예약 라디오 버튼의 raw <input>을 직접 클릭하고 있었다. React는 이걸 무시하고 즉시 '현재 발행'으로 되돌린다. 그런데 확인 코드는 클릭 직후의 순간값을 읽어 "예약 걸림"이라고 거짓 성공을 보고했다. 최악의 경우 예약인 줄 알고 확정 버튼을 눌러 즉시 공개 발행이 나가는 구조였다. 날짜 입력도 마찬가지 — 읽기전용 datepicker에 JS로 값을 밀어 넣고 있었고, 당연히 무시됐다.
수리는 세 겹으로 했다. ① 라디오는 label을 클릭하고 0.9초 뒤에 다시 읽어 확인, ② 날짜는 달력 위젯을 사람처럼 실제로 넘겨서 클릭, ③ 그리고 핵심 — 확정 버튼을 누르기 전에 예약 여부·날짜·시·분을 화면에서 되읽어 기대값과 대조하고, 하나라도 어긋나면 확정을 안 누르고 중단한다. 은행 이체 직전에 받는 사람 이름을 다시 확인하는 것과 같다. 클릭이 성공했다는 것과 원하는 상태가 됐다는 것은 다른 명제다.
라이브로 검증했다. 예약 카운트가 0건에서 1건으로 늘고, 예약 목록에 지정한 날짜·시각 그대로 글이 올라간 것까지 확인.
"2개 중 1개만 링크가 들어갔어요"
티스토리 쪽에도 닮은꼴 버그가 있었다. 발행 시 쿠팡 링크를 예쁜 OG 박스 카드로 바꾸는데, 카드 생성이 실패해도 원본 링크를 지우는 뒷정리는 무조건 실행됐다. 카드도 없고 링크도 없는 글이 나오는 것. 카드가 성공했을 때만 정리하고, 실패하면 클릭 가능한 인라인 링크라도 보존하게 고쳤다.
새 PC의 첫 글만 링크가 빠지는 현상도 잡았다 — 첫 쿠팡 크롤링은 통과 쿠키가 없어 차단되는 콜드스타트 문제라, 재시도(첫 시도가 쿠키를 예열)와 함께 링크를 못 딴 글은 조용히 넘어가지 않고 '링크대기' 상태로 표시하게 했다. 실패는 숨기는 게 아니라 보이게 하는 것이 먼저다.
쿠팡 링크 미스터리 3부작
그런데 그 "링크대기"가 8건 쌓였다. 원인을 쫓아가 보니 범인이 하나가 아니라 셋이었다.
1부 — 레이스. 블로그 2개를 병렬로 돌리는데, 쿠팡 소싱·링크 발급 브라우저가 같은 프로필 폴더를 공유하고 있었다. 두 워커가 동시에 브라우저를 띄우면 늦게 온 쪽이 즉사한다. DB 타임라인을 보니 실패 글이 성공 글과 6~27초 간격의 '쌍'으로 나타났다 — 동시 시작의 지문이다. 전역 락으로 브라우저 구간을 직렬화해서 해결.
2부 — 차단. 락을 넣고도 발급이 0건인 날이 있었다. 덤프를 떠 보니 파트너스 페이지가 Akamai 403 차단 페이지였다. 하루에 빌드·프로브를 반복하며 자동 접근이 잦아지자 IP 기반 소프트 차단이 걸린 것. 대응으로 차단 감지 시 30분 쿨다운(그동안 브라우저를 아예 안 열음), 상품 간 랜덤 지연, 그리고 차단과 무관하게 사람이 직접 링크를 붙여넣는 수동 경로를 추가했다.
3부 — 죽은 트릭. 진짜 근본 원인은 따로 있었다. 링크 자동 발급에 쓰던 딥URL 트릭이 파트너스 사이트 개편으로 조용히 죽어 있었다. 화면을 흉내 내는 자동화는 화면이 바뀌면 같이 죽는다. 그래서 화면 대신 그 화면이 부르는 것을 찾았다 — 파트너스의 JS 번들을 뒤져 내부 딥링크 변환 API(/api/v1/url/any)를 발견했고, 로그인 쿠키를 실은 GET 한 번으로 상품 URL이 제휴 단축링크로 돌아온다. 검색도 DOM 조작도 필요 없어 개편에도 견고하다. 쌓여 있던 8건도 전부 구제해 링크대기 0건.
설치했더니 글이 사라졌다
설치 파일을 실행한 사용자가 "지난 글이 없어졌다"고 했다. 데이터가 지워진 건 아니었다 — 갈라진 것이었다. 앱은 데이터를 exe 옆에 두는 포터블 구조인데, 설치 파일이 새 위치에 빈 데이터로 깔리면서 기존 데이터와 다른 살림을 차린 것.
디스크에 온전히 남아 있던 고아 글 65개를 다시 색인해 복구했다(내용은 100%, 다만 글별 예약일·상태 정보는 원본 DB에만 있어 되살리지 못했다). 그리고 재발 방지로 데이터 위치 고정 포인터를 넣었다 — 이제 exe가 어디에 설치되든 같은 데이터 폴더를 바라본다. 집이 여러 채여도 금고는 하나만 두는 방식이다.
같은 세션에 UI도 크게 갈아엎었다. 한 페이지에 6개 섹션이 세로로 쌓여 있던 것을 상단 탭(글 목록·생성·발행·계정·상태)으로 바꿔 스크롤을 없앴다. 그런데 배포했더니 "개선이 하나도 안 보인다"는 제보 — 범인은 pywebview의 디스크 캐시였다. 앱을 업데이트해도 웹뷰가 옛 화면을 캐시에서 꺼내 보여주고 있었다. Cache-Control: no-store 헤더로 봉인.
골라 쓰는 이미지 엔진, 거짓말 안 하는 상태
굵직한 수리 사이에 제품다움을 채우는 개선도 이어졌다.
- 이미지 백엔드 선택: 글 모델과 별개로 이미지 엔진을 고른다 — Codex(구독, 무과금)·Antigravity(agy)·OpenAI API·나노바나나(Gemini, 장당 $0.034 최저가). 실패 시 플레이스홀더로 폴백.
- 사용량 한도 메시지 구체화: "한도 도달"이 아니라 무엇이(글/이미지) 어느 서비스에서 소진됐고 언제 리셋되는지 말해 준다. 예:
⏸ [글] Claude(구독) 한도 도달 — 15:30 리셋 후 자동 재개 (3건 대기). - 발행 이력 정확도: 임시저장도 '발행됨'으로 집계되던 것을 갈라
임시저장됨·예약확정·발행됨을 구분하고, 발행 작업과 글 상태를 배선해 영원히 "진행 중"으로 남는 유령 이력을 없앴다. - 글 중복 방지: 주제를 키워드 돌려쓰기로 뽑아 같은 글이 반복되던 것을, DB의 기존 글 제목을 제외한 LLM 브레인스토밍 + 각도 변형(추천/비교/후기/고르는법…)으로 바꿨다. SEO 중복 감점 방지.
"메모장 같은 창이 자꾸 떠요"
마지막 세션은 사용자가 체감한 자잘한 불편 4가지를 잡았다. 이런 게 사실 제품의 인상을 결정한다.
- 유령 창: 글 생성 중 "메모장 같은 창"이 계속 깜빡였다. 창 없는(windowed) exe는 부모 콘솔이 없어서, CLI를 부를 때마다 Windows가 새 터미널 창을 띄운 것. 숨김 플래그(
CREATE_NO_WINDOW)를 씌운 공용 실행 헬퍼로 호출부 5곳을 교체했다. - 스크롤 리셋: 글 목록이 20초마다 맨 위로 튀었다. 폴링이 매번 목록 DOM을 통째로 다시 그려서였다. 데이터 서명을 비교해 바뀐 게 없으면 아예 다시 그리지 않고, 그려야 할 때도 스크롤 위치를 저장했다 복원한다. 발행 중에도 목록을 자유롭게 볼 수 있게 됐다.
- 암호 같은 상태 이름: "예약"과 "예약확정"의 차이를 만든 사람만 알았다. 예약준비(앱에만 지정, 아직 안 올라감)·예약완료(블로그에 예약 등록 끝, 시간 되면 자동 공개)·발행중으로 바꾸고, 모든 상태 배지에 툴팁과 ❓ 도움말 모달(흐름도 포함)을 달았다.
- 목록 UX: 행간·폰트 확대, hover 하이라이트, 1160px 폭 안에 전 열 수납, 이중 스크롤 제거.
덤으로 선택 글 영구 일괄삭제와, 계정 삭제 시 그 계정의 글·작업폴더까지 함께 지우는 정리(purge) 옵션도 추가했다. 오프라인 검증은 이 기간 133개에서 217/217 통과로 늘었다.
운영 화면도 제품답게 다시 다듬었다
기능이 안정된 뒤에는 매일 마주치는 운영 화면을 다시 정리했다. 짙은 네이비 배경에 시안·블루 포인트를 두고, 카드·탭·입력 포커스·버튼·모달·스크롤바가 한 제품처럼 보이도록 디자인 체계를 맞췄다. 탭 이름도 콘텐츠 / 새 글 만들기 / 발행 현황 / 블로그 계정 / 시스템 상태로 바꿔, 처음 보는 사람도 어디서 무엇을 해야 하는지 바로 알 수 있게 했다.
좁은 화면도 빠뜨리지 않았다. 900px과 680px을 기준으로 레이아웃이 자연스럽게 접히고, 움직임에 민감한 사용자를 위해 prefers-reduced-motion도 반영했다.
기능 계약은 그대로 지켰다. 디자인 변경은 web/index.html 한 파일에만 넣었고 web/app.js와 백엔드는 건드리지 않았다. 중복 ID는 0개, JS 리터럴 ID 참조 누락도 0개였다. 오프라인 회귀검증 217/217 PASS, PyInstaller 빌드 성공, 소스와 번들 HTML의 SHA-256 일치, /와 /api/status HTTP 200, Cache-Control: no-store까지 확인했다.
검증한 번들은 C:\IceBlogation 실행본에 반영했다. 기존 iceblogation.db와 content 폴더는 보존했다. 버전 표기는 1.4.2를 유지했고 이번에는 설치파일을 다시 만들지 않았다.
남은 것
공식 쿠팡 API 자격(매출 15만 원)이 생기면 웹 자동화 없이 API 키 세 줄로 전환된다 — 코드는 이미 준비돼 있다. 다음은 실배치를 돌려 링크대기 0건이 유지되는지 최종 확인하는 일이다.
2주간의 교훈은 하나로 모인다. 자동화의 가치는 속도가 아니라 "했다고 하면 정말 한 것"이라는 신뢰다. 확정 전 되읽기 검증처럼, 시스템이 자기 말을 스스로 의심하게 만드는 장치가 그 신뢰를 만든다.