IT

Playwright로 티스토리 글 올리기, 본문이 빈 채 저장되던 이유

밍글링글링 2026. 9. 10.
728x90

티스토리 오픈 API가 2024년 2월에 완전히 문을 닫은 뒤로, 글을 프로그램으로 올리는 길은 브라우저를 직접 조작하는 것뿐이다. 이 글은 Playwright로 티스토리 에디터를 자동화하다가 겪은 가장 당혹스러운 문제, 즉 스크립트는 성공했다고 하는데 열어보면 제목만 있고 본문이 비어 있는 현상의 원인과 해결을 다룬다. 결론을 먼저 말하면, 에디터의 자바스크립트 API로 값을 넣으면 화면에는 보이지만 저장되지 않고, 실제 키 입력으로 넣어야 저장된다.

오픈 API가 사라진 자리를 브라우저 자동화가 메운다

티스토리는 2023년 12월 22일 공지에서 오픈 API를 "파일 첨부 > 글 관련 기능 > 댓글 관련 기능 > 그 외 기능" 순으로 2024년 2월 말까지 순차 종료한다고 알렸다. 그 뒤로 POST /post/write 같은 정석 경로는 없다. 남은 방법은 사람이 하는 일을 그대로 흉내 내는 것이다. 로그인한 브라우저로 글쓰기 화면을 열고, 제목을 치고, 본문을 넣고, 저장 버튼을 누른다.

나는 이 경로를 Playwright(Python)로 만들었다. 제목 입력은 #post-title-inpfill()로 끝났다. 본문은 기본 편집기가 TinyMCE라 다루기 번거로워서, 에디터 모드를 HTML로 바꾼 뒤 CodeMirror에 완성된 HTML을 통째로 넣는 쪽을 택했다. 여기까지는 어느 자동화 글에나 나오는 흔한 설계다.

setValue는 화면만 바꾸고 저장은 바꾸지 않았다

CodeMirror 5는 래퍼 DOM 요소에 인스턴스가 붙어 있어서 브라우저 콘솔에서 document.querySelector('.CodeMirror').CodeMirror로 잡을 수 있다. 그래서 첫 구현은 이랬다.

page.evaluate("""(html) => {
    const el = document.querySelector('.CodeMirror');
    el.CodeMirror.setValue(html);
}""", html)
page.click("a.action:has-text('임시저장')")

임시저장 개수가 0에서 1로 늘었고 스크립트는 "완료"를 찍었다. 그런데 관리 화면에서 그 글을 열자 제목만 복원되고 본문은 빈 칸이었다. 에디터 화면에서는 분명히 HTML이 들어가 있는 것을 스크린샷으로도 확인했는데 말이다.

CodeMirror 매뉴얼에 따르면 doc.setValue(content)는 "에디터 내용을 설정"하고, 내용이 바뀔 때마다 change 이벤트가 {from, to, text, removed, origin} 객체와 함께 발생한다. 그러니 이벤트가 안 난 것은 아니다. 문제는 티스토리 쪽이다. 에디터 래퍼의 클래스명이 ReactCodemirror였다. 편집기 위에 React 상태가 한 겹 더 있고, 저장 버튼은 CodeMirror가 아니라 그 React 상태를 읽어 서버로 보낸다. 그 상태가 갱신되지 않은 것이다.

같은 종류의 문제가 React 폼 입력에서 잘 알려져 있다. Cory Rylan의 글은 "React는 입력 상태가 언제 설정되고 바뀌었는지 알기 위해 input의 value setter를 오버로드한다"고 설명한다. DOM 값을 직접 바꾸면 React의 추적 장치가 변화를 모른 채 넘어간다. 티스토리 에디터가 정확히 어떤 경로로 상태를 갱신하는지는 코드를 보지 않아 확언할 수 없다. 다만 프로그램적 변경은 무시되고 실제 입력만 반영된다는 점은 관찰로 확인했다.

실제 키 입력으로 넣으면 코드 들여쓰기까지 그대로 저장된다

해결은 사람이 하는 대로 하는 것이었다. 에디터를 클릭해 포커스를 주고, 전체 선택 뒤에 텍스트를 넣는다.

cm = page.locator(".CodeMirror").first
cm.wait_for(state="visible", timeout=15000)
cm.click()
page.keyboard.press("Control+a")
page.keyboard.insert_text(html)

keyboard.insert_text는 Playwright 문서대로 "input 이벤트만 보내고 keydown, keyup, keypress는 내지 않는다." 글자 하나씩 키를 누르는 keyboard.type보다 훨씬 빠르면서도, 에디터 입장에서는 사용자가 붙여넣기한 것과 같다. 이 방식으로 넣은 글은 재오픈했을 때 제목, 본문, H2 세 개가 그대로 있었다.

한 가지 걱정은 코드블록이었다. CodeMirror의 HTML 모드는 줄바꿈 뒤에 자동 들여쓰기를 붙일 수 있어서, <pre><code> 안의 파이썬 코드가 망가질 가능성이 있었다. 4단계 들여쓰기가 있는 코드를 넣고 getValue()로 되읽어 원본과 문자열 비교를 했다. 완전히 일치했다.

조용한 성공보다 시끄러운 실패가 낫다

이 버그의 진짜 문제는 실패가 조용했다는 점이다. 스크립트는 성공 코드를 돌려줬고, 임시저장 개수도 늘었다. 사람이 열어보기 전까지는 아무도 몰랐다. 매일 자동으로 발행하는 파이프라인이었다면 빈 글이 며칠씩 올라갔을 것이다.

그래서 본문을 넣은 직후 에디터에서 값을 되읽어 입력값과 대조하는 검증을 넣었다. 다르면 예외를 던지고 저장 단계로 가지 않는다.

got = page.evaluate("() => document.querySelector('.CodeMirror').CodeMirror.getValue()")
if got.strip() != html.strip():
    raise RuntimeError(f"본문 불일치: 입력 {len(html)}자 / 에디터 {len(got)}자")

반론이 있을 수 있다. 되읽기 검증은 CodeMirror 값만 보므로, 이번 버그처럼 CodeMirror는 맞는데 React 상태가 틀린 경우는 못 잡는다는 것이다. 맞는 지적이다. 이 검증은 "글이 안 들어간" 경우를 잡지, "들어갔는데 저장 안 된" 경우를 잡지 못한다. 후자는 저장 뒤 재오픈해서 확인하는 통합 테스트로만 잡힌다. 그래도 넣어둘 가치는 있다. 다음에 티스토리가 에디터를 바꿔 입력 자체가 실패하면, 그날 바로 알 수 있기 때문이다.

자동화가 조용히 망가지는 것을 막는 방법은 결국 하나다. 성공했다고 믿지 말고, 결과를 다시 읽어서 확인한다.

참고 자료

728x90

댓글