Claude Code로 블로그 글 발행 자동화 구축하기

작성일: 2026년 5월 9일 PM 04:36(마지막 수정: 2026년 5월 9일 PM 05:12)
조회수: 24

thumbnail

개발동생이 만든 /deep-interview로 요구사항을 구체화하고, /pwrc-plan → /pwrc-work → /pwrc-review → /pwrc-compound 사이클로 구현한 기록입니다. 블로그 발행 자동화를 시작으로 Unsplash 썸네일 검색, 전역 커맨드 전환까지 파이프라인이 보완되어 온 과정을 담았습니다. 이 글도 그렇게 배포되었습니다.


발단: "어디서든 블로그 글을 발행하고 싶다"

마크다운으로 글을 쓰고 나서 블로그에 올리는 과정이 번거로웠습니다. 관리자 페이지에 접속해서 제목 입력하고, 내용 붙여넣고, 해시태그 하나하나 선택하고... 반복하다 보니 자동화하고 싶어졌습니다.

처음에는 막연했습니다.

"md 파일을 블로그에 올리는 자동화를 만들고 싶은데, CI로 할지 브라우저 자동화로 할지, 이미지는 어떻게 처리하고, 해시태그는..."

이 모호한 상태에서 개발을 바로 시작하면 나중에 방향을 바꾸는 비용이 커집니다. 그래서 개발동생이 만든 /deep-interview 스킬을 먼저 실행했습니다.


/deep-interview: 모호한 것을 구체적으로

개발동생이 만들어 공개한 /deep-interview 스킬을 가져와 첫 단계에 사용했습니다. 이 스킬은 소크라테스식으로 한 번에 하나씩 질문하며 요구사항을 좁혀갑니다. 답을 정해주지 않고, 내 암묵적 가정과 판단 기준이 드러나도록 묻습니다.

다섯 번의 질문-답변으로 다음이 결정됐습니다.

결정 항목선택이유
실행 환경Claude Code 슬래시 커맨드이미 이 환경에서 작업 중
발행 방식Supabase 직접 호출브라우저 없이 동작, .env.local 크레덴셜 활용
이미지 처리로컬 → Supabase Storage 업로드md 내 로컬 이미지 경로를 공개 URL로 치환
콘텐츠 생성미리 작성한 md 파일 + 어투 다듬기AI 글 생성은 나중 단계로 분리
해시태그 확인해시태그 단계에서만 사용자 확인DB 오염 방지, 어투 다듬기는 자동

interview가 끝난 후 요구사항 정리:

md 파일 입력
  → 기존 블로그 글 조회 (어투 학습)
  → Claude 어투 다듬기 (자동)
  → 로컬 이미지 → Supabase Storage 업로드 → URL 치환
  → 기존 해시태그 DB 조회 → Claude 매칭/신규 제안
  → 사용자 해시태그 확인 ← 유일한 중단점
  → Supabase에 글 등록

/pwrc-plan: 코드 탐색 후 설계

개발동생이 기존 코드베이스를 읽어 중요한 사실을 파악했습니다.

  • createPost는 Next.js 서버 환경에 의존 → CLI 스크립트에서 직접 쓸 수 없음
  • extractImagePathsFromMarkdown은 Supabase Storage URL만 파싱 → 로컬 경로 처리 불가
  • createHashtags는 이름 문자열을 받아 upsert → CLI에서 그대로 재구현 가능
  • createServiceRoleClientNEXT_PUBLIC_SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY만 있으면 됨

결론: Next.js 의존성 없이 @supabase/supabase-js를 직접 호출하는 독립 스크립트 5개를 만들기로.

생성할 파일:

scripts/blog-publish/
  supabase-client.ts    # 공통 Supabase 클라이언트
  get-hashtags.ts       # DB 해시태그 전체 조회
  get-recent-posts.ts   # 어투 학습용 최근 글 조회
  upload-images.ts      # 로컬 이미지 → Supabase Storage
  create-post.ts        # 최종 posts 테이블 insert
.claude/commands/
  blog-publish.md       # 슬래시 커맨드 정의

/pwrc-work: 구현

TDD exception을 선언하고 (테스트 러너 없음) 각 스크립트를 "파일 없음 → 실행 실패 → 파일 생성 → 실행 성공"의 Red → Green 사이클로 작성했습니다.

supabase-client.ts — dotenv로 .env.local을 로드하고 service role 클라이언트 생성:

typescript
import { createClient } from '@supabase/supabase-js';
import { config } from 'dotenv';
import { resolve } from 'path';

config({ path: resolve(process.cwd(), '.env.local') });

export const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
);
export const BUCKET = 'files';

upload-images.ts — 로컬 이미지를 permanent/image/{uuid}.ext로 업로드:

typescript
export async function uploadLocalImage(localPath: string): Promise<string> {
    const ext = extname(localPath).toLowerCase();
    const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
    const fileBuffer = readFileSync(localPath);
    const storagePath = `permanent/image/${randomUUID()}${ext}`;

    const { error } = await supabase.storage
        .from(BUCKET)
        .upload(storagePath, fileBuffer, { contentType, upsert: false });

    if (error)
        throw new Error(`이미지 업로드 실패 (${localPath}): ${error.message}`);

    const { data } = supabase.storage.from(BUCKET).getPublicUrl(storagePath);
    return data.publicUrl;
}

create-post.ts--file, --title, --hashtags, --dry-run CLI 인자 지원:

bash
# dry-run으로 먼저 확인
npx tsx scripts/blog-publish/create-post.ts \
  --file docs/my-post.md \
  --hashtags "next.js,cache,rendering" \
  --dry-run

# 실제 등록
npx tsx scripts/blog-publish/create-post.ts \
  --file /tmp/blog-publish-content.md \
  --title "글 제목" \
  --hashtags "next.js,cache,rendering"

/pwrc-review & /pwrc-compound: 루프에서 레슨 추출

리뷰를 두 번 돌렸고, 같은 카테고리의 문제가 반복됐습니다.

Review #1에서 발견:

  • String.replace()replaceAll() (동일 이미지 두 번째 참조 미교체 버그)
  • get-hashtags.ts, get-recent-posts.ts — CLI guard 없어 import 시 부수효과

Review #2에서 발견:

  • upload-images.ts — guard 패턴이 다른 스크립트와 불일치 (if (imagePath) vs fileURLToPath)

같은 "CLI guard 패턴 불일치" 문제가 두 번 나왔습니다. /pwrc-compound로 레슨을 추출했습니다.

markdown
## Prevention rule

- Work checkpoint: export 함수와 CLI 진입점을 함께 가진 스크립트를 작성할 때,
  동일 디렉터리의 모든 스크립트가 같은 guard 패턴을 사용하는지 확인한다.
  패턴: if (fileURLToPath(import.meta.url) === process.argv[1]) { ... }

이 레슨은 ~/.claude/lessons/ 에 저장되어 다음 /pwrc-plan 때 자동 로드됩니다. 같은 실수가 반복되지 않도록 프로젝트 기억이 쌓입니다.


완성된 파이프라인: /blog-publish

.claude/commands/blog-publish.md가 슬래시 커맨드로 등록됩니다.

/blog-publish docs/my-post.md

실행하면 Claude Code가 순서대로 처리합니다:

1. md 파일 읽기 → 제목 추출 (첫 줄 # 헤딩)
2. 최근 글 5개 조회 → 어투 분석 → 본문 자동 다듬기
3. 로컬 이미지 탐지 → Supabase Storage 업로드 → URL 치환
4. DB 해시태그 조회 → Claude 매칭 → 제안 출력

[해시태그 제안]
기존 태그: next.js, cache, rendering, performance ...
신규 태그: 없음

이 해시태그들로 등록할까요?

5. 승인 시: /tmp/blog-publish-content.md 저장 → create-post.ts CLI 호출 → 등록 완료

실제로 이 글 직전에 등록한 "Next.js 캐싱 계층 전체 흐름"이 이 파이프라인으로 발행된 첫 번째 글입니다.


보완: 썸네일 이미지 자동화

파이프라인을 쓰다 보니 아쉬운 점이 생겼습니다. 글 목록 페이지에서 썸네일이 없는 글은 어색하게 빈자리로 표시됩니다. 매번 별도로 이미지를 찾아 마크다운에 직접 붙여넣는 과정이 번거로웠습니다.

이번에도 /deep-interview로 먼저 요구사항을 좁혔습니다. 네 번의 질문-답변으로 결정됐습니다.

결정 항목선택검토한 대안이유
이미지 출처Unsplash APIGoogle Custom Search API (100건/일 제한), Pexels무료·저작권 프리, 검색 API 제공
Storage 전략Unsplash URL 직접 삽입리사이즈 후 Supabase 업로드 (완전 소유권, ~100–200KB/장)Supabase Storage 용량 부담 없음
선택 방식후보 3장 제시 → 번호 선택검색 결과 1위 자동 삽입 (완전 자동)썸네일은 글의 첫인상, 눈으로 한 번 확인이 낫다
검색어 생성제목 + 해시태그 자동 추출 + 재검색 옵션매번 사용자가 직접 입력해시태그 확정 후 검색하면 정확도가 높음

Storage 전략을 두고 잠깐 고민이 있었습니다. 이미지를 Supabase에 다운로드·업로드하면 완전히 소유할 수 있지만, Unsplash는 URL 파라미터로 리사이즈가 되어서 저장 용량 부담이 없습니다. 외부 URL이라 나중에 깨질 수 있지만, 그건 그때 직접 수정하면 된다고 판단했습니다.

검색어는 해시태그 확정 이후 단계에서 사용합니다. 해시태그가 확정되고 나서 검색어를 조합하면 더 정확한 결과를 얻을 수 있기 때문입니다. 결과가 마음에 안 들면 직접 검색어를 입력해 재검색할 수 있습니다.

.claude/commands/blog-publish.md에 4.5단계를 추가했습니다:

4단계: 해시태그 매칭 → 사용자 확인
4.5단계: 썸네일 검색 [신규]
  → 제목 + 확정 해시태그 → 영문 검색어 자동 생성
  → Unsplash API 호출 → 후보 3장 출력
  → 번호 선택 or 재검색 or skip
  → 선택 시 마크다운 맨 앞에 삽입 → extractThumbnail이 자동 인식
5단계: 글 등록

기존 extractThumbnail 함수는 마크다운에서 첫 번째 http 이미지 URL을 찾아 썸네일로 씁니다. 그래서 추가 코드 변경 없이 맨 앞에 이미지를 삽입하는 것만으로 연동됩니다.

물론 next.config.ts에서 새로운 도메인인 image.unsplash.com을 추가해야 합니다.

보완: 전역 커맨드로 전환

파이프라인을 쓰다 보니 또 다른 불편함이 생겼습니다. 글은 꼭 myblog 디렉터리에서만 쓰는 게 아닙니다. 다른 프로젝트를 작업하다가 문득 글감이 떠오르면, 그 자리에서 바로 발행하고 싶었습니다.

Claude Code 슬래시 커맨드는 두 가지 범위가 있습니다.

  • 프로젝트 커맨드: .claude/commands/*.md — 해당 프로젝트 안에서만 동작
  • 전역 커맨드: ~/.claude/commands/*.md — 어느 디렉터리에서든 동작

기존 .claude/commands/blog-publish.md~/.claude/commands/blog-publish.md로 복사하면 됩니다. 단, 커맨드 안의 모든 상대 경로(scripts/blog-publish/..., .env.local)를 myblog 절대경로 기반으로 바꿔야 합니다.

bash
# 변경 전 (프로젝트 내 상대경로)
npx tsx scripts/blog-publish/get-recent-posts.ts 5

# 변경 후 (전역 커맨드에서 절대경로)
cd /Users/kim-young-yin/Documents/codes/myblog && npx tsx scripts/blog-publish/get-recent-posts.ts 5

$ARGUMENTS로 받는 md 파일 경로도 상대경로일 수 있으므로, 호출 위치 기준으로 절대경로 변환을 명시해 두었습니다. 이제 어느 디렉터리에서든 /blog-publish ~/notes/my-post.md로 발행할 수 있습니다.

후기

몇 가지 느낀 점이 있습니다.

모호한 것을 먼저 좁혀야 한다. /deep-interview 없이 바로 "자동화 만들어줘"라고 했다면 내가 원하지 않는 방향으로 만들어졌을 겁니다. 질문에 답하는 과정에서 나도 몰랐던 내 판단 기준이 드러났습니다.

Review 루프는 Compound로 끊어야 한다. 같은 문제를 Work → Review → Work → Review로 반복하면 비용이 쌓입니다. /pwrc-compound는 반복 패턴에서 레슨을 추출해 저장합니다. 다음 프로젝트에서 자동으로 적용됩니다. 이는 제가 구현해놓았던 하네스 + 컴파운드 엔지니어링 스킬인 /pwrc-입니다.

설계 결정을 추적할 수 있다. 왜 Supabase를 직접 호출했는지, 왜 해시태그만 확인 단계를 뒀는지 — 대화 기록과 레슨 파일에 남아 있습니다. 나중에 수정할 때 맥락을 잃지 않습니다.

0개의 댓글
💬

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!