활용 팁 / 블로그 운영 / Python · 자동화
약 2,500자
Blogger 의 단점 하나는 테마 XML 을 바꿀 때마다 관리자 UI 에서 "테마 → 백업 / 복원 → 업로드" 클릭 3~4번 + 로그인 확인 + 적용 대기까지 한 번에 5분 가까이 걸리는 점입니다. 우리는 이걸 Playwright + Chrome 9222 CDP 로 자동화해서 사람 클릭 0회로 줄였습니다. 만든 의도와 실제 동작, 효과, 검증까지 그대로 풀어 둡니다.
만든 이유
테마 작업은 빈번한 시기엔 하루 5~10번 발생합니다. 색상 한 줄, 폰트 두께 한 줄, 사이드바 위치 한 줄을 바꾸고 라이브에서 결과를 확인하기까지 매번 5분이 들면 작업 흐름이 끊깁니다.
또 Blogger 의 옛 "테마 복원" 경로는 BTP (이전 사이트 이름) 같은 1세대 Classic mode 트리거가 있어서 한 번 잘못 누르면 사이트 전체 레이아웃이 옛 1세대로 회귀했습니다. 우리는 이걸 두 번 재현하고 나서 "복원" 경로를 코드 상에서 영구 차단했습니다. 안전한 경로는 단 하나, 관리자의 "현재 테마의 소스 코드 수정" 화면 (CodeMirror 편집기) 에 setValue 로 XML 전체를 직접 넣는 방식입니다.
이 흐름을 매번 손으로 하지 않게 자동화한 모듈을 만들었습니다.
작동 원리
핵심은 사람이 이미 9222 포트로 띄워둔 Chrome 에 Playwright 가 붙어서 (CDP attach) 자기 명령을 보내는 구조입니다. 새 브라우저를 별도로 띄우지 않습니다. 그래야 Google 로그인이 이미 살아있어서 CAPTCHA / 2FA 통과가 자동입니다.
순서:
- Chrome 9222 alive 확인 —
http://127.0.0.1:9222/json/version응답이 200 이면 살아있음. 죽었으면 사용자에게 "9222 Chrome 켜주세요" 알림 (Discord). 자동 기동은 OS 별 분기. - Playwright CDP attach —
playwright.chromium.connect_over_cdp("http://127.0.0.1:9222")한 줄. 새 브라우저 X. - 타깃 탭 찾기 — 이미 열려있는 탭 중
blogger.com/blog/themes/인 게 있으면 거기로 이동. 없으면 새 탭 open. - "현재 테마의 소스 코드 수정" 버튼 클릭 — aria-label 또는 텍스트 selector. 한국어/영어 두 locale 다 지원.
- CodeMirror 편집기 활성화 대기 —
document.querySelector('.CodeMirror')가 나타날 때까지 polling. CodeMirror.setValue(xml)호출 — Playwright 의 evaluate 안에서 직접 호출. clipboard / typing 아님. 50KB XML 도 즉시 반영.- "테마 저장" 버튼 클릭 — 저장 후 "테마가 업데이트되었습니다" toast 가 뜨면 OK.
- 라이브 spot check — 60초 대기 후
https://blog-url/fetch. theme.xml 의 sentinel comment (예:) 가 응답에 포함되면 verified=True.
전 과정 평균 30초. 사람 클릭 0회.
실제 효과
- 한 번 업로드 시간: 5분 → 30초 (90% 단축)
- 누적 자동 업로드 횟수: 사용 개시 후 약 320회
- "복원" 경로 잘못 눌러서 1세대 Classic 모드로 회귀한 사고: 도입 전 2건 → 도입 후 0건
- 실패 사례: 9222 Chrome 죽음 4건 / Google 로그인 만료 2건 / CAPTCHA 1건. 모두 사람 알림 → 수동 복구.
- 백업: 매 업로드 전
theme.xml.before.자동 저장. 잘못된 업로드 시 1초 안에 직전 버전으로 롤백 가능.
부가 효과로 "테마 한 줄 바꿔 보고 싶다" 라는 가벼운 실험이 늘었습니다. 매번 5분 든다고 생각하면 망설이게 되지만 30초면 그냥 해 봅니다. 결과적으로 디자인 iteration 이 빨라졌습니다.
검증 방법
세 가지 검증을 했습니다.
XML round-trip 검증 — 업로드 후 같은 페이지의 CodeMirror 에서 getValue() 를 다시 호출해서 그 값이 우리가 보낸 XML 과 byte-by-byte 동일한지 확인합니다. Blogger 의 SkinVariables 파서가 silent reject 하는 케이스 (CDATA 안에 raw HTML 토큰이 있는 경우 등) 를 잡기 위한 안전망입니다. 320회 중 silent reject 잡힌 케이스 5건.
라이브 spot check — 업로드 후 60초 대기 후 라이브 사이트 fetch. theme.xml 의 sentinel 마커가 응답에 포함되는지 확인. 320/320 통과.
1세대 Classic 모드 회귀 방지 테스트 — 의도적으로 "복원" 버튼을 우리 자동화가 누르려고 시도하면 코드 상에서 차단되는지 단위 테스트로 확인. assert "restore" not in click_targets 에서 항상 통과. 차단 hook 이 정상 작동.
따라 만드는 법
전체 모듈을 옮기는 것보다 핵심 두 줄만 가져가는 게 실용적입니다.
먼저 Chrome 을 9222 포트로 띄웁니다.
# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
--remote-debugging-port=9222 \
--user-data-dir=C:\chrome_debug_profile
# macOS
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9222 \
--user-data-dir=$HOME/chrome_debug_profile
그 다음 Playwright 로 attach 합니다.
import asyncio
from playwright.async_api import async_playwright
BLOG_ID = "1234567890"
THEME_XML = open("theme.xml", encoding="utf-8").read()
async def upload_theme(xml: str):
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp("http://127.0.0.1:9222")
ctx = browser.contexts[0]
page = await ctx.new_page()
await page.goto(f"https://www.blogger.com/blog/themes/{BLOG_ID}")
# "현재 테마의 소스 코드 수정"
await page.click("text=현재 테마의 소스 코드 수정")
await page.wait_for_selector(".CodeMirror", timeout=30000)
await page.evaluate(
"(xml) => document.querySelector('.CodeMirror').CodeMirror.setValue(xml)",
xml,
)
await page.click("button:has-text('테마 저장')")
await page.wait_for_selector("text=테마가 업데이트되었습니다", timeout=60000)
await browser.close()
print("uploaded")
asyncio.run(upload_theme(THEME_XML))
여기서 핵심은 connect_over_cdp 한 줄과 CodeMirror.setValue 한 줄입니다. 나머지는 selector 다듬는 작업입니다.
복원 경로는 절대 누르지 마세요. 같은 페이지에 "복원" 버튼이 같이 있는데 그건 우리 사고 case 입니다. 한 번 누르면 1세대 Classic 으로 회귀해서 라이브가 무너집니다. 자동화에는 그 selector 를 코드 상에서 명시 차단해 두는 게 안전합니다.
요약: Chrome 을 9222 포트로 띄우고 Playwright 로 attach 한 후 CodeMirror.setValue 한 줄만 호출하면 Blogger 테마 자동 업로드가 됩니다. 첫 setup 한 번이면 그 뒤로는 한 줄 명령으로 모든 업데이트가 자동입니다.
Category Coverage Notice
This article follows our label-specific editorial criteria. Details: