개발일지
Architecture·6 min read

DB 없이 블로그를 만들고, 자동으로 쓰게 했습니다

블로그에 DB를 안 붙였다. 글은 레포 속 마크다운이고 push하면 배포된다. 콘텐츠가 코드가 되니 코드처럼 되돌릴 수 있었고, 밤마다 개발일지와 블로그 초안이 알아서 쌓이게 자동화할 수도 있었다. 그 무인 파이프라인을 좁은 반경 안에 가둔 방법까지.

글을 한 문단 고쳐 쓰다가 마음에 안 들어서 git diff를 쳤다. 방금 바꾼 세 줄이 빨강과 초록으로 떴다. 초록을 버리고 빨강으로 돌아갔다. 글을 코드처럼 되돌린 것이다.

이 블로그에는 DB가 없다. 자체 백엔드도 없다. 글은 레포 안에 마크다운 파일로 들어 있고, 커밋해서 push하면 Vercel이 알아서 배포한다. GitHub를 CMS로 쓴 셈인데, 나는 이 방식이 처음이었다.

처음엔 임시방편이라고 생각했다. 나중에 제대로 된 CMS를 붙이자고. 그런데 쓰다 보니 이게 임시방편이 아니라 그냥 나한테 맞는 방식이었다.

되돌리기가 공짜였다

가장 먼저 좋았던 건 이력이 공짜라는 점이었다. 글의 모든 수정이 커밋으로 남는다. 언제 무슨 문장을 고쳤는지 git log로 뒤질 수 있고, 어제 잘못 손댄 문단은 커밋 하나 되돌리면 그대로 복구된다. 백업이나 버전 관리를 따로 만들 필요가 없다. Git이 이미 그걸 하고 있으니까.

쓰는 자리도 익숙했다. 웹 에디터에 로그인해서 쓰는 게 아니라, 코드 짜던 그 에디터에서 그대로 쓴다. 마음에 안 들면 브랜치를 파서 실험한다. 관리자 화면이 없다는 게 불편이 아니라 오히려 홀가분했다.

관리자라고 부를 만한 건 파일 하나였다

폰에서 급히 오타를 고칠 일이 생겨서, Pages CMS를 하나 얹었다. 레포 루트에 .pages.yml 파일을 두면 GitHub 로그인으로 들어가는 웹 편집 화면이 생긴다. 그런데 그 화면에서 글을 저장해도 결국 하는 일은 레포에 마크다운을 커밋하는 것뿐이다. 편집기가 웹으로 바뀌었을 뿐, 글이 사는 곳은 여전히 git이다.

여기서 한 번 걸렸다. Pages CMS가 frontmatter의 date를 날짜 타입으로 읽어서, 저장할 때마다 형식을 제멋대로 바꿔놨다. .pages.yml에서 date 필드를 문자열로 못 박고 나서야 멈췄다.

마크다운 안에서 케이스 스터디를 조립했다

순수 마크다운만으로는 부족한 순간이 있었다. 프로젝트 소개 페이지는 그냥 글이 아니라 구조가 있는 케이스 스터디여야 했다. 문제-결정-결과의 흐름, 내가 왜 이 선택을 했는지, 어떤 기능이 있는지. 이걸 글줄로만 늘어놓으면 밋밋해진다.

그렇다고 페이지 하나하나를 리액트 컴포넌트로 짜면 글 쓰는 게 너무 무거워진다. 문장 하나 고치려고 JSX를 헤집고 싶지는 않았다.

그래서 중간을 택했다. Flow, Decision, Features 같은 커스텀 블록을 MDX 컴포넌트로 만들어 두고, 마크다운 본문 안에 필요한 자리에 끼워 넣었다. 글은 계속 마크다운으로 쓰되, 구조가 필요한 대목만 블록으로 조립하는 식이다. content/projects 아래 마크다운 한 파일이 곧 프로젝트 페이지 하나가 된다.

스크린샷 준비중

왼쪽은 마크다운 소스, 오른쪽은 배포된 페이지. 같은 파일이다.

MDX로 렌더한다는 게 나중에 사소하게 발목을 잡기도 했다. 본문에 무심코 넣은 HTML 주석 <!-- -->이 빌드를 통째로 넘어뜨렸다. MDX는 그 자리를 주석으로 안 읽고 태그로 파싱하려다 깨진다. MDX 주석 {/* */}으로 바꿔서 끝냈다.

파일이니까 스크립트가 건드릴 수 있었다

글이 결국 레포 안의 파일이다. 파일이면 스크립트가 열어서 고칠 수 있다. 사람이 웹 어드민에 로그인해야 하는 CMS였다면 엄두도 못 냈을 일이다.

이게 왜 중요했냐면, 나한테는 기록이 자꾸 증발하는 문제가 있었다. 두 달 전에 풀었던 에러를, 두 달 뒤에 똑같이 30분을 헤매고 나서야 그때 그거라는 걸 깨달았다. 분명히 풀었는데 어떻게 풀었는지가 아무 데도 안 남아 있었다.

커밋 로그가 있긴 했다. 하지만 커밋은 무엇을 했는지는 남겨도 왜 막혔고 어떻게 풀었는지는 안 남긴다. 게다가 커밋하지 않은 작업은 로그에 아예 없다. 하루 종일 붙잡았지만 아직 커밋 못 한 파일이 그날 가장 중요한 작업인 경우가 많은데, 그게 통째로 사라진다.

블로그도 여러 번 시작했다가 매번 접었다. 원인은 늘 같았다. 빈 화면에서 글을 처음부터 쓰는 게 너무 무거웠다.

그래서 접근을 바꿨다. 글을 쓰겠다고 결심하는 대신, 이미 하고 있는 작업이 알아서 기록으로 남게 만들기로 했다. 글이 파일이고 파일은 스크립트가 건드릴 수 있으니, 해볼 만한 일이었다.

사진 준비중

기록을 따로 만들지 않는다. 작업의 톱밥으로 떨어지게 둔다.

세 층으로 쌓이게 했다

기록을 세 층으로 나눴다. 맨 아래가 가장 날것이고, 위로 올라갈수록 다듬는다.

맨 아래는 일자별 개발일지다. 그날 만진 것, 막힌 것, 배운 것을 하루 한 파일로 모은다. 비공개고, 매일 밤 초안이 자동으로 생긴다.

그 위는 이슈 원장이다. 막히면 그 자리에서 [OPEN]으로 적고, 풀리면 원인과 해결을 덧붙여 [RESOLVED]로 닫는다. 깨달음은 떠오를 때 바로 박아둔다.

맨 위가 블로그다. 닫힌 이슈와 배움 중에서 글이 될 만한 걸 골라 한 편에 한 주제로 다듬는다. 검토하고 발행한다.

층을 세 개나 둔 건 각 층이 다루는 단위가 다르기 때문이다. 일지는 하루 단위로 쌓이고, 블로그는 주제 단위로 나온다. 어제 개발일지만 봐도 새김AI 스켈레톤, noon 눈 애니메이션, 포트폴리오 허브 개편이 한 파일에 다 뒤섞여 있다. 그걸 그대로 공개하면 잡탕이다. 거기서 한 주제만 추려내야 글 한 편이 된다.

매일 밤, 내 맥에서

매일 밤 11시에 내 맥에서 launchd가 헤드리스로 claude -p "/devlog auto"를 돌린다. 그날 만진 파일과 이슈 원장을 긁어 개발일지 초안을 하나 만들고, 다 되면 macOS 알림을 띄운다. 자고 일어나면 어제 하루가 초안으로 정리돼 있는 것이다.

스크린샷 준비중

자고 일어나면 어제가 초안으로 정리돼 있다.

내 맥에서 도는 건 이유가 있었다. 처음엔 클라우드에 올리려 했다. 그런데 클라우드에서 도는 에이전트는 내 로컬 파일을 못 본다. 아직 커밋 안 한 작업, 그러니까 내가 가장 남기고 싶은 부분이 안 보인다는 뜻이다. 결국 로컬 스케줄러로 내렸고, 밤마다 내 맥에서 돌면서 미커밋 작업까지 전부 훑는다.

훑는 기준도 커밋이 아니라 파일 수정시각(mtime)으로 잡았다. 커밋 로그만으로는 하루가 복원되지 않았기 때문이다. 새로 시작한 프로젝트는 통째로 untracked라 로그에 한 줄로 뭉개지고, 기존 파일 수정은 언제 했는지가 안 남는다. mtime으로 긁으면 그날 손댄 파일이 커밋 여부와 상관없이 다 잡힌다. 이슈는 로그에서 뽑으려 하지 않고 앞서 말한 별도 원장으로 따로 추적했다.

자정을 넘겨 일하면 날짜가 어긋나는 문제도 있었다. 새벽 1시 작업이 다음 날로 잡혀서 어젯밤 몫이 엉뚱한 날짜에 붙었다. 개발일을 06시부터 다음 날 04시까지로 정의해서, 늦은 밤 작업을 전날로 묶었다.

여기서 한 발 더 나갔다. 개발일지 다음에 /blog-draft auto를 이어 붙여, 글감이 될 만한 걸 블로그 초안으로 빚고 그걸 posts 폴더에 커밋해서 push하게 했다. push가 들어가면 Vercel이 그 초안을 [초안] 프리뷰로 배포하고, 다시 알림이 온다. 완성된 글이 아니라, 검토를 기다리는 초안이 프리뷰 URL로 올라오는 것이다.

무인으로 push하게 두는 게 무서웠다

여기가 이 파이프라인에서 제일 오래 고민한 부분이다.

헤드리스로 도는 프로세스는 아무도 안 본다. 내가 자는 사이에 뭔가 잘못을 저지르면 그 자리에서 막을 사람이 없다. 사람 없이 돌리려고 /devlog를 비대화 모드로 실행했더니, 명령을 실행할 때마다 권한을 묻는 프롬프트가 떴고 대답할 사람이 없으니 거기서 멈췄다. 모든 명령을 무조건 허용하는 옵션으로 우회하려다 그만뒀다. 밤새 무인으로 도는 프로세스에 셸 전권을 쥐여주면, 이론상 파일을 지우든 엉뚱한 커밋을 하든 뭐든 할 수 있게 된다.

그래서 할 수 있는 일을 좁혔다. 임의의 셸을 여는 대신, 이 작업에 실제로 필요한 읽기 명령만 몇 개 허용 목록으로 열었다. git 조회, find, date, stat, grep, ls 정도. 딱 그날 만진 파일을 훑는 데 필요한 것들이다. 파일 읽기와 쓰기는 되지만 그 밖의 셸 실행은 막았다.

push도 마찬가지로 가뒀다. 커밋과 push는 오직 posts 폴더 안에서만 일어나게 했다. 여기서 Git이 CMS인 게 오히려 유리했다. 콘텐츠가 경로로 딱 나뉘어 있으니, 이 폴더 밖은 건드리지 마라는 규칙을 그대로 박을 수 있었다. DB였다면 이렇게 깔끔하게 선을 긋기 어려웠을 것이다.

자동으로 흐르는 구간과, 사람이 방아쇠를 당기는 구간.
자동으로 흐르는 구간과, 사람이 방아쇠를 당기는 구간.

가장 위험한 단계, 그러니까 실제로 push까지 하는 부분은 아예 기본값을 꺼둔 채로 뒀다. 개발일지 자동 생성은 켜두되, 블로그 초안을 push하는 단계는 수동으로 며칠 돌려보고 믿을 만해진 다음에야 켰다.

완전 무인은 아니다

이렇게 써놓고 보면 밤사이 알아서 글이 올라오는 블로그처럼 들리지만, 그건 아니다.

자동화되는 건 딱 초안을 만들고 긁어모으는 데까지다. 이건 이슈로 기록할 만하다, 이 배움은 글이 되겠다, 이 초안을 발행하자 같은 판단은 전부 내가 한다. 방아쇠는 사람이 당긴다. 발행 승인 앞에서 자동화는 멈추고, 그 앞까지 밀어주는 게 이 파이프라인의 역할이다.

이 경계를 흐리고 싶은 유혹이 없었던 건 아니다. 판단까지 자동 안으로 밀어 넣으면 더 그럴듯해 보이니까. 그래도 검토 없이 내 이름으로 나가는 글을 만들고 싶지는 않았다.

지금도 글은 레포 안 마크다운이고, 관리자 화면이라 할 건 .pages.yml 하나가 전부다. 언젠가 글이 수백 개가 되면 이 방식이 삐걱댈지도 모른다. 그건 그때 가서 옮기면 된다. 마크다운은 어디로든 들고 갈 수 있으니까. 적어도 지금은, DB를 안 붙이길 잘했다.