title: SNS 자동 수집 파이프라인 (YouTube RSS + SNS) date: 2026-02-26 type: implementation-plan project: content-pipeline status: draft task_id: 947dc9b8-bd96-4134-a92f-f18be0327bfb repo: migkjy/ai-blog
SNS 자동 수집 파이프라인 구현 플랜
1. 배경 및 목적
현재 content-pipeline은 RSS 피드(17개 소스)에서만 AI 뉴스를 수집한다. CEO가 소비하는 YouTube 채널, Threads/Instagram 트렌드 등은 수동으로 확인해야 하므로 콘텐츠 커버리지에 공백이 생긴다.
목표: RSS 이외의 추가 소스(YouTube, Threads, Instagram)를 자동 수집하여 content-pipeline input을 확장한다.
2. 현재 아키텍처 분석
2.1 기존 수집 패턴 (src/pipeline/collect.ts)
FeedSource[] (17개 피드 목록)
→ rss-parser로 파싱 (10초 타임아웃, 피드당 최대 10건)
→ CollectedItem 인터페이스로 정규화
→ URL 정규화 + 제목 유사도 기반 중복 제거
→ saveCollectedNews()로 DB 저장 (ON CONFLICT DO NOTHING)
핵심 인터페이스:
interface FeedSource {
name: string;
url: string;
lang: "en" | "ko";
grade: "S" | "A" | "B";
category: "news" | "official" | "community" | "research";
}
interface CollectedItem {
title: string;
url: string;
source: string;
lang: string;
grade: string;
category: string;
summary: string | null;
content_snippet: string | null;
published_at: Date | null;
}
2.2 파이프라인 스테이지 구조
stage-collect.ts → collectNews() 호출 + pipeline_logs 기록
stage-generate.ts → AI 콘텐츠 생성
stage-publish.ts → 승인 후 발행
Cron: GET /api/cron/pipeline (매일 06:00 KST, 월~금)
- Stage 1(수집) → Stage 2(생성) 순차 실행
2.3 DB 스키마 (collected_news 테이블)
collected_news (
id, title, url UNIQUE, source, summary, content_snippet,
published_at, used_in_newsletter, created_at
)
2.4 2단계 필터링 프로세스 (rss-filter-process.md)
1단계: 경량 필터링 (키워드 매칭, 등급별 우선순위, 토큰 0) 2단계: AI 요약 생성 (1단계 통과분만)
새 수집 소스도 이 2단계 필터를 동일하게 통과시켜야 한다.
3. 구현 범위 및 우선순위
우선순위 기준: CEO 블로킹 없는 것 우선
| 순위 | 소스 | API 키 필요 | CEO 블로킹 | 구현 난이도 |
|---|---|---|---|---|
| 1 | YouTube 채널 RSS | 없음 | 없음 | 낮음 |
| 2 | YouTube 플레이리스트 RSS | 없음 | 없음 | 낮음 |
| 3 | YouTube Data API (좋아요/저장) | YOUTUBE_API_KEY | 있음 | 중간 |
| 4 | Threads/Instagram | 공식 API 제한 | 있음 | 높음 (mock) |
4. 태스크 분할
Task 1: YouTube RSS 수집기 구현 (collect-youtube.ts)
목표: YouTube 채널/플레이리스트 RSS를 기존 collect.ts 패턴과 동일하게 수집
구현 상세:
1-1. YouTube RSS 소스 목록 (YOUTUBE_FEEDS)
export interface YouTubeFeedSource {
name: string;
channelId?: string; // 채널 RSS용
playlistId?: string; // 플레이리스트 RSS용
lang: "en" | "ko";
grade: "S" | "A" | "B";
category: "tutorial" | "news" | "review" | "talk";
}
YouTube RSS URL 형식:
- 채널:
https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID} - 플레이리스트:
https://www.youtube.com/feeds/videos.xml?playlist_id={PLAYLIST_ID}
초기 채널 목록 (AI 관련 한/영 채널 5~8개):
- AI 관련 한국 채널: 노마드코더, 조코딩 등 (CEO 구독 확인 후 확정)
- 해외: Fireship, Two Minute Papers, AI Explained 등
1-2. collectYouTube() 함수
export async function collectYouTube(): Promise<CollectedItem[]> {
// 기존 rss-parser 재활용 (YouTube RSS = Atom feed, rss-parser 지원)
// 피드당 최대 5건 (영상은 RSS보다 빈도 낮음)
// CollectedItem으로 정규화:
// - title: 영상 제목
// - url: 영상 URL (https://www.youtube.com/watch?v=XXX)
// - source: 채널명
// - category: youtube_feed.category
// - summary: 영상 description 첫 500자
// - content_snippet: null (영상은 텍스트 본문 없음)
// - published_at: pubDate
}
1-3. 기존 collectNews()에 통합 OR 별도 호출
방안 A (추천): stage-collect.ts에서 collectNews() + collectYouTube() 병렬 호출 후 합산
- 기존
collect.ts코드 변경 최소화 - YouTube 수집 실패해도 RSS 수집에 영향 없음
// stage-collect.ts 수정
const [rssItems, ytItems] = await Promise.all([
collectNews(),
collectYouTube(),
]);
const allItems = [...rssItems, ...ytItems];
const saved = await saveCollectedNews(allItems);
방안 B: collect.ts의 RSS_FEEDS에 YouTube RSS URL 직접 추가
- 가장 간단하지만 YouTube 특유의 메타데이터(channelId 등) 활용 불가
- YouTube RSS 실패 시 일반 RSS와 동일하게 처리됨
결정: 방안 A -- 모듈 분리가 향후 YouTube API 통합 시에도 유리
1-4. collected_news 테이블 활용
기존 collected_news 테이블을 그대로 사용한다:
source컬럼에 "YouTube: {채널명}" 형태로 저장urlUNIQUE 제약으로 중복 영상 자동 방지- 새 컬럼 추가 불필요 (MVP 원칙)
산출물: src/pipeline/collect-youtube.ts
예상 코드량: ~120줄
CEO 블로킹: 없음
Task 2: stage-collect.ts 통합 및 Cron 파이프라인 수정
목표: YouTube 수집을 기존 파이프라인 스테이지에 통합
구현 상세:
2-1. stage-collect.ts 수정
import { collectNews, saveCollectedNews } from './collect';
import { collectYouTube } from './collect-youtube';
export async function runCollectStage(triggerType): Promise<CollectResult> {
// ... 기존 pipeline_logs 기록 로직 유지
// 병렬 수집
const [rssItems, ytItems] = await Promise.allSettled([
collectNews(),
collectYouTube(),
]);
const allItems = [
...(rssItems.status === 'fulfilled' ? rssItems.value : []),
...(ytItems.status === 'fulfilled' ? ytItems.value : []),
];
const saved = await saveCollectedNews(allItems);
// ... 기존 로깅/통계 로직
// metadata에 youtube 통계 추가
const metadata = {
rss_items: rssItems.status === 'fulfilled' ? rssItems.value.length : 0,
youtube_items: ytItems.status === 'fulfilled' ? ytItems.value.length : 0,
saved_items: saved,
};
}
2-2. Cron 엔드포인트는 변경 불필요
/api/cron/pipeline는 runCollectStage()만 호출하므로, stage-collect 수정만으로 자동 반영.
2-3. CLI 스크립트 추가
// package.json scripts
"pipeline:collect:youtube": "tsx src/pipeline/collect-youtube.ts"
산출물: stage-collect.ts 수정, package.json 스크립트 추가
예상 코드량: ~30줄 수정
CEO 블로킹: 없음
Task 3: YouTube 채널 목록 설정 파일
목표: CEO가 쉽게 채널을 추가/제거할 수 있도록 설정 분리
구현 상세:
3-1. src/config/youtube-channels.ts 생성
import { YouTubeFeedSource } from '../pipeline/collect-youtube';
export const YOUTUBE_CHANNELS: YouTubeFeedSource[] = [
// === AI/Tech 한국어 채널 ===
{
name: "노마드코더",
channelId: "UCUpJs89fSBXNolQGOYKn0YQ",
lang: "ko",
grade: "A",
category: "tutorial",
},
{
name: "조코딩",
channelId: "UCQNE2JmbasNYbjGAcuBiRRg",
lang: "ko",
grade: "A",
category: "tutorial",
},
// === AI/Tech 영어 채널 ===
{
name: "Fireship",
channelId: "UCsBjURrPoezykLs9EqgamOA",
lang: "en",
grade: "S",
category: "news",
},
{
name: "Two Minute Papers",
channelId: "UCbfYPyITQ-7l4upoX8nvctg",
lang: "en",
grade: "A",
category: "review",
},
{
name: "AI Explained",
channelId: "UCNJ1Ymd5yFuUPtn21xtRbbw",
lang: "en",
grade: "S",
category: "news",
},
{
name: "Matt Wolfe",
channelId: "UCJIBYMRMEfqVMGJJgWVAmnw",
lang: "en",
grade: "A",
category: "review",
},
];
3-2. CEO가 채널 추가하는 방법
해당 파일에 객체 추가만 하면 됨. channelId는 YouTube 채널 페이지 소스에서 확인 가능.
향후 UI 대시보드에서 관리 가능 (Phase 2).
산출물: src/config/youtube-channels.ts
예상 코드량: ~60줄
CEO 블로킹: 채널 목록 확정 (확정 전까지 기본 목록으로 동작)
Task 4: Threads/Instagram Mock 인터페이스 + 향후 확장점
목표: SNS 수집 확장 포인트만 마련 (실제 구현은 API 확보 후)
구현 상세:
4-1. 현실적 대안 분석
| 플랫폼 | 공식 API | 대안 | 실현 가능성 |
|---|---|---|---|
| Threads | 없음 (Meta Basic API만) | 해시태그 RSS 서비스, 웹 크롤링 | 낮음 (ToS 위반 위험) |
| Graph API (비즈니스만) | 해시태그 RSS, 공개 프로필 | 중간 (인증 복잡) | |
| X/Twitter | API v2 (유료) | RSS 서비스 (Nitter 등 불안정) | 중간 (비용 발생) |
4-2. collect-sns.ts Mock 인터페이스
export interface SnsFeedSource {
name: string;
platform: "threads" | "instagram" | "twitter";
identifier: string; // 계정 핸들 또는 해시태그
lang: "en" | "ko";
grade: "S" | "A" | "B";
category: string;
}
export async function collectSns(): Promise<CollectedItem[]> {
// Phase 1: Mock 반환 (빈 배열)
// Phase 2: 실제 API/크롤링 구현 시 채워넣기
console.log('[collect-sns] SNS 수집은 Phase 2에서 구현 예정');
return [];
}
4-3. stage-collect.ts에 미리 연결
const [rssItems, ytItems, snsItems] = await Promise.allSettled([
collectNews(),
collectYouTube(),
collectSns(), // Phase 1: 빈 배열 반환
]);
이렇게 하면 향후 SNS API가 확보되었을 때 collectSns() 내부만 채우면 된다.
산출물: src/pipeline/collect-sns.ts (mock)
예상 코드량: ~40줄
CEO 블로킹: Threads/Instagram API 접근 방법 결정 (Phase 2)
Task 5: 테스트 및 검증
목표: YouTube RSS 수집이 정상 동작하는지 검증
구현 상세:
5-1. CLI 단독 실행 테스트
# YouTube 수집만 단독 실행
npx tsx src/pipeline/collect-youtube.ts
# 기대 출력:
# [collect-youtube] Fetching: Fireship ...
# [collect-youtube] Fireship: 5 items
# [collect-youtube] Summary: 6/6 channels OK, 0 failed, 28 raw → 25 after dedup
5-2. 통합 파이프라인 테스트
# stage-collect 전체 실행 (RSS + YouTube)
npx tsx -e "import { runCollectStage } from './src/pipeline/stage-collect'; runCollectStage('manual').then(r => console.log(JSON.stringify(r, null, 2)))"
5-3. 검증 항목
- YouTube RSS 파싱 정상 (rss-parser가 Atom feed 처리)
- CollectedItem 정규화 정상 (title, url, source 등)
- 중복 제거 정상 (deduplicateNews 재활용)
- DB 저장 정상 (collected_news에 youtube 소스 포함)
- 기존 RSS 수집에 영향 없음 (Promise.allSettled 격리)
- YouTube 피드 실패 시 RSS만으로 정상 동작
산출물: 테스트 실행 로그 CEO 블로킹: 없음
5. 파일 변경 요약
| 파일 | 변경 유형 | 설명 |
|---|---|---|
src/pipeline/collect-youtube.ts | 신규 | YouTube RSS 수집기 |
src/config/youtube-channels.ts | 신규 | YouTube 채널 목록 |
src/pipeline/collect-sns.ts | 신규 | SNS 수집 mock 인터페이스 |
src/pipeline/stage-collect.ts | 수정 | YouTube + SNS 수집 통합 |
package.json | 수정 | CLI 스크립트 추가 |
기존 코드 변경 최소화 원칙 준수: collect.ts는 변경하지 않음.
6. CEO 블로킹 항목
| 항목 | 블로킹 대상 | 대안 |
|---|---|---|
| YouTube 채널 목록 확정 | Task 3 (설정) | 기본 AI 채널 6개로 시작 가능 |
| YOUTUBE_API_KEY | Task 없음 (이번 범위 밖) | RSS로 대체, API 불필요 |
| Threads/Instagram API | Task 4 (mock만) | Phase 2로 이관 |
핵심: Task 1~3은 CEO 블로킹 없이 즉시 구현 가능
7. 의존성
- 새로운 npm 패키지 없음 (기존
rss-parser재활용) - DB 스키마 변경 없음 (기존
collected_news테이블 그대로 사용) - 환경변수 추가 없음
8. 리스크 및 완화
| 리스크 | 확률 | 완화 |
|---|---|---|
| YouTube RSS 응답 지연/차단 | 낮음 | 10초 타임아웃 + Promise.allSettled 격리 |
| rss-parser가 YouTube Atom feed 미지원 | 매우 낮음 | rss-parser v3.13은 Atom 지원 확인됨 |
| 수집량 증가로 토큰 낭비 | 중간 | rss-filter-process 2단계 필터를 YouTube에도 동일 적용 |
| YouTube 채널 ID 오류 | 낮음 | 수집 시 개별 피드 실패는 skip 처리 |
9. 예상 효과
- 콘텐츠 소스 17개 → 23
25개로 확장 (YouTube 68개 추가) - YouTube AI 콘텐츠 트렌드를 블로그/뉴스레터에 반영 가능
- CEO 블로킹 없이 즉시 실행 가능한 인프라 확장
- 향후 SNS 소스 추가 시 collect-sns.ts 내부만 구현하면 됨