# ThreadAuto Remotion 마이그레이션 초안

**작성일:** 2026-03-10
**작성팀:** dev1 (헤르메스 팀장)
**관련 태스크:** task-434.1
**기반 리서치:** remotion-deep-dive.md

---

## 1. 마이그레이션 전략: 카드뉴스 먼저

### 1.1 순서 결정 근거

| 항목 | 카드뉴스 먼저 | 영상 먼저 |
|------|------------|----------|
| 복잡도 | 낮음 (정지 이미지) | 높음 (프레임 애니메이션) |
| 검증 속도 | 빠름 (renderStill 2~10초) | 느림 (renderMedia 1~3분) |
| 리스크 | 낮음 (Pillow 병행 가능) | 높음 (MoviePy 전면 교체) |
| 비즈니스 임팩트 | 높음 (매일 카드뉴스 발행중) | 중간 (영상은 아직 미발행) |

**결론: 카드뉴스 → 영상 순서로 진행**

### 1.2 Phase 개요

```
Phase 0: 환경 셋업 (1~2일)
Phase 1: POC - 카드뉴스 1종 (1~2주)
Phase 2: 카드뉴스 전체 전환 (3~4주)
Phase 3: 영상 파이프라인 전환 (3~4주)
Phase 4: 최적화 + 고급 효과 (2~3주)
```

---

## 2. Phase 0: 환경 셋업 (1~2일)

### 2.1 시스템 패키지 설치

```bash
# Ubuntu Chromium 의존성
sudo apt-get install -y \
  libnss3 libdbus-1-3 libatk1.0-0 libasound2t64 \
  libxrandr2 libxkbcommon-dev libxfixes3 libxcomposite1 libxdamage1 \
  libgbm-dev libcups2 libcairo2 libpango-1.0-0 libatk-bridge2.0-0 \
  fonts-noto-cjk fonts-noto-color-emoji
```

### 2.2 Remotion 프로젝트 초기화

```bash
cd /home/jay/projects/ThreadAuto
mkdir remotion && cd remotion

# package.json 생성
npm init -y
npm install remotion @remotion/cli @remotion/bundler @remotion/renderer
npm install -D typescript @types/react @types/react-dom

# Chrome 사전 다운로드
npx remotion browser ensure
```

### 2.3 필요한 패키지/의존성

**Node.js 패키지:**

```json
{
  "dependencies": {
    "remotion": "^4.0",
    "@remotion/cli": "^4.0",
    "@remotion/bundler": "^4.0",
    "@remotion/renderer": "^4.0",
    "@remotion/google-fonts": "^4.0",
    "@remotion/lottie": "^4.0",
    "@remotion/transitions": "^4.0",
    "react": "^18.0",
    "react-dom": "^18.0",
    "express": "^4.18",
    "uuid": "^9.0"
  },
  "devDependencies": {
    "typescript": "^5.0",
    "@types/react": "^18.0",
    "@types/react-dom": "^18.0",
    "@types/express": "^4.17"
  }
}
```

**Python 패키지 (추가):**

```
requests>=2.31.0  # RemotionClient HTTP 통신 (이미 설치됨)
```

---

## 3. 템플릿 구조 설계 (보험 콘텐츠 특화)

### 3.1 디렉토리 구조

```
ThreadAuto/
├── remotion/                    ← 신규 Remotion 프로젝트
│   ├── package.json
│   ├── tsconfig.json
│   ├── remotion.config.ts       ← CLI 설정 (멀티프로세스 등)
│   ├── src/
│   │   ├── index.ts             ← 엔트리포인트
│   │   ├── Root.tsx             ← Composition 등록
│   │   ├── compositions/        ← 최상위 컴포지션
│   │   │   ├── CardNews.tsx     ← 카드뉴스 (Still)
│   │   │   ├── ShortForm.tsx    ← 숏폼 영상 (Video)
│   │   │   └── Reel.tsx         ← 릴스 (Video)
│   │   ├── slides/              ← 슬라이드 타입별 컴포넌트
│   │   │   ├── CoverSlide.tsx
│   │   │   ├── CardListSlide.tsx
│   │   │   ├── DetailSlide.tsx
│   │   │   ├── CtaSlide.tsx
│   │   │   └── BodySlide.tsx
│   │   ├── components/          ← 공통 UI 컴포넌트
│   │   │   ├── BrandBar.tsx
│   │   │   ├── BrandFooter.tsx
│   │   │   ├── GlassCard.tsx
│   │   │   ├── PillBadge.tsx
│   │   │   ├── InfoBox.tsx
│   │   │   ├── CountUp.tsx
│   │   │   └── AnimatedChart.tsx
│   │   ├── themes/              ← 테마 시스템
│   │   │   ├── types.ts
│   │   │   ├── navy-gold.ts
│   │   │   ├── forest.ts
│   │   │   ├── coral.ts
│   │   │   ├── slate.ts
│   │   │   └── midnight.ts
│   │   ├── animations/          ← 애니메이션 유틸
│   │   │   ├── fadeIn.ts
│   │   │   ├── slideIn.ts
│   │   │   ├── countUp.ts
│   │   │   └── stagger.ts
│   │   └── utils/
│   │       ├── fonts.ts
│   │       └── props.ts         ← inputProps 타입 정의
│   └── render-server/           ← Express.js 렌더 서버
│       ├── server.ts
│       └── routes/
│           └── renders.ts
├── renderer/                    ← 기존 Pillow (Phase 2까지 병행)
│   ├── remotion_adapter.py      ← JSON → Remotion props 변환
│   ├── remotion_client.py       ← HTTP API 클라이언트
│   └── ...existing files...
├── video/                       ← 기존 MoviePy (Phase 3까지 병행)
└── ...
```

### 3.2 테마 시스템 매핑

현재 Python themes.py의 5종 테마 → TypeScript 인터페이스로 변환:

```typescript
// remotion/src/themes/types.ts
export interface Theme {
  name: string;
  primary: string;
  accent: string;
  bg_gradient: [string, string];
  text_primary: string;
  text_secondary: string;
  card_bg: string;
  border_color: string;
  // 추가 가능
  cover_gradient?: string;
  glass_bg?: string;
}

// remotion/src/themes/navy-gold.ts
export const NavyGold: Theme = {
  name: "NavyGold",
  primary: "#1B2A4A",
  accent: "#C8A951",
  bg_gradient: ["#0F1A2E", "#1B2A4A"],
  text_primary: "#FFFFFF",
  text_secondary: "#A0AEC0",
  card_bg: "rgba(255, 255, 255, 0.06)",
  border_color: "rgba(200, 169, 81, 0.3)",
};
```

### 3.3 슬라이드 컴포넌트 설계

```tsx
// remotion/src/compositions/CardNews.tsx
import { Still } from 'remotion';

// Root.tsx에서 각 슬라이드를 별도 Still로 등록
// → renderStill() 호출 시 slideIndex로 특정 슬라이드 렌더링

<Still id="CardNews" component={CardNewsStill} width={1080} height={1350}
  defaultProps={{
    slides: [],
    theme: NavyGold,
    slideIndex: 0,
  }}
/>

// CardNewsStill 컴포넌트
const CardNewsStill: React.FC<CardNewsProps> = ({ slides, theme, slideIndex }) => {
  const slide = slides[slideIndex];
  const SlideComponent = SLIDE_MAP[slide.type];  // cover → CoverSlide 등

  return (
    <AbsoluteFill style={{
      background: `linear-gradient(180deg, ${theme.bg_gradient[0]}, ${theme.bg_gradient[1]})`,
      fontFamily: 'Pretendard, Noto Sans KR, sans-serif',
    }}>
      <BrandBar color={theme.primary} />
      <SlideComponent slide={slide} theme={theme} />
      <BrandFooter text="인카다이렉트 TOP사업단" color={theme.primary} />
    </AbsoluteFill>
  );
};

const SLIDE_MAP = {
  cover: CoverSlide,
  card_list: CardListSlide,
  detail: DetailSlide,
  cta: CtaSlide,
  body: BodySlide,
};
```

---

## 4. 기존 파이프라인과의 연결점

### 4.1 현재 파이프라인 흐름

```
topic_selector → crawler → content_generator_v2 → compliance_filter
→ daily_queue/{date}.json → renderer/cardnews.py → publisher
```

### 4.2 Remotion 연결 후 파이프라인

```
topic_selector → crawler → content_generator_v2 → compliance_filter
→ daily_queue/{date}.json
→ renderer/remotion_adapter.py (JSON 변환)
→ renderer/remotion_client.py (HTTP API 호출)
→ Remotion render-server (Node.js, localhost:3000)
→ output/cardnews_{timestamp}_{idx}.png
→ publisher
```

### 4.3 핵심 연결 파일

**renderer/remotion_adapter.py** (신규):

```python
"""daily_queue JSON을 Remotion inputProps로 변환하는 어댑터."""

from renderer.themes import Theme, get_theme

def slides_to_remotion_props(content: dict, theme: Theme) -> dict:
    """content_generator_v2 출력을 Remotion inputProps로 변환."""
    slides = content.get("slides", [])
    total = len(slides)
    counter = 0
    enriched = []
    for i, slide in enumerate(slides):
        s = dict(slide)
        s["page_num"] = i + 1
        s["total_pages"] = total
        if s.get("type") != "cover":
            counter += 1
            s["content_index"] = counter
        enriched.append(s)
    return {
        "slides": enriched,
        "theme": {
            "name": theme.name,
            "primary": theme.primary,
            "accent": theme.accent,
            "bg_gradient": list(theme.bg_gradient),
            "text_primary": theme.text_primary,
            "text_secondary": theme.text_secondary,
            "card_bg": theme.card_bg,
            "border_color": theme.border_color,
        },
        "caption": content.get("caption", ""),
        "hashtags": content.get("hashtags", []),
    }
```

**run_full_pipeline.py 분기 로직** (수정):

```python
USE_REMOTION = os.getenv("REMOTION_RENDER", "0") == "1"

if USE_REMOTION:
    from renderer.remotion_adapter import slides_to_remotion_props
    from renderer.remotion_client import RemotionClient
    client = RemotionClient()
    image_paths = []
    for idx, slide in enumerate(content["slides"]):
        props = slides_to_remotion_props(content, theme)
        props["slideIndex"] = idx
        result = client.render_still("CardNews", props)
        image_paths.append(result.output_path)
else:
    # 기존 Pillow 렌더링
    image_paths = [str(p) for p in renderer.render_from_slides(...)]
```

### 4.4 publisher 영향 없음

`publisher/threads_publisher.py`는 `image_paths` 리스트만 받으므로, Remotion 출력 경로를 그대로 전달하면 변경 불필요.

---

## 5. Phase별 마이그레이션 상세 계획

### Phase 1: POC - 카드뉴스 cover 1종 (1~2주)

**목표:** Remotion이 동일한 카드뉴스를 생성할 수 있는지 검증

**마이크로태스크:**

| # | 작업 | 담당 | 시간 | 결과물 |
|---|------|------|------|--------|
| 1.1 | Remotion 프로젝트 초기화 + tsconfig + remotion.config.ts | 이리스 | 2h | remotion/ 디렉토리 |
| 1.2 | NavyGold 테마 TypeScript 정의 | 이리스 | 1h | themes/navy-gold.ts |
| 1.3 | CoverSlide 컴포넌트 구현 | 이리스 | 4h | slides/CoverSlide.tsx |
| 1.4 | BrandBar + BrandFooter 공통 컴포넌트 | 이리스 | 2h | components/ |
| 1.5 | render-server Express 세팅 | 불칸 | 4h | render-server/ |
| 1.6 | remotion_adapter.py 작성 | 불칸 | 2h | renderer/remotion_adapter.py |
| 1.7 | remotion_client.py 작성 | 불칸 | 2h | renderer/remotion_client.py |
| 1.8 | Pillow vs Remotion 시각적 비교 테스트 | 아르고스 | 2h | 비교 보고서 |
| 1.9 | 시스템 패키지 설치 + 서버 환경 검증 | 불칸 | 1h | 설치 스크립트 |

**테스트:** daily_queue JSON 1개 → Pillow 출력 vs Remotion 출력 시각적 비교

### Phase 2: 카드뉴스 전체 전환 (3~4주)

**목표:** 5종 테마 × 5개 슬라이드 타입 완전 구현

**슬라이드 타입 전환 순서 (난이도 순):**

1. **cover** → 그라데이션 배경, Pill badge, 수직 공간 분배
2. **cta** (summary_cta) → 글래스카드, CTA 배너
3. **body** → 단일 카드, 원형 배지, 수직 중앙 정렬
4. **card_list** → 다중 카드 bottom-align, overflow 처리 (가장 복잡)
5. **detail** → infobox, accent 수직바

**테마 전환 순서:**
1. NavyGold (기본)
2. Forest
3. Coral
4. Slate
5. Midnight

**병행 운영:** `REMOTION_RENDER=1` 환경변수로 feature flag 제어

### Phase 3: 영상 파이프라인 전환 (3~4주)

**목표:** MoviePy 기반 영상 → Remotion Video 컴포지션

**핵심 이식 대상:**

| 현재 (MoviePy/NumPy) | Remotion 대응 |
|---------------------|--------------|
| scene_composer.compose_scenes() | Remotion Sequence 구조 |
| scene_renderer.render_typing_frames() | interpolate() + useCurrentFrame() |
| scene_renderer.render_fade_in_text_frames() | spring() 애니메이션 |
| scene_renderer.render_checklist_frames() | Sequence + spring() |
| video_generator.fade_transition() | @remotion/transitions |
| video_generator.attach_bgm() | `<Audio>` 컴포넌트 |
| ken_burns() | CSS transform + interpolate() |

**마이크로태스크:**

| # | 작업 | 시간 |
|---|------|------|
| 3.1 | ShortForm Composition 등록 (1080x1920, 30fps) | 2h |
| 3.2 | 5종 Scene 컴포넌트 (hook/checklist/data/info/cta) | 16h |
| 3.3 | 전환 효과 (fade/slide/zoom) TransitionSeries 구현 | 4h |
| 3.4 | Audio 컴포넌트 (Edge TTS MP3 동기화) | 4h |
| 3.5 | Python video 파이프라인 → Remotion 클라이언트 연결 | 4h |
| 3.6 | 영상 출력 규격 검증 (Threads/Reels 업로드 테스트) | 4h |

### Phase 4: 최적화 + 고급 효과 (2~3주)

**목표:** 프로덕션 안정화 + 시각적 차별화

- 렌더링 성능 최적화 (concurrency 튜닝, 번들 캐시)
- Lottie 보험 아이콘 라이브러리 구축
- 카운트업/차트 애니메이션 도입
- 글래스모피즘/파티클 효과
- render-server 프로세스 관리 (systemd 데몬화)
- 장애 복구 (Chromium 크래시 시 자동 재시작)

---

## 6. 예상 소요 기간/난이도

| Phase | 기간 | 난이도 | 리스크 |
|-------|------|--------|--------|
| Phase 0: 환경 셋업 | 1~2일 | 낮음 | 시스템 패키지 누락 |
| Phase 1: POC | 1~2주 | 중간 | 폰트 렌더링 차이 |
| Phase 2: 카드뉴스 전환 | 3~4주 | 높음 | card_list 동적 높이 |
| Phase 3: 영상 전환 | 3~4주 | 높음 | TTS 동기화 타이밍 |
| Phase 4: 최적화 | 2~3주 | 중간 | 메모리 최적화 |
| **총계** | **10~14주** | | |

### 난이도별 주요 도전 과제

**높음:**
- `render_card_list()` 동적 카드 높이 계산 → CSS auto height + DOM 측정으로 대체
- cardnews.py 82KB의 2200줄+ 로직 → React 컴포넌트 20~25개로 분해
- MoviePy scene 렌더러의 프레임 단위 로직 → useCurrentFrame() 기반 재작성

**중간:**
- Python ↔ Node.js HTTP 통신 안정성 (타임아웃, 에러 핸들링)
- 테마 시스템 Python→TypeScript 1:1 매핑 정확성
- Chromium 프로세스 장기 운영 안정성

**낮음:**
- 시스템 패키지 설치
- JSON props 변환 어댑터
- 출력 경로 규칙 유지

---

## 7. 리스크 및 완화 방안

### 7.1 기술적 리스크

| 리스크 | 확률 | 영향 | 완화 방안 |
|--------|------|------|----------|
| Chromium OOM (메모리 부족) | 중 | 높 | RAM 확인, concurrency 제한, 큐 순차 실행 |
| 폰트 렌더링 차이 | 높 | 중 | @font-face로 Pretendard 명시적 로드 |
| 기존 테스트 무효화 | 확실 | 중 | Remotion 전용 통합 테스트 새로 작성 |
| Node.js 서버 크래시 | 낮 | 높 | systemd 자동 재시작, health check |
| Phase 2 공수 초과 | 중 | 중 | card_list/detail만 우선, body는 후순위 |

### 7.2 운영 리스크

| 리스크 | 완화 방안 |
|--------|----------|
| Remotion 장애 시 발행 중단 | `REMOTION_RENDER=0`으로 Pillow 즉시 폴백 |
| Node.js 서버 포트 충돌 | 3000번 전용 포트 + systemd 관리 |
| Remotion 버전 업 호환성 | package-lock.json 고정, 분기별 업데이트 |

---

## 8. todo.json 업데이트 제안

기존 issue-007을 Remotion 기반으로 업데이트:

```json
{
  "id": "issue-007",
  "project": "ThreadAuto",
  "title": "Remotion 기반 영상/카드뉴스 파이프라인 구축",
  "description": "Pillow+MoviePy → Remotion(React) 전환. 카드뉴스 렌더링 품질 향상 + 숏폼 영상 모션 그래픽 도입",
  "priority": "high",
  "status": "pending",
  "sub_items": [
    {"title": "Phase 0: 환경 셋업 (시스템 패키지 + Remotion 프로젝트 초기화)", "done": false},
    {"title": "Phase 1 POC: CoverSlide 1종 + render-server + Python 클라이언트", "done": false},
    {"title": "Phase 2-1: 카드뉴스 5종 슬라이드 컴포넌트 (cover/cta/body/card_list/detail)", "done": false},
    {"title": "Phase 2-2: 5종 테마 TypeScript 이식 (NavyGold/Forest/Coral/Slate/Midnight)", "done": false},
    {"title": "Phase 2-3: Pillow→Remotion 파이프라인 전환 + feature flag", "done": false},
    {"title": "Phase 3-1: ShortForm 영상 Composition + Scene 컴포넌트", "done": false},
    {"title": "Phase 3-2: 전환 효과 + Audio(TTS) 동기화", "done": false},
    {"title": "Phase 3-3: 영상 파이프라인 Remotion 전환 + Reels 업로드 검증", "done": false},
    {"title": "Phase 4: 렌더링 최적화 + Lottie/차트 고급 효과 도입", "done": false}
  ]
}
```

---

## 9. 빠른 시작 체크리스트

```bash
# 1. 서버 RAM 확인
free -h

# 2. 시스템 패키지 설치
sudo apt-get install -y libnss3 libdbus-1-3 libatk1.0-0 libasound2t64 \
  libxrandr2 libxkbcommon-dev libxfixes3 libxcomposite1 libxdamage1 \
  libgbm-dev libcups2 libcairo2 libpango-1.0-0 libatk-bridge2.0-0 \
  fonts-noto-cjk fonts-noto-color-emoji

# 3. Remotion 프로젝트 생성
cd /home/jay/projects/ThreadAuto
mkdir -p remotion && cd remotion
npm init -y
npm install remotion @remotion/cli @remotion/bundler @remotion/renderer
npm install react react-dom express uuid
npm install -D typescript @types/react @types/react-dom

# 4. Chrome 사전 다운로드
npx remotion browser ensure

# 5. 첫 번째 Composition 테스트
npx remotion preview  # 로컬 프리뷰 (GUI 필요)
npx remotion still CardNews ./output/test.png  # 서버에서 CLI 렌더

# 6. render-server 실행
node render-server/server.js  # port 3000

# 7. Python 클라이언트 테스트
python -c "
from renderer.remotion_client import RemotionClient
client = RemotionClient()
result = client.render_still('CardNews', {'slides': [...], 'theme': {...}})
print(result)
"
```
