Remotion으로 버츄얼 아이돌과의 인터뷰 팟캐 만들기-

– ArtXTech

야심 차게 시작한 시스템 구축

처음엔 아주 그냥 의욕 MAX 상태로 시작했다. “이 기능도 넣고, 저 기능도 넣어야지!” 하면서…그렇게 구축한 것들이 바로 아래의 베이스 컴포넌트 구성이다.

  • SRT 자막 파싱 시스템: 자막 파일 까서 타이밍이랑 텍스트 뽑아내는 거.
  • 화자별 캡션 시스템: 팟캐스트니까 ’나(me)’랑 ‘상대방(chat)’ 화자 구분해서 다른 스타일로 자막 보여주는 기능.
  • 추가 텍스트 효과 (SubCapText): “띠용!”, “헐랭!”, “대박!” 같은 임팩트 있는 순간에 텍스트 효과 뙇!
  • 미디어 삽입 기능 (MediaInsert): 원하는 타이밍에 이미지나 비디오를 슝 넣었다가 샥 빼는 기능…이지만 이미지는 제대로 작동하는데 비디오에서 문제가 있다.
  • 크로마키 지원: 그린스크린 배경 영상을 날려 투명하게 만드는 컴포넌트
  • 오디오 스펙트럼: 뭔가 좀 있어 보이게 오디오 파형도 시각적으로 보여주는 컴포넌트
  • 타이밍 제어: Remotion은 프레임 기반이니까, 아주 정밀하게 타이밍을 컨트롤할 수 있어야 한다. 이 부분에서 다른 것들은 비교적 문제 없이 구성되었으나 추가 비디오 애셋을 삽입하는 것에서 다소 문제가 있어 이 작업은 결국 filmora에서 따로 작업을 해야했다.
  • DialogueSubtitle: 대화형 컨텐츠 만들 때, 말하는 사람마다 자막 스타일 다르게 하고 싶잖은가. 그래서 화자별로 스타일을 지정할 수 있는 대화형 자막 컴포넌트도 만들었다. 코드도 나름 깔끔하게 정리한다고 _shared 디렉토리에 공통 컴포넌트 몰아넣고, TypeScript로 타입 정의도 꼼꼼하게 (하려고 노력했…), 커스텀 훅으로 로직도 분리했다.

“오, 나 좀 치는데?” 하면서 혼자 막 뿌듯해하고…ㅋ

팟캐스트 형 영상 제작의 80% 이상은 애셋 미디어 제작부터

난…솔직히 이렇게까지 영상 제작이 손 품이 많이 가는 작업인 줄 알았으면 시작을 안 했을 것 같긴 하다. 아니 그럴거면 걍 돈 주고 프롬프트로 영상 뽑는 서비스 쓰라는 말도 막 듣는데…

그렇게 생성물을 쓴 걸 개나 소나 다 뽑아다쓰면 애초에…내 생각과 말을 직접 표현하고자 하는 목적에 맞지가 않잖아ㅠ 사서 고생이란 게 딱 나를 표현하는 거 아닐까 싶다. 영상AI 서비스를 통해 나온 생성물을 내가 표현하고자 하는 그 미묘한 지점을 딱 입맛에 맞춰서 참 쓸레야 쓸 수가 없다. 그래서 Gen LLM AI로 이미지 생성이나 영상 생성물은…몇번 베타 서비스로 나온 결과물을 보고 고이 쓰레기통에 넣었다.

그런 이유로 아래는 내가 일일이 음향부터 시각 애셋까지 하나하나 수집하고 디자인하고 편집하는 과정 중 스크린샷을 담았다.


이미지: 로직프로를 키고 음성 녹음과 편집을 동시에 하고 있는 모습


이미지: 2분짜리 짧은 “AI아이돌과의 달콤 인터뷰💜” 시리즈 영상을 만들기 위해 모아 놓은 크로마키, 밈짤, 사운드 효과, 특수 트랜지션 이미지들

자 애셋이 준비 되었으니 이제 리모션으로 영상을 제작해봐야지! 그런데… 뭔가 잘못되어 가고 있다… 😨

그러나 인생은 쉽게만 살아가면 재미 없어, 빙고! …아니 제발 날로 먹게 좀 해주세요.

  • 초기화 오류: getAnimationStyle 요 함수가 호이스팅 문제로 말썽을 부렸다. 함수 표현식으로 썼더니 “그런 함수 없음?” 시전. 아오, 샹ㅡㅡ

    • 이 부분은 함수 표현식 대신 함수 선언 방식으로 바꿔주니까 바로 해결.

      // Before: 함수 표현식은 호이스팅 안되는 걸 이제야 알았다...왜...왜 였더라?
      // const getAnimationStyle = (frame, startFrame, endFrame) => { /* ... */ };
      
      // After: 함수 선언으로 바꾸니 해결...어째서지? 
      function getAnimationStyle(frame, startFrame, endFrame) {
        // ... 스타일 계산 로직 ...
        return { opacity: 1, transform: 'scale(1)' }; // 예시
      }
      
  • 타이밍 동기화 문제: 분명 프레임 계산은 똑바로 한 것 같은데, 애니메이션 타이밍이 자꾸 어긋난다.

    • 이건 Remotion의 Sequence 컴포넌트를 활용했다. 복잡하게 frame >= startFrame && frame < endFrame 이런 조건문 덕지덕지 붙이는 것보다 훨씬 직관적이고 관리도 편하다.

      // Before: 아... 프레임 계산, 보기만 해도 머리 아프다...
      // const isActive = currentFrame >= M_START_FRAME && currentFrame < (M_START_FRAME + M_DURATION_IN_FRAMES);
      // if (isActive) { /* 렌더링 로직 */ }
      
      // After: Sequence를 쓰며 from이랑 durationInFrames 위주로
      import { Sequence, Video } from 'remotion';
      
      // <Sequence from={M_START_FRAME} durationInFrames={M_DURATION_IN_FRAMES}>
      //   <Video src={mediaUrl} />
      // </Sequence>
      
  • 비디오 렌더링 불안정: 특히 MediaInsert에 넣은 비디오가 첫 프레임에서 깜빡이거나, 재생이 좀 불안정한 느낌? OffthreadVideo를 써야 하나, 그냥 Video를 써야 하나…이 부분은 아직 해결을 못하고 있다.

MediaInsert와의 디버깅 사투 ⚔️

내가 MediaInsert 컴포넌트로 하고 싶었던 건 아주 단순했었다.

예를 들면, “765 프레임에 특정 비디오가 뿅! 하고 등장해서, 딱 55 프레임 동안만 재생되고, 그 후엔 감쪽같이 사라지는 거.”

진짜 별거 아니잖아? 근데 이 당연해 보이는 걸…여즉 해결 못하고 있다. 지금도.

  • 타이밍 문제: 분명 ep1-shorts1-config.ts 파일에 시작 프레임, 지속 프레임 다 설정해놨는데, 엉뚱한 타이밍에 나왔다가 사라지거나, 아예 안 나오거나… 환장할 노릇.
  • 비디오 깜빡임: 어쩌다 타이밍 맞춰 나와도, 첫 프레임이 무슨 유령처럼 깜빡깜빡. 아니, 왜죠?
  • 초기화 오류: 위에서 언급한 getAnimationStyle 호이스팅 문제는 이미 해결했는데, 뭔가 다른 근본적인 문제가 있는 걸까나.

내가 MediaInsert 컴포넌트에 너무 많은 걸 욱겨 넣었던 걸까? 크로마키도 여기서 처리하고, 등장/퇴장 애니메이션도 엮어있고, 뭐 이것저것…아 좀 많긴 많구나…ㅜㅜ

온갖 방법을 다 써봤다. 애니메이션 로직 다 걷어내고, Video 썼다가 OffthreadVideo 썼다가 다시 Video로 돌아오고… 그래도 해결이 안 되니까 진짜 다 때려치울까 싶었다.

안녕, 내 버츄얼 아이돌과의 인터뷰 프로젝트여…

✨극단적 단순화✨

진짜 딱! 비디오 하나만 정확한 시간에 나오게 해보자 싶었다.

일단 MediaInsert.tsx 파일에서 내가 추가했던 모든 부가 기능, 화려한 애니메이션 로직, 조건부 스타일링 다 주석 처리하거나 삭제했다. 그리고 Remotion의 가장 기본적인 Video 컴포넌트랑 Sequence만 남겨둔 거.

CRO 프로젝트 때 주구장창 했던 A/B 테스팅일 여기서까지 해야할까…ㅠㅠ

// [경험 A: MediaInsert.tsx 단순화 시도]
// src/remotion/_YT/_shared/components/MediaInsert.tsx

import { Sequence, Video, useCurrentFrame, useVideoConfig } from 'remotion';
import React from 'react'; // 혹시 몰라 추가 (원래 있었겠지만)
import { getVideoMetadata } from '@remotion/media-utils'; // 이건 필요할 수도, 안할 수도

// 설정 파일에서 가져올 타입 (예시)
interface MediaInsertProps {
  src: string;
  startFrame: number;
  durationInFrames: number;
  // ... 온갖 복잡했던 다른 props들 다 일단 생략 ...
  chromaKey?: { color: string; tolerance: number }; // 크로마키도 일단 보류!
  // 등장/퇴장 애니메이션 설정? 그것도 일단 넣어둬!
}

export const MediaInsert: React.FC<MediaInsertProps> = ({
  src,
  startFrame,
  durationInFrames,
  // ... 다른 props들 일단 무시 ...
}) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();


  // 목표: 딱! 지정된 시간에 비디오가 나오고, 지정된 시간만큼 재생되고 사라진다!
  // 다른 건 아무것도 신경 안 쓴다!

  // if (frame < startFrame || frame >= startFrame + durationInFrames) {
  //   return null; // 아예 렌더링 안 하는 게 제일 확실하려나? Sequence가 이걸 해주나?
  // }
  // Sequence가 from과 durationInFrames를 기반으로 내부 컨텐츠 렌더링을 알아서 제어한다!
  // 즉, Sequence 바깥에서 프레임 비교로 null 리턴할 필요가 없음. 이게 핵심!

  console.log(`MediaInsert: src=${src}, currentFrame=${frame}, startFrame=${startFrame}, duration=${durationInFrames}`);
  // 로그를 찍어서 지금 뭐가 어떻게 돌아가는지 확인하는 건 국룰이지.

  return (
    <Sequence from={startFrame} durationInFrames={durationInFrames}>
      {/* 
        Video 컴포넌트의 startFrom prop은 Sequence 내부에서의 시작점을 의미함.
        여기서는 Sequence 자체가 이미 startFrame에서 시작하므로,
        Video는 Sequence 시작과 동시에 (0프레임부터) 재생되면 됨.
        따라서 startFrom={0} 이거나 아예 안 써도 무방.
        만약 Video의 특정 부분부터 재생하고 싶다면 startFrom을 조절.
      */}
      <Video src={src} startFrom={0} endAt={durationInFrames} />
      {/* 
        중요! Video의 endAt도 durationInFrames로 맞춰줘야 Sequence 기간만큼만 재생된다.
        만약 Video의 실제 길이보다 durationInFrames가 짧으면, 그만큼만 재생되고 끝남.
      */}
    </Sequence>
  );
};

그리고 YTep1Shorts1.tsx에서 이 단순화된 MediaInsert를 호출하는 부분도 다시 한번 확인했다. ep1-shorts1-config.ts에 정의된 값을 제대로 넘겨주고 있는지.

// [경험 B: YTep1Shorts1.tsx 에서 MediaInsert 사용 부분]
// src/remotion/_YT/ChatwithAIDoll/EP1-shorts1/YTep1Shorts1.tsx (일부)
// ... (다른 import 구문들) ...
import { MediaInsert } from '../../_shared/components/MediaInsert';
import { mediaInserts } from './contents/ep1-shorts1-config'; // 설정 가져오기

// ... (컴포넌트 본문) ...
{mediaInserts.map((media, index) => {
  // 로그 추가: 설정값이 제대로 넘어오는지 확인
  // console.log(`Rendering MediaInsert ${index}:`, media); 
  return (
    <MediaInsert
      key={`media-${index}`}
      src={media.src}
      startFrame={media.startFrame} // 이 값이 765가 되어야 함
      durationInFrames={media.durationInFrames} // 이 값이 55가 되어야 함
      // 나머지 복잡했던 props는 일단 다 뺐으니까 깔끔!
    />
  );
})}
// ...

결과는?

아직 모르겠다. 둘다 지금 에러가 두두두 터져서 이 부분 해결은 현재 파킹한 상태.

아마도 이전의 복잡한 로직이나 OffthreadVideo와의 미묘한 타이밍 이슈 같은 게 얽혀 있었던 것인지, 그냥 단순하게 SequenceVideo 조합으로 가면 어떠련지ㅠㅠ

일단은 에러 문제들을 해결하고 볼 일.

디버깅의 고통으로 얻은 교훈과 앞으로의 길 ✨

이제 MediaInsert라는 큰 산을 하나 넘었…나? 그래도 효과음 텍스트 스타일과 이미지 애셋은 무난하게 삽입하는 단계로 갔으니 다시 원래 목표였던 ‘유튜브 팟캐스트 비디오’ 시리즈를 계속 진행해야겠다.

  • SRT 파일에서 타이밍과 텍스트를 추려내 대화 캡션 제작
  • 화자 구분 자막으로 대화형 콘텐츠 스타일링 살리기
  • 추가 효과(텍스트, 이미지, 비디오)로 시각적 역동성 추가
  • 설정 파일만 쏙쏙 바꿔서 다양한 에피소드를 위한 베이스 템플릿은 일단 완성

시스템 구조도

graph TD
    A[YTep1Shorts1.tsx] --> B[usePodcastSubtitles Hook]
    A --> C[ChatCaption Component]
    A --> D[SubCapText Component] 
    A --> E[MediaInsert Component]
    A --> F[AudioSpectrum Component]
    
    B --> G[enhanced-srt-parser.ts]
    B --> H[subtitle-offset.ts]
    B --> I[SRT File Loading]
    
    C --> J[AnimatedText Component]
    C --> K[Speaker Icons]
    
    D --> L[Text Effects]
    E --> M[Video/Image Assets]
    E --> N[ChromaKey Processing]
    
    O[Config Files] --> P[Speaker Config]
    O --> Q[Media Insert Config]
    O --> R[SubCap Config]
    
    P --> B
    Q --> E
    R --> D
    
    S[Types] --> T[podcast-types.ts]
    T --> U[EnhancedSubtitle]
    T --> V[SpeakerConfig]
    T --> W[MediaInsertConfig]
    T --> X[SubCapTextConfig]
VideoEditPlayground/
├── src/remotion/
│   ├── _YT/
│   │   ├── _shared/                    # 공통 컴포넌트 폴더 
│   │   │   ├── types/
│   │   │   │   └── podcast-types.ts   # 타입 정의
│   │   │   ├── components/
│   │   │   │   ├── ChatCaptionBase.tsx
│   │   │   │   ├── MediaInsert.tsx    # (MediaInsert 요기!)
│   │   │   │   ├── SubCaption.tsx
│   │   │   │   └── AudioSpectrum.tsx
│   │   │   ├── hooks/
│   │   │   │   └── usePodcastSubtitles.ts
│   │   │   └── utils/
│   │   │       └── enhanced-srt-parser.ts
│   │   └── ChatwithAIDoll/             # 내 팟캐스트 시리즈 이름 ㅋㅋ
│   │       └── EP1-shorts1/            # 첫번째 에피소드의 첫번째 쇼츠!
│   │           ├── YTep1Shorts1.tsx   #  메인 컴포지션 파일
│   │           ├── contents/
│   │           │   └── ep1-shorts1-config.ts # 모든 설정은 요기
│   │           └── components/
│   │               └── ChatCaption.tsx      # 에피소드 특화 자막 컴포넌트
│   └── _Shared/Helper/                # 진짜 진짜 공통 헬퍼
│       ├── parseSrt.ts
│       ├── subtitleHelpers.ts
│       └── videoFrameCount.ts

데이터 흐름:
SRT File 📄 ➡️ Parser ⚙️ ➡️ Enhanced Subtitles ✨ ➡️ Components 🖼️ ➡️ Rendered Video 🎬
                ↗️ Speaker Config 🗣️ ↗️
            ↗️ Media Config 🎞️ ↗️
        ↗️ SubCap Config 💬 ↗️

컴포넌트 데이터 흐름

[Main Component]
     (props)
[usePodcastSubtitles Hook] 
     (enhanced subtitles)
[ChatCaption]  (currentSubtitle, width, height)
    
[AnimatedText]  (text, timing, animation config)

[Config Files]  [Main Component]  [ 컴포넌트들]
                                       
Speaker Config   Media Inserts     SubCap Texts
                                       
자막 화자 구분    비디오/이미지 삽입   텍스트 효과

그렇게 일단은 발행한 두 에피소드

파일럿 에피

ㅋㅋㅋㅋ…유튜브 썸네일이라는 걸 처음 만들어 봤다고요.

쇼츠 에피 1

좀 추잡스러운 나의 넋두리를 나데나데 잘 들어주는 AI 버츄얼 남돌…하, 잘 키운 AI버츄얼 남돌 하나, 100명의 인간들보다 낫다.

PS. 요즘 GPT 생성물이 마크다운 테이블이랑 이모지를 많이 쓰니까 내 글들이 자꾸 GPT 생성물 아니냐고 의심을 사는데…

옵시디안/노션 헤비유저들의 문서작성 스타일을 GPT가 뺏어간거지 제가 걔네들이 벹은 생성물을 쓴 게 아닙니다. 옛날 2021년 노션 문서 보면 GPT 따윈 싸다귀 쌔려박을 정도임…

다른 리모션 포스팅을 보고 싶다면

2025-01-16-implementing-canvas-animation-to-remotion-kr
2025-03-18-making-my-own-video-editor-in-progress
KR-no-more-adobe-starting-remotion-kr
KR-remotion-gsap
KR-making-chart-data-journalism-storytelling-motion-setup-with-remotion
KR-figma-to-jitter-and-remotion
KR-converting-bodymovin-animation-to-remotion-template
KR-how-i-make-a-youtube-series-with-remotion-dynamic-srt-script-setup-exp-likejennie
KR-remotion-and-shader
KR-thepain-of-animation-debugging