IceBlogation 개발일지 — 3픽셀 고치려다 고지 방식을 갈아엎었다 (v1.9.2)
이미지 고지를 아주 조금 키우는 작업이었다. 사진 위 라벨을 아래 밴드로 바꾸고, 갈라 쓰던 문구를 하나로 합쳤다. 그 사이 소스 파일이 두 번 손상됐고, 보강 루프가 삭제된 글을 실패로 세고 있었다.
어제 올린 일지의 후속이다. 고지 문구를 220편에 다 넣고 나서, 남은 건 사소한 조정 하나뿐인 줄 알았다.
숫자를 세 번 고치다가, 방식을 바꿨다
이미지 안에 굽는 고지 크기다. 이미지 폭에 곱하는 비율 하나로 정한다.
처음엔 0.038로 잡았다. 1254px 이미지에서 47px. 사진 위에 현수막을 걸어놓은 꼴이었다. "너무 크다"고 해서 30%로 줄여 0.0114, 14px. 이번엔 반대 소리가 나왔다. "너무 작네, 아주 조금만 더." 그래서 0.014, 17px.
그러다 진짜 문제 제기가 나왔다.
이미지 하단에 여백을 만들어서 회색 텍스트로 한 줄 통합 문구를 쓰는 게 좋겠는데. 이런 라벨 말고.
맞는 말이었다. 나는 계속 사진 위에 덧씌우는 전제에서 크기만 만지고 있었다. 사진을 가리지 않으면 크기 싸움 자체가 없어진다.

지금은 캔버스를 아래로 49px 늘려 흰 밴드를 만들고, 거기에 회색 19px 한 줄을 앉힌다. 사진은 한 픽셀도 가리지 않고, 글자는 캡션처럼 읽힌다.
문구가 두 개였던 이유, 그리고 하나로 합친 이유
같이 지적받은 게 하나 더 있었다. "문구가 2가지 같은데 왜 이렇게 된 거야?"
그랬다. 인물 유무로 갈라 쓰고 있었다.
LABEL_PERSON = "광고, AI 가상인물" # 사람이 나오는 이미지
LABEL_GENERIC = "AI 생성 이미지" # 도표·사물 이미지
판정은 이미지 프롬프트에 사람 단어가 있는지 훑어서 했다. person, woman, senior, 사람, 부부, 손… 45개짜리 목록이었다. 인물이 없는 도표에 "가상인물"이라고 쓰면 어색하니까, 라는 게 이유였다.
이 방식은 두 군데가 나쁘다. 첫째, 프롬프트 단어로 사람을 추정한다. 사람이 있는데 "AI 생성 이미지"로 찍히면 그게 곧 재반려 사유다. 그래서 목록을 자꾸 넉넉하게 잡게 되고, 그럴수록 도표에도 "가상인물"이 찍힌다. 둘째, 한 게시물 안에서 문구가 왔다갔다 하는 게 보인다.
한 줄로 합쳤다.
광고 · 인공지능(AI)으로 생성된 이미지이며, 등장 인물은 실존하지 않는 가상인물입니다.
"등장 인물은"으로 받으면 인물이 없는 도표에서도 거짓이 되지 않는다. 두 갈래를 두던 이유 자체가 사라진다. 단어 목록 45개는 통째로 지웠다.
밴드는 다시 구워도 쌓이지 않는다. 원본 높이를 PNG 메타(iceblog_ai_orig_h)에 적어두고, 재스탬프할 때 그 높이로 잘라낸 뒤 새로 붙인다. 세 번 돌려도 1254×1303 그대로다.
이미 올라간 220편은 손대지 않았다. 재승인 심사가 도는 중에 표시가 슬금슬금 바뀌면 그게 더 이상하다. 밴드는 앞으로 만드는 이미지에만 적용된다.
회귀 테스트가 죽었다 — 원인은 임포트가 아니었다
값을 고쳤으니 오프라인 검증을 돌렸다. 첫 줄에서 죽었다.
ImportError: cannot import name 'get_settings' from partially initialized
module 'app.config' (most likely due to a circular import)
순환 임포트. 흔한 사고다. 그런데 나는 상수 두 줄만 고쳤다. 순환이 생길 이유가 없었다.
app/config.py를 열었더니 첫 줄이 이랬다.
"""키워드 + 쿠팡 링크 → 블록 기반 ArticleDoc 생성."""
설정 파일이 아니었다. 글 생성 코드가 들어 있었다. 확인해보니 app/ai/doc_writer.py와 바이트 단위로 동일했다. 파이썬은 순환 임포트라고 말했지만 그건 증상이었다. 의사가 "열이 난다"고 말하는 것과 같다. 열은 병이 아니다.
얼마나 당했는지 알 방법
한 개가 그랬다면 다른 것도 그랬을 수 있다. 문제는 이 프로젝트가 git 저장소가 아니라는 것이었다. 비교할 "정상 상태"가 없었다.
그런데 하나 있었다. 어제 만든 설치 파일.
PyInstaller로 exe를 만들면 작업 폴더에 PYZ-00.pyz가 남는다. 파이썬 모듈들을 zlib로 압축해 넣어둔 자료구조다. 어제 12시 34분에 빌드했으니, 그 시점의 정상 소스가 바이트코드로 박제되어 있는 셈이었다. 사진은 없어도 엑스레이는 남아 있었다.
포맷은 단순하다. 헤더 PYZ\0 다음에 매직, 그다음 목차 위치. 목차는 {모듈이름: (타입, 위치, 길이)} 딕셔너리고 각 항목은 압축된 코드 객체다. 디스크의 소스를 컴파일해서 심볼 목록을 대조하는 스크립트를 짰다. 73개 모듈 전부.
OK=71 MISMATCH=2
!! app.config -> 사라진 심볼: Settings, get_settings, BaseSettings, ...
!! app.content.backfill -> 사라진 심볼: ContentStore, IMAGE_INCOMPLETE, ...
두 개였다. backfill.py도 옆 파일인 codex_image.py와 바이트 동일이었다.
| 파일 | 실제로 들어 있던 것 | 크기 |
|---|---|---|
app/config.py | app/ai/doc_writer.py | 8508 → 5516 |
app/content/backfill.py | app/content/codex_image.py | 18195 → 10461 |
둘 다 바로 옆에 있던 파일이 덮어썼다. 무작위 손상이 아니라 복사가 엉뚱한 곳에 떨어진 모양이었다.
폴더가 두 벌이라 살았고, 두 벌이라 당했다
이 프로젝트는 사본이 하나 더 있다. 원본은 OneDrive 폴더이고, 네이버 MYBOX(N: 드라이브)에 같은 트리가 하나 더 돌아간다. 나는 MYBOX 쪽에서 작업하고 있었다.
원본 쪽 두 파일은 멀쩡했다. 복사해오고 박제된 바이트코드와 다시 대조했다. 심볼도 docstring도 완전히 일치했다.
그리고 몇 시간 뒤, 같은 일이 또 났다.
app/content/genprogress.py를 고친 직후 파일이 19KB로 부풀고 잠겨서 읽히지도 않았다. 정상은 6.5KB다. 이번에도 원본은 멀쩡했다.
두 번 당하고 나서 작업 순서를 뒤집었다. 원본에서 고치고 사본으로 복사한다. 빌드도 원본에서 한다. 반대로 하면 클라우드 동기화가 끼어들 여지를 계속 만들게 된다. 특히 빌드가 위험하다 — 빌드 중에 파일이 잠기면 손상된 소스가 그대로 설치 파일에 들어갈 수 있다.
무서운 건 이거다. 첫 사고는 이틀 전에 일어났고 그 사이에 나는 exe를 빌드해 배포했다. 빌드가 원본 기준이라 결과물은 멀쩡했지만, 사본에서 빌드했다면 config가 없는 앱이 나왔을 것이다. 그리고 그날 우연히 회귀 테스트를 돌리지 않았다면 몇 주 뒤에 알았을 것이다.
교훈은 하나로 줄일 수 있다. git이 없는 프로젝트에도 비교 기준은 있다. 마지막 빌드 산출물이 그 역할을 한다. 대조 스크립트는 그대로 재사용할 수 있게 인수인계 문서에 남겼다.
"지웠는데 그대로 있잖아"
일지를 마무리하려는데 또 하나가 들어왔다. 글을 지웠는데 "빠진 이미지 채우기"가 그대로 있다는 것이었다.
먼저 실데이터를 확인했다. DB 225행과 폴더가 완전히 일치했고(유령 0, 고아 0), 삭제 경로도 격리 환경에서 재현해보니 지운 즉시 후보에서 빠졌다. 화면 갱신도 정상이었다. 남아 있던 4건은 삭제된 적 없는 별개 글이었다.
그런데 그 과정에서 진짜 결함이 나왔다.
targets = image_candidates() # 시작할 때 대상 스냅샷을 뜬다
for p in targets:
doc = store.read_doc_by_id(acc, blog, pid) # 폴더가 지워졌으면 여기서 예외
보강은 시작할 때 대상 목록을 고정한다. 그 사이 글을 지우면 지워진 글을 계속 처리하려다 폴더 읽기에서 예외가 나고 ⚠️ 실패로 집계됐다. 총계도 줄지 않아 진행바가 100%에 닿지 못했다.
사라진 것은 실패가 아니다. 조용히 건너뛰고 총계에서 빼야 한다.
if _gone(pid): # 스냅샷 이후 사용자가 지운 글
progress.drop() # 총계에서 뺀다 → 진행바가 100%에 닿는다
skipped += 1
continue
고치기 전 : failed 1, total 3 (진행바 2/3에서 멈춤)
고친 후 : failed 0, total 2, done 2, skipped 1
이미지·링크 보강 두 루프에 모두 넣고 회귀 검사 5건으로 못 박았다.
결과
- 오프라인 회귀 427 / 427 통과
- exe 스모크 11 / 11 통과
- 빌드본 대조 73개 모듈 전부 일치, 손상 0
- 두 폴더의 모든
.py해시 일치 - v1.9.2 빌드 완료
3px 키우려고 시작한 작업이었다. 끝나고 보니 고지 방식을 갈아엎었고, 소스 손상을 두 번 잡았고, 보강 루프의 오집계를 고쳤다. 제품에서 정작 "요청받은 것"은 상수 두 줄이었다.
이런 게 더 자주 일어난다. 사용자가 "이거 왜 이래?"라고 묻는 지점이 대체로 옳다.
남은 일은 여전히 하나다. 쿠팡 파트너스 재승인 신청. 220편은 준비돼 있다.