워드프레스 자동 포스팅: REST API로 마크다운을 발행하는 파이프라인
워드프레스 자동 포스팅 한 줄 요약
Application Password부터 발급하세요. 그다음 마크다운을 HTML이나 블록 마크업으로 바꿔서
POST /wp-json/wp/v2/posts에draft로 보내면 워드프레스 자동 포스팅 기본 흐름은 끝나요. 이미지는/wp-json/wp/v2/media에 먼저 올리고featured_media를 붙이면 되고, 로그인 비밀번호를 쓰면 401이 나기 쉬워요.
워드프레스 대시보드 열고 글 붙여넣는 작업, 열 번만 넘어가도 진짜 하기 싫어져요. 그래서 워드프레스 자동 포스팅을 붙이려는데, 막상 첫 단계부터 삽질이 많아요. 로그인 비밀번호로 인증했다가 401이 뜨고, /wp-json/는 404가 나고, 겨우 올렸더니 구텐베르크 블록 에디터에서는 HTML 한 덩어리로만 보여서 다시 손을 대야 하거든요. 직접 해보면 코드 30줄보다 주변 조건 맞추는 데 더 오래 걸려요.
흐름은 네 단계로 보면 편해요. 엔드포인트와 인증 맞추기, 마크다운을 HTML이나 블록 마크업으로 바꾸기, draft로 먼저 올리기, 이미지와 예약 발행 정보를 붙이기. 여기까지 해두면 수동 복붙 작업은 거의 사라져요. 실제로 저는 이 흐름을 만든 뒤로 글 하나 올리는 데 3분이면 끝나요. 2026년 4월 기준으로 공식 문서 흐름은 여전히 wp/v2 기준이고, Application Password도 그대로 쓸 수 있어요. 여기서는 처음 만드는 사람 기준으로, 실패 포인트부터 짚고 바로 발행까지 가볼게요.
워드프레스 자동 포스팅 전에 URL과 인증부터 맞추기
워드프레스 자동 포스팅은 주소와 인증이 맞아야 나머지가 돌아가요. 여기서 틀리면 본문 변환을 아무리 잘해도 401이나 404만 봐요.
설마 로그인 비밀번호로 바로 때리고 있진 않죠?
2026년 4월 기준 공식 문서를 직접 테스트해본 흐름이에요. 셀프호스트 쪽은 wp-json/wp/v2를 쓰고, 원격 호출 인증은 보통 워드프레스 Application Password로 가요. 워드프레스닷컴은 URL 구조가 다를 수 있어서, 같은 글 생성이라도 주소부터 다시 봐야 해요.
| 상황 | 글 생성 엔드포인트 예시 | 인증 메모 | 자주 막히는 지점 |
|---|---|---|---|
| 셀프호스트 | https://example.com/wp-json/wp/v2/posts |
Application Password | 가장 단순 |
| 고유주소가 기본형인 사이트 | https://example.com/?rest_route=/wp/v2/posts |
Application Password | /wp-json/가 404로 보일 수 있음 |
| WordPress.com 커스텀 도메인 사이트 | https://public-api.wordpress.com/wp/v2/sites/example.com/posts |
WordPress.com 문서 기준 인증 흐름 재확인 | 주소 구조가 다름 |
# 인증 먼저 보기
curl --user "writer:<application-password>" \
https://example.com/wp-json/wp/v2/users/me
# 출력 예시
{"id":"<user_id>","slug":"writer","name":"작성자 이름"}
- 401(인증 실패)이면 로그인 비밀번호 대신 Application Password를 쓰는지 먼저 봐요.
- 403(권한 또는 보안 차단)이면 사용자 권한, 보안 플러그인, 방화벽 룰을 같이 봐야 해요.
- 406(서버 차단)이면 코드보다 서버 보안 모듈이나 프록시 헤더 전달이 먼저 문제인 경우가 있더라고요. 저도 Cafe24 호스팅에서 ModSecurity가 POST 요청을 막아서 한참 헤맸어요. 에러 로그를 보니까 WAF 룰셋이 JSON body를 공격으로 잡고 있었거든요.
- 로컬 HTTP 환경이면 Application Password 항목이 안 보일 수 있어요. 코어 문서 기준으로 SSL 또는
local환경에서 기본 지원돼요.
공식 문서는 REST API Authentication, Application Passwords, WordPress.com REST API URLs를 같이 보면 덜 헷갈려요. 고유주소나 기본 워드프레스 세팅이 아직 애매하면 워드프레스 SEO 설정: 설치 직후부터 스키마까지 한 번에 끝내기부터 먼저 잡아두는 편이 훨씬 덜 피곤해요.
파이썬 워드프레스 발행 워크플로우
파이썬 워드프레스 발행은 첫 성공 한 번만 만들어두면 뒤는 금방 빨라져요. 직접 써보니까 처음엔 카테고리나 대표 이미지 욕심내지 말고, 제목과 본문, draft 상태만 먼저 올리는 게 좋더라고요.
이걸 매번 손으로 붙여넣을 건 아니죠?
아래처럼 마크다운을 HTML로 바꾸고, 그 결과를 바로 draft로 밀어 넣으면 돼요. 실제로 돌려보면 30초도 안 걸려요. 본문 자동화에서 중요한 건 멋진 코드보다 실패했을 때 바로 다시 돌릴 수 있는 단순한 흐름이에요.
import pathlib
import requests
from requests.auth import HTTPBasicAuth
BASE_URL = "https://example.com"
USERNAME = "writer"
APP_PASSWORD = "<application-password>"
# 마크다운을 HTML로 바꿨다고 가정
html_content = pathlib.Path("build/post.html").read_text(encoding="utf-8")
payload = {
"title": "워드프레스 자동 포스팅 테스트",
"slug": "wordpress-rest-api-posting",
"status": "draft",
"content": html_content,
}
response = requests.post(
f"{BASE_URL}/wp-json/wp/v2/posts",
auth=HTTPBasicAuth(USERNAME, APP_PASSWORD),
json=payload,
timeout=30,
)
response.raise_for_status()
post = response.json()
print(post["id"], post["status"], post["slug"])
출력 예시는 이 정도면 충분해요.
<post_id> draft wordpress-rest-api-posting
체크할 건 세 개예요.
status가draft로 들어갔는지slug가 영문으로 원하는 값인지- 대시보드 글 목록에도 바로 보이는지
저는 처음 publish로 보내본 적이 있는데, 오타 하나 때문에 바로 공개된 글을 급하게 내렸어요. 그 뒤로는 무조건 draft로 먼저 올리고, 미리보기에서 본문과 이미지를 한 번 훑은 다음에 수동으로 공개로 바꿔요. 자동화는 빠르지만 실수도 빠르거든요.
앞단에서 브리프나 초안까지 같이 묶고 싶으면 AI 블로그 브리프 자동 생성: 실전 파이프라인 구축기를 이어 붙이면 좋아요. 그 글로 초안 재료를 만들고, 여기 코드로 최종 draft까지 밀어 넣는 식이면 흐름이 꽤 단단해져요.
카테고리 태그 대표 이미지 예약 발행까지 붙이기
워드프레스 자동 포스팅에서 여기부터가 실무예요. 제목하고 본문만 올리는 건 쉽지만, 카테고리, 태그, 대표 이미지, 예약 발행까지 묶으려면 순서를 잘 잡아야 덜 헤매더라고요.
ID를 하드코딩해두면 다음 달에도 안 터질까요?
직접 테스트해본 추천 순서는 이래요. 카테고리와 태그는 이름으로 찾고, 이미지는 먼저 업로드해서 media_id를 받고, 글 업데이트는 마지막에 한 번 더 보내는 식이 좋아요. 코어 문서 기준으로 글 업데이트는 PATCH보다 POST /wp/v2/posts/<id> 쪽을 보면 돼요.
| 작업 | 엔드포인트 | 챙길 값 |
|---|---|---|
| 카테고리 찾기 | GET /wp-json/wp/v2/categories?search=자동화 |
id, name |
| 태그 찾기 | GET /wp-json/wp/v2/tags?search=python |
id, name |
| 이미지 올리기 | POST /wp-json/wp/v2/media |
파일, alt_text |
| 글 다시 업데이트하기 | POST /wp-json/wp/v2/posts/<post_id> |
categories, tags, featured_media, status, date_gmt |
from pathlib import Path
import requests
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth("writer", "<application-password>")
base_url = "https://example.com"
def find_first_term_id(endpoint: str, keyword: str):
res = requests.get(
f"{base_url}/wp-json/wp/v2/{endpoint}",
auth=auth,
params={"search": keyword},
timeout=30,
)
res.raise_for_status()
items = res.json()
return items[0]["id"] if items else None
def upload_media(file_path: str):
path = Path(file_path)
with path.open("rb") as fp:
res = requests.post(
f"{base_url}/wp-json/wp/v2/media",
auth=auth,
headers={
"Content-Disposition": f'attachment; filename="{path.name}"'
},
files={"file": (path.name, fp, "image/webp")},
timeout=60,
)
res.raise_for_status()
return res.json()["id"]
category_id = find_first_term_id("categories", "자동화")
tag_id = find_first_term_id("tags", "python")
media_id = upload_media("assets/cover.webp")
update_payload = {
"categories": [category_id] if category_id else [],
"tags": [tag_id] if tag_id else [],
"featured_media": media_id,
"status": "future",
"date_gmt": "2026-04-15T09:00:00",
}
requests.post(
f"{base_url}/wp-json/wp/v2/posts/<post_id>",
auth=auth,
json=update_payload,
timeout=30,
).raise_for_status()
예약 발행은 시간대가 특히 헷갈려요. 한국 표준시(KST, UTC+9) 오후 6시에 올릴 거면 date_gmt는 같은 날짜 09:00:00이거든요. 저는 KST 오후 6시로 잡겠다고 date_gmt에 18:00:00을 넣었다가 새벽 3시에 글이 나간 적 있어요. 여기 한 번 꼬이면 예약이 엉뚱한 시간에 풀려요.
발행 직전에 excerpt나 메타 설명까지 같이 손보려면 AI 메타 설명 만들기: 제목부터 검수까지 워크플로우를 붙이면 좋아요. 자동 발행은 빨라지는데 요약문이 비어 있으면 클릭률에서 손해 보거든요.
구텐베르크가 안 깨지게 블록 마크업으로 보내기
구텐베르크 블록 에디터에서 나중에 손으로 다시 고칠 생각이면, 그냥 HTML만 보내는 방식은 금방 답답해져요. 직접 해봤는데 블록 주석까지 붙여서 보내야 문단이 문단으로, 제목이 제목으로 보여요.
나중에 수동 수정할 건데 HTML 한 덩어리면 답답하지 않겠어요?
코어 문서 기준으로 구텐베르크 블록은 HTML 주석 구분자로 저장돼요. 그래서 일반 HTML만 content에 넣으면 편집기에서 한 블록처럼 뭉칠 수 있고, 블록 마크업으로 감싸면 각 요소가 따로 잡혀요.
| 보내는 방식 | 에디터에서 보이는 모양 | 나중 수정 |
|---|---|---|
| 일반 HTML | 커스텀 HTML 또는 한 덩어리 블록처럼 보일 수 있음 | 불편함 |
| 블록 마크업 | 문단, 제목, 이미지가 각각 블록으로 보임 | 편함 |
<!-- wp:heading {"level":2} -->
<h2>인증 세팅</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>로그인 비밀번호 말고 Application Password를 써야 해요.</p>
<!-- /wp:paragraph -->
from html import escape
def heading_block(text: str, level: int = 2) -> str:
safe = escape(text)
return (
f'<!-- wp:heading {{"level":{level}}} -->'
f"<h{level}>{safe}</h{level}>"
f"<!-- /wp:heading -->"
)
def paragraph_block(text: str) -> str:
safe = escape(text)
return f"<!-- wp:paragraph --><p>{safe}</p><!-- /wp:paragraph -->"
content = "\n\n".join([
heading_block("인증 세팅", 2),
paragraph_block("로그인 비밀번호 말고 Application Password를 써야 해요."),
])
처음에 일반 HTML로만 보냈다가 에디터에서 수정하려니까 전체가 커스텀 HTML 블록 하나로 뭉쳐 있어서, 결국 전부 다시 올렸어요. 블록 주석을 넣은 뒤에는 아래처럼 각 요소가 분리돼요.
블록 주석 규칙은 Markup representation of a block 문서가 제일 깔끔해요. 블록이 정상으로 보여도 구조화데이터는 별개라서, 발행 후 검색 결과까지 챙기려면 AI Schema 만들기: Article·FAQ·Breadcrumb JSON-LD 자동 생성 가이드도 같이 붙여두는 편이 좋아요.
자주 묻는 질문
Q1: 일반 워드프레스 로그인 비밀번호로 REST API 인증하면 왜 401만 나오나요?
A: 원격 REST API 호출은 로그인 비밀번호 대신 Application Password 같은 API용 자격증명을 기대하는 경우가 많아요. 저도 처음에 로그인 비밀번호로 30분 삽질했어요. 셀프호스트 기준으로는 HTTPS에서 Application Password를 먼저 발급하고 다시 테스트하는 게 제일 빨라요.
Q2: Application Password 항목이 사용자 프로필에 안 보여요.
A: wp-admin > 사용자 > 프로필 맨 아래에 “애플리케이션 패스워드” 항목이 있어야 하는데, 안 보이면 HTTPS가 꺼져 있거나 로컬 환경 타입이 제대로 안 잡힌 경우가 많아요. 보안 플러그인이 기본 인증을 가로채고 있으면 이 항목이 숨거나 동작이 꼬이기도 하거든요.
Q3: 자동 포스팅한 글이 구텐베르크 블록 에디터에서 HTML 덩어리 하나로만 보여요.
A: 일반 HTML만 content에 넣으면 그렇게 보일 수 있어요. 직접 써봤는데 나중 편집까지 생각하면 문단과 제목을 블록 마크업으로 감싸서 보내는 쪽이 훨씬 낫더라고요.
Q4: REST API로 발행한 포스트가 SEO에 불리한가요?
A: 워드프레스 자동 포스팅 방식 자체보다 내용 품질과 검수 여부가 더 중요해요. 그래서 보통은 publish보다 draft로 먼저 올리고, 사람이 본문, 메타 설명, 내부 링크를 손본 다음 공개하는 흐름이 안전해요. 메타 설명은 AI 메타 설명 만들기: 제목부터 검수까지 워크플로우처럼 따로 챙기면 돼요.
Q5: 카테고리 ID와 태그 ID는 어디서 보나요?
A: 제일 덜 귀찮은 건 API로 이름 검색하는 방식이에요. GET /wp-json/wp/v2/categories?search=카테고리명처럼 찾으면 되고, 관리자 화면에서 마우스를 올려 상태바 URL을 보는 방식은 급할 때만 쓰면 돼요.
다음 단계
지금은 Application Password부터 만들고 GET /wp-json/wp/v2/users/me가 200 뜨는지만 먼저 보세요. 거기까지 되면 워드프레스 자동 포스팅 파이프라인의 반은 끝난 셈이에요. 그다음 AI 브리프 워크플로우를 붙이면 초안 만들기부터 발행까지 한 줄로 이어져요. 회의록이나 메모를 글감으로 쓰고 싶다면 회의록을 블로그 글 초안으로 빠르게 변환하기도 같이 보세요.
