AI 뉴스 → 인스타 캐러셀: Claude Code로 풀스택 자동화 MVP를 하루 만에 구축한 기록

TL;DR

  • 목표: AI 뉴스를 매일 자동 수집 → 정제 → 한국어 요약 → 카드뉴스 7장 렌더링 → Instagram 캐러셀 업로드하는 파이프라인 구축

  • 결과: 50+개 파일, 6단계 파이프라인, Next.js 관리 대시보드까지 포함한 풀스택 MVP를 하루 만에 완성

  • 스택: Turborepo + Next.js 15 + Node.js Worker + Supabase + Satori/Sharp + OpenAI + Instagram Graph API

  • 도구: Claude Code (Claude Opus 4.5)


1. 프로젝트 개요

무엇을 만들었나

"AI News → Instagram Carousel Auto Publisher" — 매일 오전 7시(KST)에 자동 실행되어:

  1. RSS 8개 + 뉴스 API 2개에서 AI 관련 뉴스를 병렬 수집

  2. URL 해시 + Jaccard 유사도 기반 중복 제거

  3. 출처 신뢰도(30%) + 최신성(30%) + 키워드 관련도(40%) 가중 스코어링으로 Top 5 선별

  4. GPT-4o-mini로 한국어 헤드라인 + 2문장 요약 + "왜 중요한가" 생성

  5. Satori + Sharp로 1080×1350px 카드뉴스 7장(표지 + 기사5 + 마무리) 렌더링

  6. Instagram Graph API로 캐러셀 업로드 (실패 시 2회 자동 재시도)

까지의 전 과정을 자동으로 처리하는 시스템이다.

왜 만들었나

자동화 실험이 목적이었다. AI 코딩 도구가 실제로 "수집 → 가공 → 렌더링 → 외부 API 연동"이라는 다단계 파이프라인을 하나의 세션에서 완성할 수 있는지 직접 검증해보고 싶었다.


2. 아키텍처

┌─────────────────────────────────────────────────────┐
│  Turborepo Monorepo (pnpm workspace)                │
│                                                     │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │ apps/admin   │  │ apps/worker  │  │ packages/ │  │
│  │ Next.js 15   │  │ Node.js+tsx  │  │ core      │  │
│  │ App Router   │  │ node-cron    │  │ types     │  │
│  │ Tailwind v4  │  │ 6-step pipe  │  │ DB layer  │  │
│  │ 관리 대시보드 │  │ 파이프라인    │  │ scoring   │  │
│  └──────┬───────┘  └──────┬───────┘  │ dedup     │  │
│         │                 │          └─────┬─────┘  │
│         └────────┬────────┘                │        │
│                  ▼                         │        │
│         ┌────────────────┐                 │        │
│         │   Supabase     │◄────────────────┘        │
│         │ Postgres + S3  │                          │
│         └────────────────┘                          │
└─────────────────────────────────────────────────────┘
           │                    │
           ▼                    ▼
   Instagram Graph API    OpenAI API (GPT-4o-mini)

기술 선택 근거

선택

이유

대안 (기각 사유)

Satori + Sharp

서버사이드 JSX→SVG→PNG, 별도 브라우저 불필요

Canva API (Enterprise 전용), Puppeteer (무거움)

Turborepo

core 패키지를 worker/admin이 공유, 빌드 캐싱

Nx (설정 복잡), 단일 레포 (타입 공유 어려움)

Supabase

Postgres + Storage + Auth 통합, 무료 티어

PlanetScale (Storage 별도), 직접 호스팅 (관리 부담)

node-cron

경량, 프로세스 내 스케줄링

BullMQ (오버스펙), 외부 cron (인프라 의존)


3. 개발 과정 — Claude Code와의 협업

전체 타임라인

전체 개발이 하나의 Claude Code 세션에서 이루어졌다. 크게 13단계로 나뉜다.

#

단계

주요 작업

1

리서치

RSS 소스 8개, IG Graph API 요구사항, 이미지 생성 방식 비교

2

설계 확정

슬라이드 7장, Satori+Sharp, Supabase 확정

3

Root 설정

Turborepo, pnpm workspace, tsconfig, env template

4

packages/core

5개 타입 파일, DB CRUD, 스코어링, 중복제거, 유틸리티

5

DB 마이그레이션

4 테이블 + 8 인덱스 SQL

6

수집기

RSS, GNews, NewsData.io 클라이언트 3개

7

파이프라인

6단계 오케스트레이터 (collect→dedup→score→summarize→render→publish)

8

카드 템플릿

Satori JSX 템플릿 7종 + 폰트 로더 + 10MB 한국어 폰트

9

IG 퍼블리셔

Graph API v21.0 캐러셀 게시 + 캡션 + 토큰 관리

10

스케줄러

node-cron + --run-now 수동 실행

11

Admin 대시보드

Next.js 4페이지 + 4개 API 라우트 + 컴포넌트

12

빌드 수정

TypeScript strict 모드 타입 에러 수정

13

문서화

README.md

작업 방식

Claude Code에게 전체 요구사항을 한 번에 전달하고, 설계→구현 순서로 진행했다. 주요 분기점에서 3가지 질문을 받았다:

  1. 슬라이드 구성: 5장 vs 7장 → 7장(표지+본문5+마무리) 선택

  2. 이미지 생성: Puppeteer vs Satori+Sharp → Satori+Sharp 선택

  3. 인프라: Supabase 사용 가능 여부 확인

이후 계획서가 나오고, 승인하면 순차적으로 코드를 생성해나갔다.


4. 기술적 디테일

4.1 스코어링 알고리즘

score = (source_weight × 30) + (recency_score × 30) + (keyword_score × 40)
  • source_weight: Tier1(1.0) OpenAI·MIT / Tier2(0.8) TechCrunch·Verge / Tier3(0.6) HuggingFace / Tier4(0.4) API 결과

  • recency_score: <6h=1.0 / <12h=0.8 / <24h=0.5 / <48h=0.2

  • keyword_score: 고가치(GPT, LLM, transformer)=1.0 / 중가치(NLP, CV)=0.6 / 저가치(AI)=0.3

4.2 중복 제거 — 이중 방어

  1. URL 해시: SHA-256(normalizeUrl(url)) → DB UNIQUE 제약으로 같은 기사 재삽입 차단

  2. 제목 유사도: Jaccard(tokenize(title_A), tokenize(title_B)) > 0.6이면 중복 마킹

URL 정규화 시 utm_*, fbclid, gclid 등 추적 파라미터를 제거하고, 쿼리 파라미터를 정렬한다.

4.3 Satori 카드 렌더링

Satori는 제약이 많다:

  • Flexbox만 지원 (CSS Grid 불가), 모든 div에 display: 'flex' 명시

  • TTF/OTF만 지원 (WOFF2 불가)

  • 상태 없는 순수 JSX만 가능 (React hooks 불가)

  • 한국어 텍스트는 lineHeight 명시 필수

디자인 시스템:

배경: #0F0F0F (다크) / 카드면: #1A1A2E
강조: #6C63FF(보라) ↔ #00D4AA(틸) 그라디언트
텍스트: #FFFFFF(주) / #B0B0C0(부)
폰트: Noto Sans KR Variable (10MB, 한국어 전체 글리프)
크기: 1080 × 1350px (Instagram 캐러셀 최적 비율)

4.4 Instagram Graph API 캐러셀 게시 플로우

이미지 7장 → 각각 child container 생성 (POST /{user-id}/media)
         → 각 container 상태 polling (GET /{container-id}?fields=status_code)
         → FINISHED 될 때까지 대기
         → carousel container 생성 (POST /{user-id}/media, children=[...])
         → carousel 상태 polling
         → media_publish (POST /{user-id}/media_publish)
         → permalink 조회 (GET /{media-id}?fields=permalink)

실패 시 exponential backoff (5s → 10s)로 최대 2회 재시도한다.

4.5 DB 스키마

4개 테이블:

  • articles: 수집된 원본 기사 (url_hash UNIQUE로 중복 방지)

  • summaries: GPT 생성 한국어 요약 (article_id FK, slide_index)

  • posts: Instagram 게시물 (batch_date UNIQUE — 하루 1포스트 보장)

  • pipeline_logs: 단계별 실행 로그 (step, status, duration_ms)


5. 빌드 과정에서 만난 문제들

5.1 한국어 폰트 — 300KB vs 10MB

처음 다운로드한 NotoSansKR-Regular.ttf(298KB)는 Latin 서브셋이었다. 한국어 글리프가 포함되지 않아 카드뉴스에서 한글이 렌더링되지 않는다. NotoSansKR-Variable.ttf(10.4MB)로 교체하여 해결했다.

교훈: Google Fonts에서 폰트를 다운받을 때, 파일 크기로 한국어/CJK 글리프 포함 여부를 판단할 수 있다. 한국어 전체 글리프는 최소 수 MB 이상이어야 한다.

5.2 TypeScript strict 모드 — response.json() returns unknown

fetch() 응답의 .json()은 TypeScript strict 모드에서 unknown을 반환한다. 5개 이상의 파일에서 as Type 타입 단언이 필요했다.

// Before (컴파일 에러)
const data = await response.json();
data.articles // ❌ 'unknown' 형식에 'articles' 속성이 없습니다

// After
const data = (await response.json()) as GNewsResponse;
data.articles // ✅

5.3 서버 컴포넌트에서 confirm() 사용 불가

Admin 대시보드의 게시 버튼에서 confirm("게시하시겠습니까?") 호출 시, 서버 TypeScript 환경에 DOM 타입이 없어 빌드 실패. globalThis.confirm도 동일. 결국 confirm 다이얼로그를 제거하여 해결했다.

5.4 Canva API — Enterprise 전용

초기 리서치에서 Canva API를 이미지 생성 후보로 고려했으나, Enterprise 플랜 전용으로 확인되어 기각. Satori + Sharp 조합이 무료이면서도 서버사이드에서 고품질 PNG를 생성할 수 있어 적합했다.


6. 최종 산출물

파일 구성

영역

파일 수

주요 파일

Root 설정

6

package.json, turbo.json, tsconfig.json, .env.example, .gitignore, pnpm-workspace.yaml

packages/core

18

types(5), utils(4), constants(3), dedup(3), scoring(3), db(5), index.ts

supabase

1

001_initial.sql (4 tables, 8 indexes)

apps/worker

17

collectors(3), pipeline(6), openai(2), instagram(3), templates(5), cron(1), index.ts

apps/admin

15

pages(5), api routes(4), components(4), lib(2)

문서

2

README.md, DEVLOG.md

합계

~59

의존성

// 주요 프로덕션 의존성
"@supabase/supabase-js"  // DB + Storage
"openai"                  // GPT-4o-mini 요약
"satori"                  // JSX → SVG
"sharp"                   // SVG → PNG
"rss-parser"              // RSS 피드 파싱
"node-cron"               // 스케줄링
"pino"                    // 구조화된 로깅
"zod"                     // 런타임 검증
"next"                    // Admin 대시보드
"lucide-react"            // 아이콘

7. 소감

잘 된 점

  • 하루 만에 풀스택 MVP 완성: 리서치 → 설계 → 구현 → 빌드 → 문서화까지 하나의 세션에서 끝났다. 수작업으로 했다면 몇 배의 시간이 걸렸을 것이다.

  • 모노레포 구조 자동 설계: Turborepo + pnpm workspace 설정, 패키지 간 타입 공유, barrel export 구조까지 체계적으로 구성되었다.

  • 외부 API 통합: Instagram Graph API의 캐러셀 게시 플로우(child container → polling → carousel → publish)처럼 다단계 API 호출도 재시도 로직까지 포함해서 구현되었다.

아쉬운 점

  • 빌드 에러 반복: TypeScript strict 모드의 unknown 타입 이슈가 여러 파일에서 반복적으로 발생했다. 처음부터 타입 단언을 적용했으면 빌드 수정 단계가 불필요했을 것이다.

  • 폰트 크기 판단 실수: 300KB 서브셋 폰트를 먼저 다운받고 나서야 한국어 미지원을 확인했다. 한국어 프로젝트에서는 처음부터 풀 글리프 폰트를 사용해야 한다.

  • 실제 운영 검증 미완: API 키와 인스타그램 토큰이 없는 상태에서 빌드 성공까지만 확인했다. E2E 테스트는 실제 환경 설정 후 별도로 진행해야 한다.


8. 프로젝트 구조 요약

c:\node\새 폴더\
├── apps/
│   ├── admin/                  # Next.js 15 관리 대시보드
│   │   └── src/
│   │       ├── app/            # 4페이지 + 4 API 라우트
│   │       ├── components/     # 사이드바, 파이프라인 버튼
│   │       └── lib/            # Supabase 클라이언트
│   └── worker/                 # Node.js 파이프라인 워커
│       └── src/
│           ├── pipeline/       # 6단계 파이프라인
│           ├── collectors/     # RSS + API 수집기
│           ├── openai/         # GPT 요약 생성
│           ├── instagram/      # IG Graph API 연동
│           ├── templates/      # Satori 카드 템플릿 7종
│           └── cron/           # 스케줄러
├── packages/
│   └── core/                   # 공유 타입, DB, 스코어링, 중복제거
├── supabase/
│   └── migrations/             # DB 스키마 SQL
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

부록: 환경 설정 가이드

# 1. 의존성 설치
pnpm install

# 2. 환경변수 설정
cp .env.example .env
# SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENAI_API_KEY,
# INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_USER_ID 등 입력

# 3. Supabase SQL Editor에서 마이그레이션 실행
# supabase/migrations/001_initial.sql

# 4. Supabase Storage에 card-images 버킷 생성 (Public)

# 5. 개발 서버 실행
pnpm dev                           # admin + worker 동시
pnpm --filter @repo/worker run-now # 파이프라인 즉시 실행
1
1개의 답글
지피터스 뉴스레터

이번 주 AI, 딱 쓸 것만

매주 월요일 오후,
읽고 바로 써먹을 AI 소식만