대시보드가 있고, MCP 서버도 있는데 CLI를 또 만들었다. 과잉이라고 생각할 수 있다. 나도 처음엔 그렇게 생각했는데, 만들고 나니까 이게 없던 시절로 돌아가기 싫어졌다.
왜 CLI인가
개발자의 작업 흐름 대부분은 터미널 안에서 일어난다. git, npm, docker — 전부 터미널이다. 블로그에 글을 올리려고 브라우저를 열고 대시보드에 로그인하고 에디터를 찾아가는 과정이 생각보다 많은 컨텍스트 스위칭을 유발한다. "한 줄이면 끝나는 일"을 위해 브라우저 탭을 여는 건 개발자한테 일종의 마찰이다.
MCP 서버는 AI 에이전트용이다. Claude Code나 Cursor 같은 도구가 블로그를 조작할 수 있게 해주는 프로토콜인데, 사람이 직접 쓰기엔 적합하지 않다. 사람에게는 사람의 인터페이스가 필요하다. postlark posts create --title "제목" --file post.md 같은 명령어가 그 인터페이스다.
30개 커맨드를 하루 만에
@postlark/cli의 첫 커밋에는 30개가 넘는 커맨드가 한꺼번에 들어갔다. login, blogs, posts, deploy, search, analytics, domains, tokens, keys, account, packs. 이걸 어떻게 하루 만에 했냐면, 비밀은 @postlark/shared에 있다.
MCP 서버를 먼저 만들면서 API 클라이언트 코드가 이미 존재했다. 문제는 그게 MCP 서버 패키지 안에 묶여 있었다는 거다. 이걸 @postlark/shared라는 별도 패키지로 분리하면서 CLI와 MCP 서버가 동일한 API 호출 로직을 공유하게 됐다. CLI 쪽에서는 커맨드 파싱과 출력 포맷팅만 신경 쓰면 됐다.
commander.js를 골랐다. 이유는 단순하다 — Node.js CLI 프레임워크 중에서 가장 오래됐고, 가장 많이 쓰이고, 문서가 가장 좋다. yargs도 후보였는데 서브커맨드 구조가 commander 쪽이 더 깔끔했다. 의존성은 딱 두 개, commander와 @postlark/shared뿐이다. 가볍게 가고 싶었다.
deploy — 이게 핵심이다
솔직히 CLI를 만든 진짜 이유는 deploy 커맨드 때문이다.
postlark deploy ./posts
로컬 디렉토리에 마크다운 파일들을 쭉 넣어두고 이 한 줄을 실행하면, 서버에 없는 글은 새로 만들고 이미 있는 글은 업데이트한다. --dry-run으로 미리 확인할 수도 있고, --delete-missing을 붙이면 로컬에 없는 서버 글을 삭제하기도 한다.
각 마크다운 파일 상단에 frontmatter를 넣으면 된다:
---
title: 오늘의 글
slug: todays-post
tags: [typescript, tutorial]
status: published
---
slug를 안 쓰면 파일명에서 자동으로 뽑아낸다. my-post.md는 my-post가 된다. 이게 왜 중요하냐면, Git 저장소 하나에 마크다운 파일들을 관리하면서 CI/CD로 자동 배포하는 흐름이 가능해지기 때문이다. GitHub Actions에서 postlark deploy ./posts를 돌리면 끝이다. push할 때마다 블로그가 업데이트된다.
로그인의 함정
CLI에서 인증은 늘 고민거리다. 가장 쉬운 방법은 API 키를 직접 입력받는 건데, 사용자가 대시보드에서 키를 복사해 와야 한다. 이 과정이 생각보다 귀찮다.
그래서 Device Auth 플로우를 구현했다. postlark login을 치면 브라우저가 뜨고, 화면에 확인 코드가 나온다. 브라우저에서 로그인하고 코드를 확인하면 CLI가 자동으로 인증된다. GitHub CLI나 Vercel CLI에서 쓰는 그 방식이다.
근데 여기서 삽질이 있었다. 처음 구현했을 때 Device Auth 엔드포인트를 /v1/auth/device 경로에 넣었는데, /v1 아래 경로는 전부 인증 미들웨어를 타게 돼 있었다. 인증하려고 호출하는 엔드포인트가 인증을 요구하는 상황. 이걸 깨닫는 데 좀 걸렸다. 결국 Device Auth 라우트만 /v1 바깥으로 빼서 해결했다.
또 하나, 헤드리스 환경 문제가 있었다. SSH로 서버에 접속해서 CLI를 쓰는 경우 브라우저가 안 열린다. 처음엔 "Opening browser..."만 출력하고 끝이었는데, 브라우저가 안 열리는 환경에서는 사용자가 URL을 직접 복사해서 열어야 한다는 안내가 필요했다. 이건 피드백을 받고 고쳤다.
API 키 직접 입력도 여전히 지원한다. postlark login pk_live_xxxxx로 바로 넣을 수 있다. CI 환경에서는 이 방식이 더 편하다.
--json 플래그
모든 커맨드에 --json 옵션이 있다. 사람이 읽을 때는 포맷팅된 텍스트가 나오고, 스크립트에서 쓸 때는 JSON으로 뽑을 수 있다. 이게 있어야 다른 도구와 파이프라인으로 연결할 수 있다.
postlark posts --json | jq '.[].slug'
이런 식으로 쓸 수 있다는 건 CLI가 단순한 편의 도구가 아니라 자동화 파이프라인의 부품이 된다는 뜻이다.
남은 이야기
@postlark/cli는 현재 v0.1.6이다. npm install -g @postlark/cli로 설치하거나 npx @postlark/cli로 바로 쓸 수 있다. 아직 0.x 버전인 건 인터페이스가 완전히 굳지 않았기 때문인데, 핵심 커맨드들은 안정적이다.
다음 편에서는 Markdown 확장 이야기를 할 예정이다. GFM 테이블, callout 박스, Mermaid 다이어그램, KaTeX 수식 — 이것들을 에디터, 블로그, OG 이미지까지 일관되게 렌더링하는 게 생각보다 까다로운 문제였다.