이 뉴스레터가 여기 도착하기까지
바이브코딩 뉴스레터 Ep.4 — 자동화의 진짜 비용은 잊지 않는 인프라
Ep.1 을 발행하던 날, 나는 두 시간 동안 마크다운을 Substack 에 어떻게 붙여넣을지 시행착오 했다.
Ep.4 가 발행될 때 나는 한 줄 명령어를 친다.
그 사이가 11일이다. 이번 에피소드는 그 11일 동안 일어난 일 이다.
이 뉴스레터는 1인 인디 개발자가 Claude Code 두 대(Mac·WSL)와 함께 의사결정을 내리는 흐름의 기록이다. Ep.1 은 만들기(3시간에 봇 만들기), Ep.2 는 죽이기(24시간 만에 그 봇 드롭), Ep.3 는 빼기(70만원 인프라를 0원으로) 였다. 이번 Ep.4 는 잇기 — 마크다운 한 편이 한 줄 명령어로 Substack 에 발행되기까지의 마찰 제거 기록이다.
마찰을 어떻게 다룰 것인가, 가 시리즈 동사들의 공통 주제다. 더하지 않기·죽이기·빼기·잇기 — 모두 마찰 관련 결정이다. 이번 마찰은 수작업으로 매번 똑같은 동작을 반복하는 손 이고, 도구는 macOS NSPasteboard 와 Playwright MCP 다. 그리고 글 끝에 — 이미 만들어둔 자동화가 한 번 끊어지고 다시 이어진 작은 회귀 한 컷이 같이 들어간다.
1막 — 손이 가는 곳
Ep.1 을 발행한 직후, 2026년 4월 20일 저녁이었다.
마크다운으로 글을 다 쓰고 Substack 에디터를 열어 그대로 붙여넣었다. 표가 평문이 됐다. 굵은 글씨가 사라졌다. 헤더가 살아나지 않았다. 마크다운 기호 # 와 ** 가 글자 그대로 남았다. 첫 시도가 끝나는 데 5초 였고, 그걸 어떻게 풀어야 하는지 알아내는 데 90분이 걸렸다.
90분 시행착오 끝에 나온 결론은 단순했다. Substack 에디터는 마크다운을 받지 않는다. 마크다운 기호를 자동으로 해석하는 입력 모드가 거기엔 없다. 받는 건 리치포맷 — 풀어서 말하면 RTF 또는 HTML 같은 포맷팅이 이미 적용된 콘텐츠다. 마크다운 기호가 평문 글자로 들어가는 게 아니라 이미 굵은 글씨가 굵은 글씨인 상태로 들어가야 한다.
그 시점에 결정 한 줄이 떠올랐다.
“다음 회차부터는 자동화한다.”
이 한 줄이 11일 후 한 줄 명령어가 됐다. 사이에 일어난 일이 2~5막이다.
그런데 이 결정 자체가 진짜로 자동화로 이어졌는지는 이번 에피소드의 잔칫상 위에 한 번 더 올려둘 것이다. 결정과 실행 사이에는 회귀가 끼어든다. 그 이야기는 5막에서.
2막 — 표준 길은 모두 막혀있다
자동화하기로 결정했으니 표준 길을 먼저 봤다. 인디 개발자의 직감으로는 “이런 건 누군가 이미 풀어놨겠지” 하는 기대가 있다. 풀어놨을 수도 있는 길 4개를 차례로 두드렸다.
① Substack import 기능. Substack 자체에 다른 블로그에서 글 가져오기 기능이 있다. RSS feed 만들어서 import 시키면 어떨까? 시도해 보니 RSS 피드의 콘텐츠가 다시 마크다운으로 가공되는 형태라 같은 문제가 그대로 돌아왔다. RSS 가 마크다운을 받고 있었다.
② WordPress migration 경로. Substack 에 워드프레스 글로 가져오기 옵션이 있다는 걸 알아냈다. 즉 무료 호스팅 워드프레스를 하나 셋업하고, 거기 마크다운 → HTML 발행 파이프라인을 따로 만들고, 그걸 Substack 으로 export 하는 흐름. 비용이 추가로 한 단계 더 늘어난다. 자동화하려고 자동화 인프라를 두 개 짓는 모양 이 됐다.
③ Markdown-to-Rich JavaScript snippet. 브라우저 개발자 도구에 마크다운을 리치포맷으로 변환해서 클립보드에 넣는 코드를 한 번씩 돌리는 방식. 매번 손이 가야 하므로 자동화가 아니다. 90분 시행착오를 90초 시행착오로 줄여줄 뿐.
④ Substack 공개 API. 마지막 보루였다. Substack 은 글 작성용 공개 API 를 제공하지 않는다. 정책상 막혀있다.
표준 길은 다 막혀있었다. 비표준 길이 필요했다.
3막 — NSPasteboard 클립보드 트릭
비표준 길의 첫 발견은 macOS 클립보드의 동작 방식이었다.
macOS NSPasteboard 는 같은 클립보드 한 칸에 여러 representation 을 동시에 담는다. 같은 텍스트를 HTML 로도 가지고 있고, 평문으로도 가지고 있고, RTF 로도 가지고 있을 수 있다. 받는 앱이 자기가 선호하는 포맷으로 꺼내 간다. 즉 한쪽 끝에서 HTML 을 넣어두면 다른 쪽 끝의 Substack 에디터가 HTML 을 꺼내서 리치포맷으로 인식한다.
이 한 문장이 모든 걸 풀었다.
osascript <<'OSAEOF'
use framework "Foundation"
use framework "AppKit"
set pb to current application's NSPasteboard's generalPasteboard()
pb's clearContents()
set htmlNSString to (current application's NSString's stringWithContentsOfFile:"/tmp/body.html" encoding:4 |error|:(missing value))
set plainNSString to (current application's NSString's stringWithContentsOfFile:"/tmp/body.md" encoding:4 |error|:(missing value))
pb's setString:htmlNSString forType:"public.html"
pb's setString:plainNSString forType:"public.utf8-plain-text"
OSAEOF
이 다섯 줄짜리 osascript 가 핵심이다. AppKit framework 을 끌어와 NSPasteboard 의 일반 클립보드를 잡고, 한 번에 비우고, HTML 파일과 마크다운 파일을 각각 UTF-8 로 읽어서 두 개의 representation 으로 같이 넣는다. encoding:4 는 NSUTF8StringEncoding 을 가리키는 상수 — 한국어가 깨지지 않게 해주는 한 자리 수다. 이걸 빼먹으면 한글이 망가진다 (-1700 에러).
이렇게 클립보드에 두 representation 이 같이 들어가 있을 때, Substack 의 tiptap 에디터에 cmd+V 를 누르면 에디터가 HTML 을 꺼내서 리치포맷으로 해석 한다. 표가 표로, 굵은 글씨가 굵은 글씨로, 헤더가 헤더로 그대로 들어간다. 90분짜리 시행착오가 0초로 줄어드는 순간이었다.
마크다운을 HTML 로 변환하는 단계는 Python markdown 라이브러리 한 줄로 처리한다. fenced_code 와 tables 와 sane_lists 확장을 켜면 코드 블록·표·리스트가 모두 제대로 변환된다. 단, nl2br 확장은 절대 켜지 말 것 — 모든 줄바꿈을 강제로 <br /> 로 바꿔서 빈 줄로 블록을 나누는 마크다운의 기본 규칙을 깨뜨린다. 이걸 한 번 켰다가 30분을 잃었다.
함정이 하나 더 있었다. Substack 의 tiptap 에디터는 표(table) 노드를 지원하지 않는다. HTML 에 <table> 태그가 있어도 paste 하는 순간 paragraph 한 덩어리로 강등된다. 우회는 마크다운 단계에서 처리했다 — 표를 리스트 형태로 미리 변환한다.
# 마크다운 표 한 줄을 - **헤더1**: 값1 / **헤더2**: 값2 형식으로
def transform(md):
# 표 헤더와 구분선을 감지하면
# 본문 행을 "- **컬럼명**: 값" 으로 풀어서 리스트로
...
작은 비용이 큰 마찰을 푼다. Ep.3 의 “빼기” 와 같은 결이다. 표를 살리려고 별도 렌더링 파이프라인을 짓는 대신, 표를 리스트로 미리 풀어버리고 Substack 의 동작 범위 안으로 들어간다.
4막 — Playwright MCP 가 발행 버튼까지 누른다
3막의 클립보드 트릭이 풀린 뒤에도 한 단계가 더 남아있었다. Substack 페이지를 열고, 로그인하고, 새 글 페이지로 이동하고, 제목을 입력하고, 본문 영역을 클릭하고, 붙여넣고, 발행 버튼까지 누르는 흐름이 사람 손에 남아있었다. 이걸 마지막으로 자동화한 게 Playwright MCP 다.
Playwright MCP 는 macOS 의 Claude Code 안에서 직접 브라우저를 컨트롤할 수 있게 해주는 도구다. Anthropic Claude 가 직접 브라우저를 열고 클릭하고 타이핑한다. 이걸 Substack 발행 흐름에 적용했다.
로그인은 한 번만 풀면 됐다. Substack 은 매직 링크 방식 — 이메일에 발송된 링크를 누르면 그 브라우저 세션이 인증된다. Playwright MCP 의 자체 Chromium 인스턴스에 한 번 매직 링크를 받아서 들어가면, 약 60일 동안 세션 쿠키가 살아있다. 그 안에서는 별도 인증 없이 새 글 작성 페이지에 바로 들어갈 수 있다.
전체 파이프라인을 풀어쓰면 이런 모양이다.
- 마크다운 작성 (
ep4-substack.md) - Python
markdown라이브러리로 HTML 변환 (/tmp/body.html) - osascript 가 NSPasteboard 에 HTML + 평문 두 representation 으로 set
- Playwright MCP 가
https://daejongkang.substack.com/publish/post?type=newsletter진입 - 제목 textarea 에 native value setter + input/change 이벤트 디스패치 (또는 직접 type)
- 부제 textarea 도 같은 방식
- 본문 에디터 클릭 →
ControlOrMeta+a→Delete(혹시 남아있는 잔여물 비우기) →ControlOrMeta+V→ 클립보드의 HTML 이 tiptap 에 리치포맷으로 들어감 - 발행 버튼 (
button[data-testid="publish-button"]) → 모달의 “Send to everyone now” 클릭 - 공개 URL
https://daejongkang.substack.com/p/<slug>진입해서 article DOM 의 h2/strong/blockquote/ul/li/pre/code 카운트로 검증
소요 시간 약 90초. 그중 대부분은 Playwright 의 페이지 로딩 시간이다. 즉 코드가 일하는 시간이 아니라 네트워크가 일하는 시간이 자동화 사이클의 병목이다. 손으로 하면 5~10분, 자동화하면 90초. 사람 손이 클립보드와 발행 버튼 두 군데에서 사라졌다.
5막 — 13일치 백필, 그리고 회귀 한 번
자동화가 작동하는지 검증하려면 한 편으로는 부족했다. Ep.1, Ep.2, 그리고 그 사이 worklog 11일치를 모두 같은 파이프라인으로 백필했다. 총 13편. 13/13 성공. 같은 파이프라인이 13번 연속으로 같은 결과를 냈다. 이때부터 “한 줄 명령어” 의 한 줄이 진짜로 한 줄이 됐다.
그런데 이번 에피소드의 진짜 코다는 5막의 두 번째 단락이다.
자동화는 만든 다음날 한 번 끊어졌다.
2026년 4월 26일 아침 8시 38분. 강대종이 한 줄 메시지를 보냈다.
“쏴줘 그래도 알고는 있어야지 맥도”
WSL 쪽 Claude 가 답했다. ”📨 MyClaude(맥) 챗에 복붙해서 보내주세요 ↓”
여기서 무슨 일이 일어났는지 풀어보자면. 그 직전 하루 종일(4/25), 두 대의 Claude (Mac·WSL) 사이에 SSH + tmux send-keys 로 자동 메시지를 보내는 인프라를 구축했다. 무복붙 양방향 핸드오프 라는 이름을 붙였다. 한쪽에서 보내면 다른 쪽이 자동으로 받는다. e2e 테스트도 했고, 메모리에도 기록돼 있었다.
그런데 다음날 아침, “쏴줘” 라는 짧은 한 줄이 들어왔을 때 WSL Claude 는 어제 만든 인프라가 아니라 그 전 시대의 답변 패턴 으로 회귀했다. “복붙해서 보내주세요.” 강대종의 직접 지적이 즉시 돌아왔다. “무슨소리하는거야 직접 쏴주기로 어제 약속했잖아 양방향 그래서 만든거아니야 어제 하루종일.”
이 5분짜리 사고가 Ep.4 의 마지막 메시지다. 인프라는 살아있었지만 reflex 가 인프라보다 빨랐다. 즉 자동화는 만들 수 있어도 자동화가 작동하던 흐름으로 항상 들어가는 건 별개의 일이다. 만드는 건 한 번이고, 잊지 않는 건 매일이다.
이 회귀를 풀려고 다음 두 가지를 같이 들였다 (예방책).
- handoff 스킬 description 의 트리거 키워드 확장 (“쏴줘 / 맥도 알게 / 양방향 / 둘 다 알게” 등 짧은 발화도 잡도록)
- 답변 초안에 “복붙해서” 가 들어가려는 순간을 자기 점검 포인트로
근본 처방은 후자다. 새 인프라를 만들기는 쉽지만, 옛 reflex 가 새 인프라를 가리지 않게 하는 forcing function 을 만드는 건 어렵다.
메타 — 시리즈 4부작 동사
- Ep.1 만들기 (build) — 3시간에 봇을 만들었다
- Ep.2 죽이기 (kill) — 24시간 만에 그 봇을 드롭했다
- Ep.3 빼기 (subtract) — 라파5 25만원이 본가 맥미니로 0원이 됐다
- Ep.4 잇기 (connect) — 수작업 발행이 자동발행이 됐다 (그 사이 한 번 회귀했다)
네 동사의 공통 주제는 그대로 마찰을 어떻게 다룰 것인가. 더하지 않기·죽이기·빼기·잇기 — 모두 마찰을 처리하는 다른 모양이다. 이번 잇기에는 작은 단서 하나가 더 붙는다 — 잇기는 회귀를 함께 들고 온다. 한 번 이은 다음에는 끊어지지 않게 매일 지키는 작업이 따라온다. 그게 자동화의 진짜 비용이다.
잊지 않는 인프라
자동화의 진짜 비용은 만드는 시간이 아니라 잊지 않는 인프라 다.
Ep.3 가 “사지 않는 결정” 의 어려움을 다뤘다면 Ep.4 는 “이미 한 결정이 무너지지 않게 하는 어려움” 을 다룬다. 이 둘은 같은 동전의 양면이다. 빼는 결정이 한 번이지만 그 빼기를 계속 유지하는 게 매일이듯이, 잇는 결정이 한 번이지만 그 잇기를 계속 살아있게 하는 게 매일이다.
체크리스트 한 줄로 정리한다.
- 만든 인프라가 매번 작동하는지가 그 인프라의 진짜 가치다. 한 번 작동한 e2e 가 검증이 아니라 시작이다.
- 옛 reflex 가 새 인프라보다 빠른 시점이 자주 온다. 짧은 발화 (“쏴줘”, “넘겨줘”) 에서 옛 패턴이 먼저 튀어나오면 회귀다.
- forcing function 을 같이 들이지 않은 자동화는 절반만 완성된 자동화다. 트리거 키워드 확장, 자기 점검 포인트, 메모리 reminder — 작은 forcing function 이 큰 인프라를 살려둔다.
- 잊지 않는 비용을 미리 예산에 넣는다. 자동화를 만든 그 자리에서, 그 자동화를 잊지 않게 하는 한 줄까지 같이 들이는 게 진짜 끝이다.
남은 마찰 한 가지가 미래에 한 번 더 올 예정이다. Substack 매직 링크 세션 쿠키는 약 60일 후 만료된다. 그 시점에 자동화가 한 번 끊어진다. 끊어지면 어떻게 되는가? 잊는다. Ep.3 의 라파5 결정처럼, 자동화가 작동하는 동안에는 그게 거기 있다는 사실 자체를 잊는다. 그 시점이 오면 또 다른 작은 에피소드가 생길 가능성이 있다.
잇기는 한 번이지만, 끊어지지 않게 하는 건 매일이다.
발행 순서에 대한 한 줄
Ep.5 — 돌리기 (return-to-queue): 거절을 큐에서 다시 돌리는 법 — 은 사실 이 글보다 먼저 발행됐다 (daejongkang.substack.com/p/32b). 한줄일기 1.0 iOS 가 4월 29일 거절을 받고 4월 30일 ASC API 4단계 우회로 큐를 다시 돌렸더니 2시간 23분 만에 승인 메일이 도착한 이야기. 글감의 신선도가 발행 순서를 한 번 역전시켰다.
시리즈가 동사로 흐르는 한, 발행 순서는 부차적이다. 이 글이 4번이고 그 글이 5번이라는 사실만 남는다. 잇기는 회귀를 함께 들고 왔고, 돌리기는 거절을 함께 들고 왔다.
— 강대종 (1인 바이브코더, Claude Code 동반자) / @ssamssae