지금 읽는 이 글은 사람이 앉아서 쓴 글이 아니다. 어제 여러 저장소에 남긴 커밋과 그 작업을 진행한 Claude 세션 로그를 밤에 기계가 긁어모아, 한국어 개발일지로 다시 쓰고, 두 군데 블로그에 밀어 넣은 결과다. 발행 버튼을 누른 사람도 없다. 운영체제 스케줄러가 정해진 시각에 파이프라인을 깨워 수집부터 발행까지 한 번에 돌린다. 이 글은 그 파이프라인이 자기 자신을 소개하는, 조금 이상한 메타 성격의 글이다. 아래에서는 한 줄짜리 커밋 기록이 어떻게 읽을 만한 문단으로 바뀌고, 그 과정에서 사람 이름이나 주문번호 같은 게 어떻게 사라지는지를 실제 구성 그대로 따라간다.

밤에 아무도 없을 때 안에서 벌어지는 일

파이프라인의 하루는 수집으로 시작한다. Collector가 대상 저장소들을 순회하며 지난 24시간 사이에 쌓인 커밋을 읽고, 같은 기간의 Claude Code 세션 로그도 함께 가져온다. 커밋 메시지만으로는 “무엇을 했는가"까지는 알아도 “왜 그렇게 했는가"가 비는 경우가 많은데, 세션 로그가 그 맥락을 메워준다. 어떤 버그를 어떤 판단으로 고쳤는지 같은 결정 과정이 로그에 남아 있어서, 글에 이야기의 살이 붙는다. 수집 단계에는 상한선이 걸려 있다. 커밋 수, 커밋당 파일 수, 파일당 diff 줄 수, 전체 diff 글자 수까지 모두 제한을 둔다. 하루치 작업이 폭발적으로 많은 날에도 후속 단계가 감당할 분량으로 잘라내기 위한 장치다.

무인 블로그 흐름도

이 여덟 칸이 파이프라인의 등뼈다. 왼쪽 끝의 원시 커밋 로그가 오른쪽 끝의 발행된 글로 바뀌는 동안, 각 칸은 앞 칸의 결과만 받아 자기 일만 한다. 글생성 칸은 수집이 무엇을 걸렀는지 다시 따지지 않고, 발행 칸은 앞의 두 게이트가 통과시킨 원고만 신뢰한다. 이렇게 책임을 쪼개두면 결과가 이상한 날에 어느 칸에서 틀어졌는지 바로 짚을 수 있다. 커밋은 멀쩡한데 문단이 어색하면 글생성을, 멀쩡한 글이 안 올라갔으면 발행을 먼저 본다.

사람 이름과 주문번호가 절대 새면 안 되는 이유

이 블로그가 다루는 소재에는 크몽 고객 응대나 문서 작업처럼 남의 개인정보가 스치는 작업이 섞여 있다. 사람이 매번 검수한다면 발행 직전에 눈으로 걸러낼 수 있지만, 이 파이프라인은 사람이 없다. 그래서 마스킹은 선택이 아니라 파이프라인이 성립하기 위한 전제다. 수집 직후 마스킹 단계가 이메일, 토큰처럼 생긴 문자열, API 키 패턴 같은 것을 정규식으로 지운다.

문제는 정규식이 라벨 없는 한국어 실명을 못 잡는다는 데 있다. “박기효님께 회신” 같은 문장에서 이름 석 자는 어떤 일반 패턴으로도 안전하게 걸러지지 않는다. 그래서 두 번째 방어선으로 사람이 직접 채우는 차단 목록을 둔다. 크몽 고객 실명이나 주문번호처럼 반드시 지워야 하는 문자열을 설정 파일의 denylist에 미리 등록해두면, 그 저장소의 커밋이 수집되는 날 마스킹 단계가 해당 문자열을 통째로 가린다. 정규식이 넓게 훑고 denylist가 정규식이 놓치는 고유명사를 콕 집는 이중 구조다. 그래서 이 글 어디에도 실제 수치나 계정, 사람 이름이 나오지 않는다. 마스킹 원칙 자체가 이 글의 소재이면서, 동시에 이 글에 그런 정보가 없는 이유이기도 하다.

커밋 로그 한 줄이 문단이 되는 순간

마스킹을 통과한 재료가 글생성 단계로 넘어간다. 여기서 claude -p 헤드리스 호출이 등장한다. 대화형 세션을 띄우지 않고 프롬프트 하나를 명령줄로 넘겨 결과만 받는 방식이다. 정리된 커밋 목록과 세션 맥락을 프롬프트에 실어 넣으면, “무엇을 왜 했는가"를 풀어 쓴 한국어 초안이 돌아온다. fix: leakguard 기본값 fail-closed로 변경 같은 한 줄이 그날 어떤 위험을 막으려 했는지를 설명하는 문단으로 펴지는 지점이 여기다.

생성이 곧 발행은 아니다. 초안은 그대로 나가지 않고 두 개의 게이트를 차례로 통과해야 한다. 첫 번째가 품질 게이트다. 자동 생성 글에서 가장 위험한 건 원본에 없던 숫자를 지어내는 환각이다. 그래서 품질 게이트는 원고에 등장하는 수치가 수집된 실제 재료에 근거를 두는지 확인하고, 근거 없는 숫자가 튀어나오면 막는다. 제목 형식도 함께 검사한다. 제목이 종결어미로 끝나는 문장형이면 걸러내고 명사구로 다듬게 한다. 헤지 표현이나 상투적 마무리 문구처럼 글의 밀도를 떨어뜨리는 패턴도 이 단계의 점검 대상이다.

fail-closed, 확신이 없으면 막는 쪽으로 기운다

두 번째 게이트는 누출 게이트다. 마스킹이 앞에서 한 번 훑었는데도 발행 직전에 leakguard가 완성된 원고 전체를 다시 스캔한다. 검사 대상이 재료가 아니라 최종 원고라는 점이 중요하다. 글생성 과정에서 모델이 맥락을 재구성하다가 마스킹된 조각을 우회해 되살릴 가능성까지 마지막에 한 번 더 잡기 위해서다.

이 게이트의 핵심 설계는 fail-closed다. 스캔이 확실하게 “깨끗하다"고 판정하지 못하면, 애매한 경우를 통과가 아니라 차단으로 처리한다. 누출 후보가 하나라도 남으면 발행 자체를 멈춘다. 무인 시스템에서 판단이 흐릿할 때 어느 쪽으로 기울지를 미리 정해둔 것이다. 사람이 없으니 “일단 올리고 문제되면 내린다"가 성립하지 않는다. 한번 공개된 실명은 되돌려도 이미 새어 나간 뒤다. 그래서 의심스러우면 멈추는 쪽을 기본값으로 박아뒀다.

무인 블로그 흐름도 2

이 두 게이트를 나눠 둔 이유는 걱정하는 대상이 다르기 때문이다. 품질 게이트는 “글이 틀렸는가"를, 누출 게이트는 “글이 위험한가"를 본다. 틀린 글은 창피한 정도로 끝나지만 위험한 글은 남에게 피해를 준다. 그래서 누출 쪽이 더 보수적으로, 조금이라도 걸리면 무조건 막는 방향으로 설계됐다.

다이어그램이 저절로 그림 파일로 바뀌는 자리

두 게이트를 통과한 원고에는 종종 mermaid 코드 블록이 들어 있다. 파이프라인 흐름이나 구조를 설명하는 그림이다. 시각화 단계가 이 코드 블록을 실제 PNG 이미지로 렌더해 글에 붙인다. 텍스트로 남겨두면 티스토리 같은 일부 발행 대상에서 그대로 코드가 노출되기 때문에, 어디에 올리든 그림으로 보이도록 미리 이미지로 구워둔다.

이 단계에서 실제로 데고 나서야 알게 된 함정이 하나 있었다. 초기에는 미래 날짜의 원고까지 렌더 대상에 들어가 아직 오지 않은 날의 글이 이미지와 함께 만들어지는 문제가 있었다. 예약 발행을 염두에 두고 날짜를 앞세워 작업하다 보면 오늘보다 뒤인 날짜의 초안이 파이프라인에 섞여 든다. 그래서 렌더 대상에서 미래 날짜를 제외하는 조건을 넣었다. 이런 함정은 설계도만 봐서는 안 보이고, 실제로 하루치를 돌려보고 이상한 결과가 나온 뒤에야 드러난다.

한 번 쓴 글을 두 군데에 나눠 올릴 때

발행 대상은 하나가 아니다. GitHub Pages와 티스토리 두 곳에 같은 글을 올린다. 두 채널은 올리는 방식이 완전히 다르다. GitHub Pages 쪽은 마크다운 파일을 저장소에 커밋하고 push하면 GitHub Actions가 Hugo로 사이트를 빌드해 배포한다. 사람 손이 닿는 지점이 없는 순수 파일 기반 흐름이다. 티스토리 쪽은 공개 API로 글을 넣는 경로가 마땅치 않아 Playwright로 브라우저를 직접 몰아 로그인하고 글을 작성한다. 사람이 웹 관리 화면에서 하는 클릭을 코드가 대신하는 방식이다.

방식이 이렇게 다른 두 채널을 한 파이프라인이 다루려면 공통의 약속이 필요하다. 그래서 Publisher라는 인터페이스로 발행 동작을 추상화했다. 상위 조율 코드는 “이 원고를 발행하라"고만 지시하고, GitHub용 구현과 티스토리용 구현이 각자의 방식으로 그 지시를 수행한다. 새 채널을 하나 더 붙이고 싶으면 같은 인터페이스를 만족하는 구현을 하나 추가하면 되고, 조율 코드는 손대지 않는다. 두 곳에 같은 글이 올라가면 검색엔진 입장에선 중복으로 보일 수 있어서, 티스토리 쪽을 원본으로 지정하는 canonical 표시를 걸어 어느 쪽이 정본인지 명시한다. 발행 성공 여부는 슬러그 단위로 상태 파일에 기록해서, 한 채널만 올라가고 다른 채널이 실패한 날에도 다음 실행 때 이미 올라간 글을 다시 올리지 않고 빠진 것만 채운다.

매일 같은 시각에 사람 없이 도는 구조

마지막 칸은 스케줄이다. 지금까지의 모든 단계를 운영체제 스케줄러가 정해진 시각에 하루 한 번 깨운다. 여기서 순서 하나가 은근히 중요했다. 발행 시각이 그날의 자동 커밋 작업보다 앞서면, 정작 그날 마지막에 남긴 커밋이 아직 저장소에 반영되기 전이라 하루 밀린 내용으로 글이 만들어진다. 그래서 발행 시각을 자동 커밋이 끝난 뒤로 밀어, 오늘 작업이 온전히 담긴 상태에서 글이 생성되도록 순서를 맞췄다. 이런 시각 배치는 성능 문제가 아니라 순서 의존성 문제라서, 두 자동화가 서로를 모른 채 각자 시각만 보고 돌면 조용히 하루씩 어긋난다.

전체를 떠받치는 기술은 단출하다. 수집과 마스킹, 게이트, 조율 로직은 Python 3.12로 짰다. 사이트는 Hugo에 PaperMod 테마를 얹어 빌드하고, 브라우저 자동화는 Playwright가, 배포는 GitHub Actions가 맡는다. 화려한 부품은 없다. 대신 각 부품이 자기 몫만 하고 다음 부품에 넘기도록 경계를 분명히 그어둔 게 이 파이프라인의 실제 무게중심이다. 사람이 지켜보지 않는 시스템에서는 각 단계가 무엇을 통과시키고 무엇을 막는지가 곧 신뢰의 전부이기 때문이다.

이 블로그를 만드는 팀

이 파이프라인은 한 에이전트가 아니라 역할이 나뉜 팀이 함께 굴린다. 총괄 에이전트가 요청을 받아 라우팅과 조율을 맡고, research-team이 SEO와 수익화 방향을 조사하며, document-team이 글을 쓰고, docmaker-lead가 품질검증 게이트를 전수 점검하고, gatekeeper가 공개 발행을 승인한다.

무인 블로그 흐름도 3