블로그 글 한 편을 올리는 데 실제로 얼마나 걸리는지 재본 적이 있습니다. 원고를 다 써둔 상태에서도 브라우저를 열고, 관리자에 로그인하고, 제목과 본문을 붙여넣고, 카테고리를 고르고, 대표이미지를 올리고, 예약 시간을 지정하기까지 12분이 걸렸습니다. 매일 한 편이면 한 달에 여섯 시간입니다. 글쓰기가 아니라 업로드 작업에만 쓰는 시간이죠. 그래서 블로그 자동 포스팅 시스템을 직접 만들었습니다. 이 글은 그 과정에서 실제로 겪은 선택과 실수를 정리한 후기입니다.
브라우저 자동화를 먼저 시도했고, 버렸습니다
처음 떠올린 방법은 Selenium이나 Playwright로 관리자 화면을 그대로 조작하는 것이었습니다. 사람이 하던 걸 그대로 흉내 내면 되니 직관적이거든요. 2주 정도 쓰다가 접었습니다. 이유는 세 가지였습니다.
- 깨지는 지점이 너무 많다. 에디터가 업데이트되면서 버튼 위치가 바뀌면 그날 발행이 통째로 실패합니다.
- 느리다. 페이지 로딩을 기다리느라 한 편에 40초 이상 걸렸습니다.
- 성공했는지 알 수가 없다. 스크린샷을 보고 사람이 판단해야 합니다. 실패해도 조용히 넘어갑니다.
워드프레스는 /wp-json/wp/v2/ 아래로 REST API를 기본 제공합니다. 글 작성, 카테고리 생성, 이미지 업로드가 전부 HTTP 요청 한 번입니다. 응답이 JSON으로 돌아오니 성공 여부를 코드로 검증할 수 있다는 점이 결정적이었습니다.
1단계 — 애플리케이션 비밀번호로 글 하나 올려보기
로그인 비밀번호를 스크립트에 넣으면 안 됩니다. 워드프레스는 API 전용 자격증명인 애플리케이션 비밀번호를 따로 제공합니다. 관리자 > 사용자 > 프로필 맨 아래에서 이름을 붙여 발급하면 abcd efgh ijkl mnop 형태의 문자열이 한 번만 표시됩니다. 유출되면 그것만 폐기하면 되고, 로그인 비밀번호는 안전합니다.
import requests
from requests.auth import HTTPBasicAuth
SITE = "https://example.com"
USER = "myaccount"
APP_PW = "abcd efgh ijkl mnop qrst uvwx" # 애플리케이션 비밀번호(공백 포함 그대로)
payload = {
"title": "REST API로 올린 첫 글",
"content": "<p>본문입니다.</p>",
"status": "draft", # 처음에는 무조건 draft로 시작한다
"categories": [9], # 카테고리 ID (숫자)
}
r = requests.post(
SITE + "/wp-json/wp/v2/posts",
json=payload,
auth=HTTPBasicAuth(USER, APP_PW),
timeout=30,
)
print(r.status_code) # 201이면 성공
print(r.json()["id"], r.json()["link"])
여기서 중요한 건 "status": "draft"입니다. 처음 며칠은 무조건 초안으로만 올리고 관리자 화면에서 눈으로 확인하세요. 블록이 깨지거나 카테고리가 엉뚱하게 잡히는 건 대부분 첫 주에 다 드러납니다.
2단계 — 본문을 손으로 조립하지 않기
가장 오래 헤맨 부분입니다. content에 그냥 HTML을 넣으면 발행은 됩니다. 그런데 나중에 편집기로 열면 글 전체가 “클래식 블록” 덩어리 하나로 보입니다. 문단 하나를 고치려 해도 손댈 수가 없습니다.
구텐베르크는 HTML 주석으로 블록 경계를 표시합니다. 그 주석 형식을 정확히 맞춰줘야 편집 가능한 글이 됩니다. 매번 손으로 쓰면 반드시 틀리므로, 문단·제목·코드 각각에 대한 작은 헬퍼 함수를 만들어 두는 편이 훨씬 안전했습니다.
// blocks.js — 구텐베르크 블록 주석을 손으로 쓰지 않기 위한 최소 헬퍼
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");
const p = (html) =>
"<!-- wp:paragraph -->\n<p>" + html + "</p>\n<!-- /wp:paragraph -->";
const h2 = (text) =>
'<!-- wp:heading -->\n<h2 class="wp-block-heading">' + text +
"</h2>\n<!-- /wp:heading -->";
const code = (src) =>
"<!-- wp:code -->\n<pre class=\"wp-block-code\"><code>" +
esc(src).replace(/\u005B/g, "[") + // 대괄호는 숏코드로 오인되므로 이스케이프
"</code></pre>\n<!-- /wp:code -->";
module.exports = { p, h2, code };
이렇게 해두면 원고 파일은 h2("설치하기"), p("먼저 ..."), code(snippet) 배열을 나열하는 것으로 끝납니다. 주석 형식 실수가 원천적으로 사라집니다.
3단계 — 스케줄러에 걸고 하루 여유를 두기
스크립트가 완성되면 윈도우 작업 스케줄러(맥·리눅스라면 cron)에 매일 오전 9시로 등록합니다. 여기서 한 가지 설계를 바꿨는데, 결과적으로 가장 잘한 선택이었습니다. 오늘 만든 글을 오늘 공개하지 않고, 내일 오전 9시로 예약합니다.
# 내일 오전 9시로 예약 발행
from datetime import datetime, timedelta
when = (datetime.now() + timedelta(days=1)).replace(
hour=9, minute=0, second=0, microsecond=0
)
payload = {
"title": title,
"content": body,
"status": "future", # publish가 아니라 future
"date": when.strftime("%Y-%m-%dT%H:%M:%S"), # 사이트 로컬 시간 기준
}
하루의 시차가 검토 시간이 됩니다. 아침에 예약 대기 목록을 열어 훑어보고, 어색한 문장은 고치고, 마음에 안 들면 예약을 취소합니다. 자동화가 사람을 대체하는 게 아니라 사람이 검토할 것만 남기는 구조가 됩니다. 큐에는 항상 한 편만 두도록 제한해서, 밀린 글이 무더기로 쌓이는 일도 막았습니다.
실제로 사고가 났던 네 지점
돌아보면 시스템이 멈춘 날은 전부 “에러가 안 나서” 문제였습니다. 조용히 실패하는 게 제일 무섭습니다.
| 증상 | 진짜 원인 | 대응 |
|---|---|---|
| 모든 API 요청이 403 | CDN 봇 차단이 기본 HTTP 라이브러리를 걸러냄 | 브라우저 User-Agent 헤더를 붙이거나 방화벽에 예외 등록 |
| 스케줄러는 “성공”, 실제론 아무 일 없음 | PowerShell 스크립트를 BOM 없는 UTF-8로 저장해 한글 경로가 깨짐 | UTF-8 BOM으로 저장하고 배포 전 드라이런으로 경로 확인 |
| 코드 블록 일부가 통째로 사라짐 | 대괄호를 워드프레스가 숏코드로 해석 | 코드 삽입 시 대괄호를 [로 이스케이프 |
| 12편이 대표이미지 없이 발행됨 | 업로드 기능이 없었고 검증도 이미지를 안 봄 | 검증 항목에 featured_media 확인을 추가 |
마지막 항목이 특히 뼈아팠습니다. 매일 “검증 통과” 로그가 찍히고 있었는데, 검증이 확인하지 않는 항목은 애초에 존재하지 않는 것과 같았습니다. 검증 스크립트는 실패할 수 있어야 의미가 있습니다. 그래서 서버 응답을 다시 읽어 실제 값과 대조하도록 고쳤습니다.
# 업로드한 대표이미지가 진짜로 걸렸는지 확인한다
res = requests.post(
SITE + "/wp-json/wp/v2/posts/" + str(post_id),
json={"featured_media": media_id},
auth=HTTPBasicAuth(USER, APP_PW),
timeout=30,
).json()
if res.get("featured_media") != media_id:
raise SystemExit("대표이미지 연결 실패: 응답값 %s" % res.get("featured_media"))
워드프레스는 존재하지 않는 첨부 ID를 보내도 에러 대신 200을 돌려줍니다. 요청이 성공한 것과 의도한 결과가 반영된 것은 다른 이야기입니다.
만들고 나서 달라진 것
현재 이 시스템은 매일 오전 9시에 돌아가고, 저는 예약 목록을 훑어보는 데 하루 3~4분을 씁니다. 업로드 작업에 쓰던 12분이 사라졌으니 한 달 기준 네 시간 정도가 남았고, 그 시간은 대부분 원고 자체를 다듬는 데 들어갑니다.
다만 솔직하게 적자면, 만드는 데 든 시간이 아낀 시간보다 많습니다. 초기 구현에 이틀, 위에 적은 사고들을 잡는 데 며칠이 더 들었습니다. 3개월쯤 지나서야 본전에 도달했습니다. 한두 달 하고 그만둘 일이라면 자동화하지 않는 편이 낫습니다. 반대로 1년 이상 이어갈 작업이라면, 시간보다도 “빼먹지 않는다”는 점이 더 큰 이득이었습니다.
시작한다면 이 순서로
- 애플리케이션 비밀번호를 발급하고, 초안 한 편을 API로 올려본다. 여기까지 30분.
- 자주 쓰는 블록(문단·제목·코드) 헬퍼를 만들어 원고 파일 형식을 정한다.
- 이미 써둔 글 한 편을 그 형식으로 옮겨 초안 발행하고, 편집기에서 블록이 살아있는지 확인한다.
- 스케줄러에 등록하되, 즉시 발행이 아니라 다음 날 예약으로 건다.
- 발행 후 응답을 다시 읽어 제목·카테고리·대표이미지를 대조하는 검증을 붙인다.
1번만 해봐도 감이 옵니다. 자동화의 성패는 결국 “실패했을 때 내가 알 수 있는가”에서 갈립니다.
마무리
자동화 시스템을 만들 때 가장 공들여야 할 부분은 발행 로직이 아니라 검증 로직이었습니다. 잘 도는 날은 어차피 신경 쓸 일이 없고, 문제는 조용히 어긋나는 날에만 생기기 때문입니다.
다음 글에서는 이 시스템의 로그를 실제로 어떻게 남기고 있는지, 실패했을 때 알림을 받는 방법을 다뤄보겠습니다.