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)에 자동 실행되어:
RSS 8개 + 뉴스 API 2개에서 AI 관련 뉴스를 병렬 수집
URL 해시 + Jaccard 유사도 기반 중복 제거
출처 신뢰도(30%) + 최신성(30%) + 키워드 관련도(40%) 가중 스코어링으로 Top 5 선별
GPT-4o-mini로 한국어 헤드라인 + 2문장 요약 + "왜 중요한가" 생성
Satori + Sharp로 1080×1350px 카드뉴스 7장(표지 + 기사5 + 마무리) 렌더링
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
문서화
작업 방식
Claude Code에게 전체 요구사항을 한 번에 전달하고, 설계→구현 순서로 진행했다. 주요 분기점에서 3가지 질문을 받았다:
슬라이드 구성: 5장 vs 7장 → 7장(표지+본문5+마무리) 선택
이미지 생성: Puppeteer vs Satori+Sharp → Satori+Sharp 선택
인프라: 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 중복 제거 — 이중 방어
URL 해시:
SHA-256(normalizeUrl(url))→ DB UNIQUE 제약으로 같은 기사 재삽입 차단제목 유사도: 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
합계
~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 # 파이프라인 즉시 실행