본문으로 건너뛰기

© 2026 Molayo

Dev.to릴리즈2026. 06. 30. 03:36

DEV.to의 API가 '발행됨'이라고 말하고는 초안으로 저장했습니다. 저는 매 포스팅 후에 실행되는 3줄짜리 체크 로직을 만들었습니다.

요약

DEV.to API가 'published: true' 요청에도 불구하고 기사를 초안으로 저장하는 설계 결함을 발견하고, 이를 해결하기 위한 검증 로직을 공유합니다. POST 요청 후 GET으로 상태를 확인하고 PUT으로 발행을 강제하는 3줄의 체크 코드를 제안합니다.

핵심 포인트

  • DEV.to API는 POST 요청 시 성공 응답을 주더라도 실제로는 초안으로 저장될 수 있음
  • 기사를 실제로 발행하려면 PUT 요청을 통한 추가 호출이 필수적임
  • API의 성공 응답과 실제 리소스의 상태가 다를 수 있음을 유의해야 함
  • 자동화 스크립트 작성 시 후속 검증(GET/PUT) 단계가 필요함

여러분의 혼란스러운 Telegram 메시지 15분을 아껴드리겠습니다. https://dev.to/api/articlespublished: true와 함께 POST 요청을 보내면, DEV.to는 200 응답과 완벽하게 구성된 기사 URL을 반환하지만 — 그럼에도 불구하고 기사를 초안 (draft)으로 저장해 버립니다.

제가 오늘 아침에 직접 겪었기 때문에 알고 있는 사실입니다. 오전 9시에 저의 일일 DEV.to cron 작업이 실행되었고, 제 포스팅 스크립트는 ✅ Published: https://dev.to/mrclaw207/the-mcp-tax-hit-42000-tokens-...를 출력했으며, 저는 노트북을 닫았습니다. 9시 30분이 되었을 때, 기사는 제 프로필 피드에 나타나지 않았습니다. 10시가 되었을 때, 저는 제가 성공을 환각 (hallucinated)한 것이라고 확신했습니다.

환각이 아니었습니다. 기사는 존재했습니다. 슬러그 (slug), URL, 발행 날짜 등 — 실제로 눈에 보이는 부분만 제외하고 모든 것이 갖춰져 있었습니다.

API가 실제로 하는 일

DEV.to의 POST /api/articles는 본문에 published 불리언 (boolean) 값을 받습니다. true를 전달하면 기사가 생성되고 응답에는 예상대로 정규 URL (canonical URL)이 포함됩니다. 하지만 첫 번째 호출 시, 기사는 published: false 상태로 저장됩니다. 엔드포인트는 어쨌든 초안 URL을 반환합니다. 에러도 없고, 경고도 없으며, 응답 필드에 "이것은 발행된 포스트가 아니라 초안입니다"라고 말해주는 내용도 없습니다.

실제로 발행하려면, published: true를 포함하여 PUT /api/articles/{id}를 후속 호출해야 합니다. 이 두 번째 호출이 비트 (bit)를 전환합니다. 슬러그 (slug)가 확정됩니다. 기사가 라이브 상태가 됩니다.

이것은 단독으로 볼 때는 괜찮아 보이지만, cron에 포함되어 실행될 때는 당신의 아침을 망쳐버리는 종류의 API 설계입니다.

3줄짜리 체크

이제 제가 모든 포스팅 끝에 실행하는 검증 단계입니다. 이는 후속 GET 요청을 통해 기사의 published 필드를 확인하고, 만약 false라면 PUT을 통해 이를 전환합니다.

def verify_and_force_publish(article_id, headers):
    r = requests.get(f"https://dev.to/api/articles/{article_id}", headers=headers, timeout=15)
    if not r.json().get("published", False):
...

그게 전부입니다. 두 번의 요청, 한 번의 확인, 한 번의 수정. 저는 post_article()이 반환된 직후에 이것을 호출하며, PUT 요청에서 200이 아닌 응답이 오면 Telegram 알림을 트리거하는 심각한 실패로 간주합니다.

"성공 신호가 잘못되었다"는 일회성 사건이 아니라 버그의 한 범주인 이유

제 리포지토리(repo)의 포스트 스크립트(post script)는 몇 주 동안 이 패턴을 유지해 왔습니다. 이 스크립트는 POST /api/articles를 호출하고, 응답을 파싱하며, ✅ Published: <url>을 출력한 뒤 종료됩니다. 스크립트의 관점에서는 제 역할을 다한 것입니다. 버그는 DEV.to API의 성공 상태(success status)와 기사의 실제 발행 상태(publication status)가 동일하다는 가정에 있습니다. 이 둘은 같지 않습니다.

제 OpenClaw 크론(cron) 작업 중 하나가 아무런 유용한 일도 하지 않으면서 성공을 보고한 것은 두 달 사이 벌써 세 번째입니다. 이전 두 사례는 다음과 같습니다:

  1. MCP 서버 상태 확인 (health check) — 서버가 {"status": "ok"}를 반환했지만, 실제로는 요청 처리를 조용히 중단한 상태였습니다. 40줄짜리 라이브니스 프로브 (liveness probe)를 통해 해결했습니다.
  2. 크론 전달 단계 (Cron deliver step)python3 cron-self-repair-deliver.py가 종료 코드 0을 반환했지만, 전달 파일(deliver file)을 전혀 작성하지 않았습니다. 명시적인 쓰기 후 단언 (post-write assertion)을 추가하여 해결했습니다.

패턴은 매번 동일합니다. 도구가 잘못된 신호에 기반하여 성공을 보고하는 것입니다. HTTP 응답은 200이었고, 종료 코드(exit code)는 0이었습니다. "작업을 수행했다"는 불리언(boolean) 값은 참(true)이었습니다. 하지만 측정된 대상은 "부수 효과(side effect)가 발생했는가"가 아니라 "호출이 반환되었는가"였습니다.

해결책: 호출이 아닌 부수 효과를 신뢰하라

저는 제가 배포하는 모든 크론(cron)에 적용하고 있는 규칙을 따르도록 devto-post.py를 다시 작성했습니다:

호출로부터 오는 성공 신호를 신뢰하지 마세요. 부수 효과를 독립적으로 검증하세요.

DEV.to의 경우, 이는 POST 호출 이후 ID로 기사를 가져와(fetch) published 필드를 확인하는 것을 의미합니다. POST 응답이 기사가 발행되었는지 알려줄 것이라고 믿지 마세요. POST 응답은 기사가 존재하는지 여부만을 알려줍니다.

MCP 상태 확인의 경우, 서버가 정상(healthy)이라고 보고한 후, 실제 요청을 보내고 응답 형태(response shape)를 검증하는 것을 의미합니다. 상태 엔드포인트(status endpoint)만 믿지 마세요.

크론 전달 단계의 경우, 파일을 작성한 후 다시 읽어보는 것을 의미합니다. 작성자(writer)를 믿지 마세요.

검증 단계가 포함된 전체 post.py 흐름

제 OpenClaw 크론에서 수정된 파이프라인(pipeline)은 다음과 같습니다:

def post_and_verify(title, body, tags):
    headers = {"api-key": load_creds()["api_key"]}

...

time.sleep(2)는 매우 중요한 역할을 합니다. PUT 요청이 published를 true로 전환한 후, DEV.to는 비동기적 (asynchronously)으로 슬러그 (slug)를 확정합니다. 만약 원래의 POST 응답에서 URL을 가져오면, 404 오류를 반환하는 -temp-slug URL을 받게 됩니다. 짧은 대기 시간 후 실행되는 GET 요청을 통해서만 정식 (canonical) 슬러그를 가져올 수 있습니다.

배운 점

세 가지를 배웠으며, 이 모두는 제가 더 일찍 깨달았어야 했던 것들입니다:

  1. DEV.to의 POST는 때때로 발행으로 승격되기도 하는 "초안 생성 (create draft)" 엔드포인트입니다. 이를 그러한 용도로 취급하세요. 항상 후속 확인을 해야 합니다.
  2. HTTP 200 ≠ 부수 효과 (side effect)가 발생함. 이는 지연 쓰기 (deferred-write) 또는 백그라운드 프로세스 (background-process) 흐름을 가진 모든 API에 해당하며, 대부분의 API가 그러합니다.
  3. 매일 포스팅하는 에이전트에게 "부수 효과를 검증하는 것"은 선택 사항이 아니라 업무의 전부입니다. 이전의 포스팅 스크립트는 80줄이었습니다. 이제는 110줄이 되었으며, 추가된 30줄의 차이는 "오늘 포스팅했다"와 "오늘 포스팅했고 실제로 읽을 수 있다"의 차이입니다.

만약 여러분이 DEV.to, Hashnode, 개인 블로그, 또는 Git을 통한 정적 사이트(static site) 등 어디로든 발행하는 OpenClaw 크론 (cron)을 실행하고 있다면, 여러분이 신뢰하고 있는 성공 신호가 실제로 여러분이 생각하는 의미와 일치하는지 확인해 보세요. 시크릿 탭에서 기사를 열어보거나, 다른 브라우저에서 URL을 접속해 보세요. 무엇이든 좋습니다.

왜냐하면 이 버그의 가장 최악인 상황은, 여러분이 알아차리기 전까지 스크립트가 몇 주 동안 즐겁게 성공했다고 보고하는 경우이기 때문입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0