블로그에 글을 쓸 때마다 웹 에디터를 열지 않고, 내 프로그램이나 사내 시스템에서 바로 발행하고 싶을 때가 있습니다. 예전부터 이런 "원격 발행"에 널리 쓰인 규격이 MetaWeblog API입니다. HTTP 위에서 XML로 함수를 호출하는 XML-RPC 방식이라, XML-RPC 클라이언트만 있으면 언어와 상관없이 쓸 수 있어요. 네이버 블로그도 한때 이 규격으로 글쓰기 API를 제공했고, 티스토리·이글루스·워드프레스 같은 설치형/가입형 블로그 대부분이 같은 방식을 지원했습니다. 관련 자료를 모아 둔 메모입니다.

원문에는 네이버블로그글쓰기API스팩문서(20100420).docx라는 스펙 문서가 첨부돼 있었는데, 파일은 남아 있지 않습니다.

광고

참고 링크 모음

규격 문서는 xmlrpc.com에 있습니다.

오래된 자료라 지금은 열리지 않는 링크도 섞여 있을 수 있습니다.

MetaWeblog 명령 여섯 개

실제로 쓰는 명령은 몇 개 안 됩니다. 인자 순서만 정확히 맞추면 됩니다.

명령 인자 용도
metaWeblog.newPost blogid, username, password, struct, publish 새 글 작성
metaWeblog.editPost postid, username, password, struct, publish 글 수정
metaWeblog.getPost postid, username, password 글 하나 조회
metaWeblog.newMediaObject blogid, username, password, struct 이미지 등 파일 업로드
metaWeblog.getCategories blogid, username, password 카테고리 목록
metaWeblog.getRecentPosts blogid, username, password, numberOfPosts 최근 글 목록

struct에는 title, description(본문 HTML), categories, dateCreated 같은 필드를 담습니다. publish는 true면 바로 공개, false면 임시 저장 성격입니다. 태그는 블로그마다 필드 이름이 달라서, 네이버는 tags, 그 밖의 많은 블로그는 mt_keywords를 씁니다. 그리고 password 자리에 블로그 로그인 비밀번호가 아니라 관리 화면에서 따로 발급받는 API 연결 암호(API 키)를 넣는 서비스가 많으니, 인증 오류가 나면 이것부터 확인하세요. 아래 코드에서 네이버와 이글루스의 비밀번호 칸이 api_key로 돼 있는 게 그 이유입니다.

예제 1: 카테고리 불러와서 발행 폼 만들기

PHP용 XML-RPC 라이브러리(phpxmlrpc의 xmlrpc.inc)를 쓰는 예제입니다. 블로그별 API 주소와 계정 정보를 배열로 잡아 두고, ?q=naver처럼 넘긴 값에 따라 대상 블로그를 고릅니다. 먼저 getCategories로 카테고리 목록을 받아 폼의 선택 상자에 채웁니다. 계정 값은 전부 자리표시이니 본인 블로그 관리 화면에서 확인한 값으로 바꿔야 합니다.

<?
//환경설정
$q_blogApiUrl = array(
"tistory"=>"http://blog.qnibus.com/api",
"naver"=>"https://api.blog.naver.com/xmlrpc",
"egloos"=>"https://rpc.egloos.com/rpc1",
"wordpress"=>"http://qnibus.wordpress.com/xmlrpc.php"
);
$q_blogId = array(
"tistory"=>"123456",
"naver"=>"blogid",
"egloos"=>"blogid",
"wordpress"=>"blogid"
);
$q_userName = array(
"tistory"=>"blog@example.com",
"naver"=>"id",
"egloos"=>"blog@example.com",
"wordpress"=>"id"
);
$q_password = array(
"tistory"=>"password",
"naver"=>"api_key",
"egloos"=>"api_key",
"wordpress"=>"password"
);
require_once 'lib/xmlrpc/lib/xmlrpc.inc';
$GLOBALS['xmlrpc_internalencoding'] = 'UTF-8'; //한글 출력 가능케 함 반드시 넣어야 함 안넣으면 오류발생
$q = $_GET['q'] ? $_GET['q'] : 'naver';
//카테고리 정보 추출 시작
$client = new xmlrpc_client($q_blogApiUrl[$q]);
$message = new xmlrpcmsg("metaWeblog.getCategories",
array(
new xmlrpcval($q_blogId[$q], "string"),
new xmlrpcval($q_userName[$q], "string"),
new xmlrpcval($q_password[$q], "string"),
)
);
$message->request_charset_encoding = 'UTF-8';
$response = $client->send($message);
$value = $response->value(); //카테고리 struct 반환
for($i=0; $i<$value->arraysize(); $i++) $categories[$i] = $value->arraymem($i)->me['struct']['title']->me['string']; //카테고리 타이틀 배열로 생성
natsort($categories); //자연어 정렬
?>
<html>
<head>
<title>원격발행</title>
</head>
<body>
<h1>metaWeblog API</h1>
<ul>
<li><a href="./?q=naver">네이버</a></li>
<li><a href="./?q=tistory">티스토리</a></li>
<li><a href="./?q=egloos">이글루스</a></li>
<li><a href="./?q=wordpress">워드프레스</a></li>
</ul>
<form method="post" enctype="multipart/form-data">
<fieldset>
<legend>newPost Publish</legend>
<input type="hidden" name="q" value="<?php echo $q?>" />
<p>
<label>Title</label>
<input type="text" name="title" size="30" />
</p>
<p>
<label>Category</label>
<select name="categories">
<?php foreach($categories as $val):?>
<option value="<?php echo $val?>"><?php echo $val?></option>
<?php endforeach?>
</select>
</p>
<p>
<label>Description</label>
<textarea name="description" cols="50" rows="5"></textarea>
</p>
<p>
<label>Tags</label>
<input type="text" name="keywords" value="PhotoShake!" />
</p>
<p>
<label>File</label>
<input type="file" name="upload" />
</p>
<p>
<input type="submit" value="submit" />
</p>
</fieldset>
</form>
</body>
</html>

$GLOBALS['xmlrpc_internalencoding'] = 'UTF-8' 줄은 빼먹으면 한글이 깨지거나 오류가 나니 꼭 넣어야 합니다. phpxmlrpc의 내부 인코딩 기본값이 UTF-8이 아니기 때문이에요. 응답에서 카테고리 제목을 꺼내는 ->me['struct']['title']->me['string'] 부분은 라이브러리 내부 구조에 직접 접근하는 방식이라, 라이브러리 버전이 바뀌면 이 경로가 달라질 수 있습니다.

예제 2: 이미지 업로드 후 글 발행

폼이 제출되면 첨부 파일이 있는 경우 newMediaObject로 먼저 업로드해서 이미지 URL을 받고, 그 URL로 <img> 태그를 만들어 본문 앞에 붙인 뒤 newPost로 발행합니다. 환경설정 부분은 예제 1과 같은데, 실제로 쓸 때는 따로 설정 파일로 빼서 include 하는 게 낫습니다.

<?
//환경설정 (공동으로 사용하는 파일이니 xmlrpc.inc나 config.php로 파일 만들어 include 시켜서 사용하세요 한눈에 보여드리려고 풀어헤쳤습니다.
//반드시 환경설정부는 자신의 블로그 관리에서 확인하신후 정보를 변경하셔야 정상 출력합니다.
$q_blogApiUrl = array(
"tistory"=>"http://blog.qnibus.com/api",
"naver"=>"https://api.blog.naver.com/xmlrpc",
"egloos"=>"https://rpc.egloos.com/rpc1",
"wordpress"=>"http://qnibus.wordpress.com/xmlrpc.php"
);
$q_blogId = array(
"tistory"=>"123456",
"naver"=>"blogid",
"egloos"=>"blogid",
"wordpress"=>"blogid"
);
$q_userName = array(
"tistory"=>"blog@example.com",
"naver"=>"id",
"egloos"=>"blog@example.com",
"wordpress"=>"id"
);
$q_password = array(
"tistory"=>"password",
"naver"=>"api_key",
"egloos"=>"api_key",
"wordpress"=>"password"
);
require_once 'xmlrpc/lib/xmlrpc.inc';
$GLOBALS['xmlrpc_internalencoding'] = 'UTF-8';
// 파일 업로드
if (!empty($_FILES['upload']['name']))
{
$tempFile = $_FILES['upload']['tmp_name'];
$fileName = $_FILES['upload']['name'];
$fileType = $_FILES['upload']['type'];
// 파일 바이너리 데이터 가져오기
$fp=fopen($tempFile, "rb");
if (!$fp) return null; // file open failure !!
while( !feof($fp))
{
$filedescription .= fread( $fp, 1024); // 1024 is the server compatible buffer size
flush();
@ob_flush();
}
fclose($fp);
$client = new xmlrpc_client($q_blogApiUrl[$q]);
$newMediaObject = new xmlrpcmsg("metaWeblog.newMediaObject",
array(
new xmlrpcval($q_blogId[$q], "string"),
new xmlrpcval($q_userName[$q], "string"),
new xmlrpcval($q_password[$q], "string"),
new xmlrpcval(
array(
'name'  => new xmlrpcval($fileName, "string"),
'type'  => new xmlrpcval($fileType, "string"),
'bits' => new xmlrpcval($filedescription, "base64"), //파일 바이너리 값
), "struct")
)
);
$newMediaObject->request_charset_encoding = 'UTF-8';
$tempResponse = $client->send($newMediaObject);
$uploadFileName = $tempResponse->value()->me['struct']['url']->me['string'];
$uploadFileSize = getimagesize($tempFile);
$description = '<img src="'.$uploadFileName.'" '.$uploadFileSize[3].' /><br />'.$description.'<p>'.$_SERVER['REMOTE_ADDR'].'</p>';
}
$content = array(
'title'   => new xmlrpcval($title, "string"),
'description'   => new xmlrpcval($description, "string"),
'dateCreated'  => new xmlrpcval(date("Ymd")."T".date("H:i:s"), "dateTime.iso8601"),
'categories' => new XMLRPCval(array(new XMLRPCval($categories,"string")), "array"),
($q=='naver') ? 'tags' : 'mt_keywords' => new xmlrpcval($keywords),
);
$client = new xmlrpc_client($q_blogApiUrl[$q]);
$message = new xmlrpcmsg("metaWeblog.newPost",
array(
new xmlrpcval($q_blogId[$q], "string"),
new xmlrpcval($q_userName[$q], "string"),
new xmlrpcval($q_password[$q], "string"),
new xmlrpcval($content, "struct"),
new xmlrpcval(true, "boolean")
)
);
$message->request_charset_encoding = 'UTF-8';
$response = $client->send($message);
$value = $response->value();
$postno = $value->scalarval(); // 발행한 결과로 블로그의 포스트 넘버를 반환합니다.
//디버그 확인
echo '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
if(!$response->faultCode()) echo "성공적으로 발행되었습니다.";
else echo htmlspecialchars($response->faultString()); //에러 메시지 출력

주의할 점이 있습니다. 이 스크립트는 $q, $title, $description, $categories, $keywords를 어디에서도 $_POST로 받지 않고 바로 씁니다. 폼 값이 같은 이름의 전역변수로 자동 등록되던 register_globals 시절 코드라서 그렇고, 이 설정은 PHP 5.4에서 없어졌습니다. 지금 PHP에서 돌리려면 맨 위에서 $q = $_POST['q'];, $title = $_POST['title'];처럼 하나씩 받아 줘야 합니다. 본문 끝에 글쓴이 IP(REMOTE_ADDR)를 붙이는 줄도 그대로 쓰면 블로그에 IP가 공개되니 빼는 게 좋습니다. 파일을 1024바이트씩 읽는 루프는 file_get_contents($tempFile) 한 줄로 바꿔도 결과가 같습니다.

dateCreated는 20191001T14:19:00 같은 ISO 8601 형식이어야 합니다. 형식이 틀리면 서버가 날짜를 무시하거나 오류를 냅니다.

예제 3: 네이버 블로그 전용 함수 두 개

네이버만 대상으로 할 때 쓰던 더 짧은 버전입니다. 이미지 업로드(upload_image)와 글 발행(new_post)을 함수로 나눴고, return_type = 'phpvals'로 설정해서 응답을 PHP 배열·값으로 바로 받기 때문에 예제 1, 2처럼 ->me[...]로 파고들 필요가 없습니다. 네이버는 blogid 자리에 아이디를 그대로 넣기 때문에 첫 번째와 두 번째 인자가 둘 다 $blog_user입니다.

include("xmlrpc.inc");
$GLOBALS['xmlrpc_internalencoding'] = 'UTF-8';

$api_url = "https://api.blog.naver.com/xmlrpc";
$blog_user = "네이버ID";
$blog_passwd = "API 연결암호";

function upload_image($fpath) {
 global $api_url, $blog_user, $blog_passwd;

 $imgbit = file_get_contents($fpath, FILE_BINARY);
 $img = new xmlrpcval(
  array (
   'bits' => new xmlrpcval($imgbit, 'base64'),
   'type' => new xmlrpcval('image/jpeg', 'string'),
   'name' => new xmlrpcval(basename($fpath), 'string')
  ), 'struct');
 $c = new xmlrpc_client($api_url);
//  $c->debug = true; // Uncomment this line for debugging info
 $c->setSSLVerifyPeer(false);
 $x = new xmlrpcmsg("metaWeblog.newMediaObject");
 $x->addParam(new xmlrpcval($blog_user, 'string'));
 $x->addParam(new xmlrpcval($blog_user, 'string'));
 $x->addParam(new xmlrpcval($blog_passwd, 'string'));
 $x->addParam($img);

 $c->return_type = 'phpvals';
 $r =$c->send($x, 3, 'https');
 if ($r->errno=="0") {
     return $r->val['url'];
 } else {
     echo "<br>There was an error<pre>";
     print_r($r);
     echo "</pre>";
     return null;
 }

} // upload_image

function new_post($subj, $content) {
 global $api_url, $blog_user, $blog_passwd;

 $c = new xmlrpc_client($api_url);
//  $c->debug = true; // Uncomment this line for debugging info
 $c->setSSLVerifyPeer(false);

 $post = new xmlrpcval(
  array (
   'title' => new xmlrpcval($subj, 'string'),
   'description' => new xmlrpcval($content, 'string')
  ), 'struct');

 $x = new xmlrpcmsg("metaWeblog.newPost");
 $x->addParam(new xmlrpcval($blog_user, 'string'));
 $x->addParam(new xmlrpcval($blog_user, 'string'));
 $x->addParam(new xmlrpcval($blog_passwd, 'string'));
 $x->addParam($post);
 $x->addParam(new xmlrpcval(true, 'boolean'));
 $x->request_charset_encoding = "UTF-8";

 $c->return_type = 'phpvals';
 $r =$c->send($x, 3, 'https');
 if ($r->errno=="0") {
  $post_url = "http://blog.naver.com/$blog_user/".$r->val;
     return $post_url;
 } else {
     echo "<br>There was an error<pre>";
     print_r($r);
     echo "</pre>";
     return null;
 }
} // new_post

쓰는 순서는 upload_image('사진경로.jpg')로 이미지 URL을 받고, 그 URL로 <img> 태그를 넣은 본문을 만들어 new_post('제목', $본문)을 부르는 식입니다. 성공하면 발행된 글의 번호가 돌아오니 blog.naver.com/아이디/글번호 주소를 바로 만들 수 있습니다. send($x, 3, 'https')의 3은 초 단위 타임아웃인데, 이미지가 크면 3초 안에 업로드가 안 끝나 실패하니 넉넉히 늘려 두는 편이 낫습니다. 이미지 MIME 타입이 image/jpeg로 고정돼 있어서 PNG를 올릴 땐 이 값도 바꿔야 합니다.

setSSLVerifyPeer(false)는 인증서 검증을 끄는 설정입니다. 예전 서버의 CA 인증서 묶음이 오래돼 HTTPS 연결이 실패할 때 흔히 쓰던 우회책인데, 계정 암호가 오가는 요청이니 가능하면 서버의 CA 인증서를 갱신하고 검증을 켜 두는 쪽을 권합니다.

지금 시점에서 볼 때

이 자료는 꽤 오래전에 모은 것이라 그대로 동작한다고 기대하면 안 됩니다. 이글루스는 서비스 자체가 종료됐고, 다른 블로그들도 XML-RPC 글쓰기 지원을 줄이거나 다른 방식의 API로 옮겨 간 경우가 많습니다. 워드프레스는 지금도 xmlrpc.php를 기본으로 두고 있지만, 보안상 호스팅이나 플러그인에서 막아 둔 경우가 흔하고 새로 만들 거라면 REST API 쪽이 일반적입니다. 실제로 붙이기 전에 대상 블로그가 현재 어떤 방식의 원격 글쓰기를 지원하는지부터 확인하세요. 그래도 MetaWeblog의 명령 구조 자체는 단순해서, 지원하는 서비스를 만나면 위 예제를 거의 그대로 응용할 수 있습니다.

참고: 사이트히스 - MetaWeblog API XML-RPC 자료