Momently 개발기 #9] 자동 발행은 왜 안 되는가 — 네이버 발행과 브라우저 확장
사진을 넣으면 블로그 초안이 나오는 로컬 LLM 파이프라인 Momently 개발기 9편.
글은 나온다. 문제는 그 글을 네이버 블로그에 올리는 마지막 1미터다. 이번
편은 기술적으로 가능한 것과 해도 되는 것 사이에 선을 긋는 이야기 — 그리고
ADR 초안의 가정들이 구현 실측에서 어떻게 수정됐는지의 기록이다.
마지막 1미터의 실패
파이프라인의 끝은 최종 마크다운 + 발행 메타데이터(제목 후보, 해시태그, 메타
디스크립션)다. 초기의 발행 경로는 콘솔의 "발행 패키지 복사" — 클립보드에
정리된 텍스트를 담아주면 사용자가 네이버 에디터에 붙여넣는 방식이었다.
실측해 보니 이 방식은 두 군데서 부러진다.
- 네이버 SmartEditor는 마크다운을 모른다.
## 제목,**굵게**가 평문
그대로 들어간다. 서식을 사용자가 다시 입힌다. - 이미지가 아예 전달이 안 된다. 본문의 이미지 참조는
—
외부 URL도 아닌 그냥 파일명이다. 사용자가 사진 N장을 에디터에서 하나씩
다시 업로드하고 위치를 다시 잡아야 한다.
사진 후기 자동화 도구인데 사진 옮기기가 수작업이라는 건 근본 결함이다.
자동 발행이라는 막힌 길
"그럼 서버가 네이버에 자동 발행하면 되잖아"가 자연스러운 다음 생각인데, 이 길은
기술이 아니라 규범에서 막힌다.
- 공식 API 없음: 네이버 블로그 글쓰기 OpenAPI는 신규 발급이 사실상 중단됐다.
- 약관: 서버가 사용자 세션으로 자동 발행하는 것은 네이버 이용약관 §7
(자동화 수단 접근 금지)에 정면 위반이다. - 에스컬레이션 리스크: 다수 계정 대량 발행으로 간주되면 계정 정지를 넘어
정보통신망법 책임까지 갈 수 있다. 봇 탐지, 2FA/캡차 우회 부담은 덤이다.
기술적으로는 headless browser로 다 된다. 하지만 "된다"와 "해도 된다"는 다른
질문이고, 개인 프로젝트라고 후자를 건너뛸 수 있는 게 아니다. 1편에서 세운
"발행 버튼은 사람이 누른다"는 원칙이 여기서 법적 리스크 판단과 정확히 겹쳤다.
검토하고 버린 경로들 (ADR 007):
| 대안 | 탈락 이유 |
|---|---|
| 서버 headless browser 자동 발행 | 약관 §7 위반 + 봇 탐지·계정 정지 + 형사 책임 가능성 |
| 네이버 공식 OpenAPI | 신규 발급 중단으로 접근 자체가 불가 |
| 본문에 public URL로 이미지 삽입 | 네이버는 외부 <img>를 자기 CDN으로 복사하지 않는다 → 외부 호스팅 의존 + 이미지 깨질 위험 |
| Electron 데스크톱 앱 | webview로 SmartEditor 접근은 가능하나 OS별 빌드·서명·자동 업데이트 파이프라인 비용이 불균형. 설치 마찰도 큼 |
| 클립보드 복사 유지 | 이미지 미전달이라는 근본 결함이 그대로 |
남는 답이 하나 있었다. 사용자 본인 PC에서, 본인 네이버 세션으로, 본인이 발행
버튼을 직접 누르는 브라우저 확장. "본인이 자기 계정의 작성을 보조받는" 형태라
약관 리스크가 가장 낮고, 이미지도 네이버 자체 업로드 경로를 태워 네이버 CDN에
정상 저장된다. Chrome/Whale(둘 다 Chromium, manifest v3) 타겟 — Whale은 네이버
사용자층에 네이버 로그인 세션이 상존한다는 점까지 계산에 넣었다.
설계: 확장은 stateless 주입기
@startuml
skinparam shadowing false
autonumber
participant "momently_console\n(결과 화면)" as C
participant "content script\n(콘솔 도메인)" as CS
participant "background\nservice worker" as B
participant "content script\n(blog.naver.com)" as NS
actor "사용자" as U
U -> C : "네이버로 보내기" 클릭
C -> CS : window.postMessage\n{title, hashtags, blocks[], imageUrls[], nonce}
CS -> CS : origin 검증 + nonce 중복 거부
CS -> B : chrome.runtime.sendMessage
B -> B : payload 스키마 검증\nimageUrls SSRF allowlist 검사
B -> B : 서명 URL로 이미지 binary\n병렬 fetch (Promise.all)
B -> NS : 글쓰기 탭으로 relay
NS -> NS : 본문 블록 순서대로 주입\n이미지는 네이버 업로드 경로로\n제목/해시태그 채움
NS --> U : 발행 버튼은 누르지 않음
U -> U : 검토 후 직접 발행 ★
@enduml
핵심 결정들:
- 콘솔 → 확장은 postMessage 핸드셰이크. 콘솔이 이미 로그인과 워크플로
세션을 갖고 있으니, 확장이 별도 토큰을 관리하면 자격증명 표면적만 는다.
확장은 콘솔이 건넨 payload를 주입할 뿐인 stateless 컴포넌트로 유지한다. - 이미지는 URL만 전달, binary는 확장이 fetch. postMessage로 binary를 넘기면
payload가 비대해진다. 짧은 TTL의 서명 URL 목록만 넘기고 background가 병렬로
가져온다. - 네이버 세션 불간섭. 확장은 네이버 쿠키를 읽지도 저장하지도 않는다. DOM
주입만 한다. - 주입 도메인 화이트리스트: 콘솔 오리진과
*.blog.naver.com두 곳에만
content script를 주입한다. 스토어 심사에서 권한 최소화 정당화와도 직결된다.
그리고 안전선: 발행 버튼은 채우기만 하고 절대 클릭하지 않는다. 이 한 줄이
이 ADR의 무게중심이다. "필드 자동 채움"과 "자동 발행" 사이가 약관 해석의
경계선이고, 사용자가 최종 검토 후 직접 발행하면 자동화 게시가 아니라 작성 편의
도구로 남는다. 동시에 AI 생성 글이 무검토로 대량 발행되는 품질 리스크도 같은
선에서 차단된다 — 1편의 "초안까지만" 원칙이 여기서 법적 안전선과 한 몸이 됐다.
솔직하게 적어두자면, ADR의 Cons 섹션에는 이것도 정면으로 인정해 뒀다:
content script의 프로그래매틱 DOM 채움 자체가 "자동화 수단"으로 해석될
가능성은 남는다. 발행 버튼 비클릭만으로 완전 면책되는 게 아니다. 그래서
보완 장치를 겹으로 뒀다 — 그중 첫째가 remote kill switch다. 확장이 기동 시
서버 config를 조회해 enabled: false면 즉시 기능을 끈다. 네이버 정책 변경이나
차단 탐지 시, 스토어 심사(수일)를 기다리지 않고 원격으로 끌 수 있다. 그리고
config 조회가 실패하면 fail-closed(기능 비활성화) — 안전장치가 네트워크
장애로 무력화되면 안 되니까. 5편의 internal token default-deny와 같은 문법이다.
postMessage는 생각보다 위험하다
"같은 origin이니 안전하다"로 끝나지 않는다. 다층으로 방어했다.
- target origin 명시: 와일드카드 금지, 구체 origin만. 수신 측도
event.origin일치 검사. - one-time nonce: 콘솔이 발급한 nonce를 background가 LRU로 기억해 중복
거부. 캡처-재전송(replay) 차단. - payload 스키마 검증: 허용 origin이라도 malformed payload(필드 누락, 과대
크기, 제어문자)는 거부. 콘솔에 XSS가 생기면 "신뢰된 origin"이 적이 되기
때문이다 — origin 검증은 1차 방어일 뿐이라는 위협 경계를 문서에 명시했다. - SSRF allowlist: imageUrls의 host를 허용 목록으로 제한하고
169.254.169.254,localhost, 사설망 대역,file:스킴을 거부한다. 확장의
background fetch는 그럴싸한 SSRF 프록시가 될 수 있는 위치다.
초안이 실측에 얻어맞은 날들
이 ADR이 특히 기억에 남는 이유는, 초안의 가정 여럿이 구현 단계 실측에서
틀린 것으로 판명됐고 그 수정 과정이 그대로 문서에 남았기 때문이다.
가정 1: "이미지는 오브젝트 스토리지에 있고 presigned URL을 쓰면 된다" —
틀림. 코드를 추적해 보니 본문 이미지는 MinIO가 아니라 orchestrator 로컬
디스크에 있었다(MinIO는 voice 전용, 5편). 업로드 사진은 원본 파일명을 버리고{projectId}/0001.jpg 순번으로 저장되고, 기존 서빙 엔드포인트는 JWT 인증을
요구한다 — 확장의 background fetch는 인증 헤더를 못 싣는다. 그래서 "signed
URL"의 실체를 새로 설계했다: 콘솔이 JWT 인증으로 서명 발급을 요청하면
(POST .../files/sign), orchestrator가 HMAC-SHA256 서명 토큰을 쿼리스트링에
박은 단기 TTL 엔드포인트(GET .../signed-files/{name}?exp=&sig=)를 내준다.
이 GET만 permitAll이고 서명 검증이 곧 인가다(상수시간 비교, 만료 검사, 발급
시점에 소유권 검사). 서명 키는 JWT 시크릿을 도메인 라벨로 분리해 재사용.
가정 2: "SmartEditor 이미지는 file input에 DataTransfer로 주입" — 절반만
맞음. 텍스트 주입을 실측해 보니 file input이 아니라 에디터의 JS 문서
API(getDocumentData()/setDocumentData())로 컴포넌트를 채우는 경로가 실제로
동작했다. 이미지의 네이버 CDN 업로드 경로는 로그인 세션이 필요해서 헤드리스로
확인이 불가능했고 — 라이브 세션에서 캡처하는 프로브 스크립트로 확정하는
단계를 계획에 넣었다.
가정 3: 확장 ID와 CORS. manifest v3의 background fetch origin은chrome-extension://<id>인데 dev와 스토어 배포의 ID가 다르고, 많은 스토리지가
이 스킴을 CORS origin으로 인식하지 않는다. manifest key 고정으로 ID를
안정화하고 host_permissions에 서비스 origin을 추가하는 것으로 정리했다.
ADR "초안(Proposed)"과 "구현 노트" 섹션이 이렇게 공존하는 게 이 문서의
가치라고 생각한다. ADR은 결정의 스냅샷이 아니라 결정의 이력이다. 초안의
가정이 틀렸다는 기록이 남아 있으면, 다음 사람(대개 미래의 나)이 같은 가정으로
출발하지 않는다.
적용은 7a(핸드셰이크 + 보안 검증만) → 7b(텍스트 주입, 셀렉터 단일 모듈화 +
미스매치 시 "확장 업데이트 필요" 안내로 graceful 실패) → 7c(이미지 파이프라인,
PoC 선행) → 7d(콘솔 payload 빌더, 확장 미설치 사용자는 클립보드 fallback 유지)
→ 7e(스토어 심사 + 네이버 정책 모니터링 책임 지정)로 단계화했다. 워크플로
도메인에는 발행 상태를 추가하지 않았다 — 발행은 사용자의 행위지 파이프라인의
단계가 아니다. 상태 머신(3편)의 순수성을 지키는 마지막 결정이었다.
이 결정을 다시 한다면
브라우저 확장이라는 방향은 다시 골라도 같다. 다만 코드 실측을 ADR 초안
전에 했어야 했다. "이미지는 presign으로"라는 가정은 레포를 30분만 추적했으면
초안 단계에서 걸렀을 것이다. ADR을 쓰는 목적이 가정을 명시하는 것이라면, 쓰기
전에 가장 싼 검증(코드 읽기)부터 하는 게 순서였다. 그래도 가정이 틀렸음을
문서에 남기고 고친 것은 — ADR을 장식이 아니라 도구로 쓴 증거라 만족한다.
마지막 편에서는 이 모든 걸 돌리는 바닥 — Mac mini 한 대 위의 Docker Compose
15개 서비스, Cloudflare Tunnel, 그리고 "gateway만 재배포했는데 백엔드가
내려갔다" 같은 운영 사고들을 다룬다. 그리고 이 시리즈가 가능했던 이유이기도 한
질문 — 혼자 개발하면서 왜 ADR을 쓰는가 — 로 마무리한다.
'AI블로그 자동 포스팅 - Momently' 카테고리의 다른 글
| [Momently 개발기 #10·完] 혼자 개발하면서 ADR을 쓰는 이유 — 운영과 회고 (0) | 2026.07.19 |
|---|---|
| [Momently 개발기 #8] 사진 속 단어 하나에 글 전체가 납치당했다 — 사용자 의도와 프롬프트 우선순위 (0) | 2026.07.19 |
| [Momently 개발기 #7] 에이전트 하나가 느려지면 전체가 죽는다 — Circuit Breaker & Bulkhead (0) | 2026.07.19 |
| [Momently 개발기 #6] 에이전트 10개의 에러가 제각각이던 날 (0) | 2026.07.19 |
| [Momently 개발기 #5] 내 말투를 학습시키기 — voice profile과 "보내고 끝" 버그 (0) | 2026.07.19 |