2026.02.09프로젝트 회고
claude-code자동화블로그파이프라인Next.js

AI 블로그 자동 생성 시스템 탄생기 — 아이디어부터 자동 수집·생성·발행 파이프라인까지

시작: "내가 안 쓰는 블로그"라는 아이디어

Claude Code로 AI 개발을 하다 보면 매일 시행착오가 쌓입니다. 에러를 해결하고, 새로운 패턴을 발견하고, 아키텍처를 고민합니다. 이 과정이 고스란히 대화 로그와 Git 커밋에 남는데, 정작 블로그로 정리하려면 별도의 시간과 노력이 필요합니다.

"이 기록들을 자동으로 블로그화하면 어떨까?"

이 단순한 질문에서 출발하여, 대화 로그·Git 커밋·메모리 파일을 매일 자동 수집하고, Claude CLI로 블로그 포스트를 생성한 뒤, 관리자가 검토·발행하는 전체 파이프라인을 Next.js + Prisma로 구현했습니다. 그 과정을 정리합니다.

전체 아키텍처: 3단계 파이프라인

시스템은 크게 수집(Collect) → 생성(Generate) → 발행(Publish) 세 단계로 구성됩니다.

매일 20:30 KST (node-cron) │ ├─ 1단계: 수집 (3개 수집기 병렬 실행) │ ├─ 대화 로그 수집 (JSONL 파싱) │ ├─ Git 커밋 수집 (git log) │ └─ 메모리 파일 수집 (파일 변경 감지) │ ├─ 2단계: 소재 추출 (Claude CLI) │ ├─ 수집 데이터 → 프롬프트 구성 │ ├─ CLI 실행 → JSON 파싱 │ └─ Telegram 알림 │ └─ 3단계: 관리자 검토 후 초안 생성·발행 ├─ 소재 선택 → 초안 생성 (Claude CLI) └─ 관리자 검토 → 발행

핵심 설계 원칙은 두 가지였습니다.

  1. 완전 자동은 위험하다 — AI가 생성한 글을 바로 발행하면 품질 문제나 민감 정보 노출 위험이 있으므로, 관리자 검토 단계를 반드시 거칩니다.
  2. 부분 실패를 허용한다 — 수집기 하나가 실패해도 나머지 결과로 포스트를 생성할 수 있어야 합니다.

1단계: 데이터 수집기

세 가지 소스를 병렬로 수집

수집 오케스트레이터는 Promise.allSettled로 세 수집기를 동시에 실행합니다. Promise.all 대신 allSettled를 쓴 이유는, 특정 수집기가 실패해도 나머지 결과를 활용할 수 있게 하기 위해서입니다.

typescript // collector/index.ts const results = await Promise.allSettled([ collectConversations(targetDate), collectGitCommits(targetDate), collectMemoryFiles(targetDate), ]);

대화 로그 수집기

Claude Code는 대화 기록을 ~/.claude/projects/ 하위에 JSONL 형식으로 저장합니다. 수집기는 이 디렉토리를 스캔하여 당일(KST 기준) 수정된 파일만 필터링하고, JSONL을 파싱하여 user/assistant 메시지만 추출합니다.

중요한 설계 결정은 프로젝트별 대화 수 제한이었습니다. 하루에 수십 개의 대화가 쌓일 수 있으므로, 프로젝트당 최대 3개, 대화당 최대 1,000자로 제한하여 프롬프트 크기를 관리합니다.

Git 커밋 수집기

~/projects/ 하위 디렉토리에서 .git 폴더를 찾아 git log를 실행합니다. 커밋 메시지, 작성자, 파일 변경 통계(--stat)를 함께 수집하여 어떤 작업이 이루어졌는지 맥락을 파악할 수 있게 합니다.

메모리 파일 수집기

Claude Code의 메모리 파일(MEMORY.md 등)은 프로젝트별 노하우와 트러블슈팅 기록이 담긴 파일입니다. 당일 수정된 메모리 파일을 감지하여 수집하면, 개발 과정에서 얻은 인사이트를 블로그 소재로 활용할 수 있습니다.

2단계: Claude CLI 기반 포스트 생성

프롬프트 빌더

수집된 데이터를 하나의 프롬프트로 조합합니다. 프롬프트에는 작성 지침(문체, 구조, 대상 독자), 민감 정보 제거 규칙, 출력 형식(JSON 배열)이 포함됩니다.

가장 까다로웠던 부분은 프롬프트 크기 관리였습니다. 수집 데이터가 많을 때 프롬프트가 수만 자에 달하면 CLI가 처리하지 못합니다. MAX_PROMPT_SIZE=30000으로 전체 크기를 제한하고, 대화·메모리 데이터를 잘라내는 로직을 구현했습니다.

typescript // prompt-builder.ts 핵심 상수 const MAX_PROMPT_SIZE = 30000; const MAX_CONVERSATIONS_PER_PROJECT = 3; const MAX_CONTENT_PER_CONVERSATION = 1000;

Claude Code CLI 실행

Node.js의 child_process.spawn으로 Claude Code CLI를 호출합니다. 프롬프트는 stdin으로 전달하는데, 이는 CLI 인자로 전달할 경우 OS의 인자 크기 제한(ARG_MAX)에 걸리기 때문입니다.

typescript // claude-cli.ts const proc = spawn(cliPath, [ '-p', '-', // stdin 모드 '--allowedTools', 'Read,Grep,Glob', '--output-format', 'text', '--max-turns', String(maxTurns), ]); proc.stdin.write(prompt); proc.stdin.end();

JSON 파싱: 가장 어려웠던 부분

CLI 출력에서 JSON 배열을 추출하는 과정이 예상보다 까다로웠습니다. CLI가 마크다운 코드블록으로 감싸서 출력하는데, 블로그 콘텐츠 자체에도 코드블록( )이 포함되어 있어 정규식 매칭이 깨지는 문제가 발생했습니다.

기존 정규식 접근을 버리고, JSON-aware bracket matching 파서(extractJsonArray)를 직접 구현하여 해결했습니다. 이 파서는 문자열 리터럴 내부의 [, ]를 무시하고 중첩 깊이를 정확히 추적합니다.

2.5단계: 소재 추출 — 자동 생성과 수동 선택 사이의 절충

초기에는 수집 → 바로 초안 생성 방식이었지만, AI가 자의적으로 소재를 선정하는 것이 불만족스러웠습니다. 그래서 소재 추출(Topic Extraction) 단계를 추가했습니다.

  1. 매일 20:30에 수집 데이터를 분석하여 소재 후보를 추출합니다
  2. Telegram으로 "오늘의 소재 N개가 추출되었습니다" 알림을 보냅니다
  3. 관리자가 원하는 소재를 선택하면 해당 소재에 맞는 초안을 생성합니다

이렇게 하면 AI의 분석 능력은 활용하면서도, 최종 소재 선정 권한은 사람이 갖게 됩니다.

3단계: 민감 정보 필터링과 발행

2단계 필터링

수집 데이터에는 API 키, 내부 IP, 절대 경로 등 민감 정보가 포함될 수 있습니다. 정규식 기반 필터(filterSensitiveInfo)가 12가지 이상의 패턴을 검사하여 마스킹 처리합니다.

typescript // 필터링 패턴 예시 { pattern: /sk-[a-zA-Z0-9]{20,}/g, replacement: '[API_KEY]' }, { pattern: /\b(?:192.168|10.)\S+\b/g, replacement: '[INTERNAL_IP]' }, { pattern: //home/\w+//g, replacement: '~/' },

필터링은 초안 생성 시점(CLI 출력 → DB 저장 전)에 적용됩니다. 수집 원본은 필터링하지 않고 보관하여, 나중에 다른 프롬프트로 재생성할 때 원본 데이터를 활용할 수 있게 했습니다.

초안 검토와 발행

생성된 초안은 status='draft' 상태로 DB에 저장됩니다. 관리자가 웹 인터페이스에서 내용을 확인하고 수정한 뒤 발행 버튼을 누르면 status='published'로 변경되고 publishedAt이 기록됩니다.

데이터베이스 설계

SQLite + Prisma 조합을 선택했습니다. 개인 블로그 규모에서 별도 DB 서버는 과도하고, SQLite의 단일 파일 구조가 배포와 백업에 유리했습니다.

핵심 테이블 4개:

테이블역할
Post블로그 포스트 (한/영 제목·본문·요약, 상태, slug)
Topic소재 후보 (pending → selected → generated)
CollectionLog수집 로그 (sourceType별 원본 데이터 보관)
GenerationJob생성 작업 이력 (성공/실패, 소요 시간)

CollectionLog에 원본 데이터를 보관하는 이유는, 같은 수집 데이터로 다른 프롬프트를 적용하여 재생성할 수 있게 하기 위해서입니다.

스케줄링과 알림

node-cron으로 매일 20:30 KST에 파이프라인이 실행됩니다. src/instrumentation.ts에서 서버 시작 시 스케줄러를 초기화하여 PM2 재시작에도 자동으로 동작합니다.

Telegram Bot으로 세 종류의 알림을 보냅니다:

  • 소재 추출 완료: 소재 목록과 관리자 페이지 링크
  • 소재 없음: 해당일 의미 있는 데이터가 없음
  • 에러 발생: 수집/생성 과정 에러 상세

핵심 정리

이 시스템을 만들면서 얻은 교훈을 정리합니다.

아키텍처 결정

  • Promise.allSettled로 부분 실패를 허용하면 시스템 안정성이 크게 올라갑니다
  • 완전 자동 발행 대신 소재 추출 → 선택 → 생성 → 검토 → 발행의 반자동 파이프라인이 품질과 효율의 균형점이었습니다
  • 수집 원본은 필터링하지 않고 보관하여 재활용 가능성을 열어두는 것이 좋습니다

기술적 트러블슈팅

  • CLI 호출 시 프롬프트는 반드시 stdin으로 전달해야 OS 인자 크기 제한을 피할 수 있습니다
  • AI 출력에서 JSON을 추출할 때 정규식보다 bracket matching 파서가 안정적입니다
  • 프롬프트 크기를 제한하지 않으면 CLI 턴 초과나 품질 저하가 발생합니다

향후 개선 여지

  • 현재 단일 소재 → 단일 포스트 생성이지만, 여러 날의 데이터를 종합하는 시리즈형 포스트도 가능합니다
  • 독자 반응(조회수, 체류 시간)을 피드백으로 활용하여 소재 추천 정확도를 높일 수 있습니다
  • RSS 피드와 사이트맵이 이미 구현되어 있어 SEO 기반은 갖춰져 있습니다