Remotion으로 뽑아낸 동적 자막에 애니메이션 효과를 넣어보기
– ArtXTech
이전에 위스퍼로 자막 파일 생성 후 자막에서 타이밍과 텍스트를 추출해 유튜브 슬라이드 쇼 영상을 제작해 보았다.
처음 영상을 만들고 나서는 유튜브 영상을 처음으로, 그것도 리모션으로 오로지 코드만 써서 영상 제작을 해본 성취감에 뿌듯했다. 시간이 그 후 좀 지나고 나니 왠지 더 다이나믹한 영상을 만들어보고 싶었다.
이왕 이리 된 거 터치디자이너로 이것 저것 특수효과를 넣어본 댄스 커버 영상을 리모션으로 재작업을 해볼까 싶기도 했고.
저 영상을 찍고 뽑고 나니 노래 가사에 맞춰 키네틱타이포 애니메이션을 넣어보면 어떨까 생각이 들었다. 그래서 지난 번에 만든 노래 가사를 Whisper STT앱을 사용해 추출한 뒤 자막 영상 제작한 프로젝트에서 좀 더 확장해 추가 컴포넌트를 제작하기로 했다.
1단계: 일단 핵심 부품부터
모든 프로젝트가 그렇듯, 가장 중요한 건 핵심 기능을 하는 컴포넌트들부터 만들어보자.
- TypewriterSubtitle: 타이핑 치는 것처럼 자막이 따다닥- 하고 나타나는 효과를 주는 컴포넌트.
- KineticGrid: 그냥 자막만 띡 나오면 좀 심심하니까, 그리드 배경에 막 동적으로 움직이는 애니메이션 효과를 주는 컴포넌트도 만들었다.
- HalftoneShader: 이건 좀 더 욕심내서 만든 건데, WebGL 기반으로 하프톤 효과를 주는 셰이더 컴포넌트다. 영상에 유니크한 질감을 입히고 싶을 때 쓰려고 만들어보았다.
이런 컴포넌트들이 잘 돌아가려면 뒤에서 궂은일 해주는 헬퍼 함수들도 필요하다.
-
SRT 파서:
.srt자막 파일, 이걸 시스템이 알아먹을 수 있게 파싱하고 처리하는 모듈을 만들었다. - subtitleHelpers: 자막이 정확한 타이밍에 뿅! 하고 나타났다가 사라져야 하는데, 그 계산을 도와주는 유틸리티 함수들이다.
- useSubtitleKineticGrid: 이건 커스텀 훅인데, 자막이랑 KineticGrid 컴포넌트를 연동하는 함수이다.
그래서 이 컴포넌트랑 유틸리티들을 가지고 실제로 프로젝트를 진행해 영상을 만들어 보았다.
- YT 비디오 프로젝트 (HowIProgressed): 이건 주로 22-24년 동안 내가 어떻게 성장했는지 뭐 그런 회고복기 영상. 자세한 작업 과정은 아래의 링크로 KR-making-first-time-youtube-vid-with-remotion
- 댄스 커버 프로젝트 (LikeJennie): Like Jennie를 듣고 춘 커버 댄스 영상만 원 소스로 추려내 여기에 터치 디자이너로 FX 프로토타입 작업을 했다. 그 뒤 가사를 싱크에 맞춰 자막 추출을 하고 그 자막 타이밍에 맞게 키네틱 타이포를 구현해 싱크를 맞췄다.
이후 나중에 이 프로젝트를 더 확장시켜 아예 인터뷰 팟캐형 영상을 만들게 된다.
- 대화형 비디오: 두 사람이 나와서 티키타카하는, 팟캐스트 목적으로 만든 유튜브용 간단한 애니메이션과 영상 편집이 곁들어진 비디오. 이 프로젝트가 위의 둘 보다 가장 힘들고 골치 아팠다.
2단계: 만들다 보면 피할 수 없는 확장성. 사람의 욕심은 끝이 없기에.
몇 날 며칠 키보드 두들기다 보니, 나름대로 아키텍처에 대한 감이 잡힌다.
-
훅 기반 로직 분리: 비즈니스 로직이랑 UI를 분리하니까 코드가 훨씬 깔끔해진다.
useSubtitleKineticGrid같은 커스텀 훅들이 그 예시.
3단계: 개발 중 맞닥뜨린 에러들
주요 이슈들 - 현재 위치
아무리 개발 과정이 항상 순탄하지만은 않다지만 고작 추가 애니메이션을 넣고 싶었을 뿐인데 이렇게나 머리가 아플일인가ㅠㅠ
-
자막 애니메이션 재시작 문제: 이게 제일 골 때렸는데, 첫 번째 자막은 타이핑 애니메이션이 잘 나오는데 그 다음 자막부터는 그냥 텍스트가 띡! 하고 나타남. 이게 뭐야. 원인을 분석해보니 컴포넌트 상태가 계속 유지되면서 애니메이션 로직이 다시 실행되지 않고 있었음. 이전 자막의 애니메이션 끝난 상태가 다음 자막에도 영향을 준 거다.
-
컴포넌트 재생성 문제: 자막 내용이 바뀔 때마다 해당 자막 컴포넌트가 새로 나타나야 하는데, 꿈쩍도 안 한다. 분명 props는 바뀌었는데 왜 리렌더링이 안 되지?! (지난 레트로 비디오를 만들며 접한 악몽의 재회인가)
원인을 분석해보니 React의 키 시스템이랑 상태 관리 쪽 문제였다. React가 같은 위치에 같은 타입의 컴포넌트가 있으면 “어? 너 아까 걔랑 똑같이 생겼으니 그럼 걍 쓰던 거 계속 써야지” 이런 식으로 최적화를 하는데. 근데 나는 자막 분리 타이밍 키마다 타이핑 효과가 새로 일어나길 바랬단 말이다.
-
화자 구분 문제: 대화형 자막 만들 때, SRT 파일에는 보통 누가 말하는지 정보가 없다. 리모션 코드 렌더링 플로우에서는 “그래서 이 대사 누가 친 건데?” 하고 알 수가 없으니 이것도 해결해야 했다.
[해결책들 - 현재 위치]
그래서 결국 이렇게 해결. 이렇게 한 건 해결할 때마다 약간 좀 내가 개쩌는 거 같아 자신감 뿜뿜(그리고 다음 버그를 마주할 때 가장 루저로 추락함)
-
컴포넌트 키 관리:
이건 React의
keyprop을 똑똑하게 써서 해결다.KineticGrid같은 컴포넌트 내부에서 동적으로 타일을 생성할 때, 각 타일의key값에 현재 자막의index를 포함시킨다.
// [키워드 A: 동적 키 생성]
const getTileKey = (row: number, col: number) => {
// 현재 자막이 없으면 'empty'라도 넣어서 키를 다르게 만든다.
return `${row}-${col}-${currentSubtitle?.index ?? 'empty'}`;
};
// 타일 렌더링 부분에서 이렇게 사용하겠지
// <Tile key={getTileKey(rowIndex, colIndex)} subtitle={currentSubtitle} />
이렇게 하면 자막이 바뀔 때마다 key 값이 달라지니까, React가 “어? 너 아까 걔랑 키가 다르네? 신입인가…?” 하고 컴포넌트를 아예 새로 만들것이다. 덕분에 상태 꼬일 일 없이 깔끔하게 재생성한다.
화자 구분 시스템:
SRT 파일 자체를 좀 건드려 자막 내용 앞에다가 [Speaker1]이나 [Speaker2] 같은 태그를 추가하는 것이다. 이 내용들은 추후 KR-how-i-made-podcast-interview-with-ai-virtual-idol 에 반영될 것이다.
// [경험 A: SRT 파일 수정]
1 [Speaker1]
00:00:00,000 --> 00:00:02,840
안녕하세요! 저는 스피커1입니다.
2 [Speaker2]
00:00:03,000 --> 00:00:05,500
반갑습니다! 스피커2입니다.
그리고 parseSrt 함수에서 이 태그를 정규식으로 딱! 잡아채서 화자 정보를 추출한다.
// [키워드 B: 화자 정보 추출 로직]
// 자막 인덱스 라인에서 "[SpeakerN]" 패턴을 찾는다
const speakerMatch = indexLine.match(/\[Speaker([12])\]/);
const speaker = speakerMatch ? `Speaker${speakerMatch[1]}` : undefined;
// speaker 변수에는 "Speaker1", "Speaker2" 또는 undefined가 담기겠지
좀 무식한 방법 같기도 하지만, 제일 확실하다.
4단계 시스템 얼개도 그려보기
[시스템 구조 다이어그램 > 전체 아키텍처 - 현재 위치]
말로만 설명하면 감이 잘 안 올까 봐, 시스템이 대충 어떻게 돌아가는지 그림으로 그려봤다.
SRT 파일이 들어오면 parseSrt가 자막 객체 배열로 만들고, useSubtitleKineticGrid 훅이 이걸 받아서 타이밍 계산하고 KineticGrid 컴포넌트에 넘겨주는 흐름이야. 그럼 KineticGrid 안에서 TypewriterSubtitle이나 DialogueSubtitle 같은 애들이 최종적으로 화면에 그려지는 거지. 오디오/비디오 원본이랑 합쳐져서 Remotion Composition을 통해 최종 비디오가 나온다. 참 쉽죠?
쉽겠냐…?
_Shared 폴더에 재사용 가능한 컴포넌트, 훅, 헬퍼들을 몰아넣고, 각 프로젝트 타입(_YT, DanceCover 등) 별로 폴더를 나눠서 관리한다.
시스템 구조 다이어그램
전체 아키텍처
graph TB
A[SRT 파일] --> B[parseSrt]
B --> C["SrtSubtitle[]"]
C --> D[useSubtitleKineticGrid]
D --> E[calculateSubtitleTiming]
D --> F[KineticGrid]
F --> G[TypewriterSubtitle]
F --> H[DialogueSubtitle]
I[Video/Audio] --> J[Remotion Composition]
G --> J
H --> J
K[HalftoneShader] --> J
L[YT Projects] --> J
M[Dance Cover Projects] --> J
N[Dialogue Projects] --> J
subgraph "Shared Components"
G
H
F
K
end
subgraph "Shared Utilities"
B
D
E
end
subgraph "Project Types"
L
M
N
end
컴포넌트 데이터 흐름 (ASCII)
┌─────────────────┐
│ SRT 파일 │
└─────────┬───────┘
│
▼
┌─────────────────┐
│ parseSrt() │
└─────────┬───────┘
│
▼
┌─────────────────┐
│ SrtSubtitle[] │
└─────────┬───────┘
│
▼
┌─────────────────┐
│useSubtitleKinet │
│icGrid Hook │
└─────────┬───────┘
│
├─────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ KineticGrid │ │ calculateSubtitle│
│ Component │ │ Timing │
└─────────┬───────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ TypewriterSub │
│ title Component │
└─────────┬───────┘
│
▼
┌─────────────────┐
│ Remotion Video │
│ Output │
└─────────────────┘
프로젝트 파일 구조
VideoEditPlayground/
├── src/remotion/
│ ├── _Shared/
│ │ ├── Typography/
│ │ │ ├── TypewriterSubtitle.tsx
│ │ │ └── DialogueSubtitle.tsx
│ │ ├── Layout/
│ │ │ └── KineticGrid.tsx
│ │ ├── Effect/
│ │ │ └── HalftoneShader.tsx
│ │ ├── Helper/
│ │ │ ├── parseSrt.ts
│ │ │ └── subtitleHelpers.ts
│ │ └── Hook/
│ │ └── useSubtitleKineticGrid.tsx
│ ├── _YT/
│ │ └── DanceCover/
│ │ ├── LikeJennie/
│ │ │ ├── LikeJennie1.tsx
│ │ │ └── LikeJennie2.tsx
│ │ └── KineticGrid.tsx
│ └── schema.ts
아무튼 이렇게 Remotion 가지고 업그레이드 된 자막을 활용한 타이핑 시스템을 만들어본 썰을 한번 쭉 풀어봤다.
처음엔 “이거 금방 만들겠는데?” 싶었는데, 역시 그래. 그럴리가ㅅㅂㅋㅋㅋ 그래도 문제 하나하나 해결해나가면서 배우는 것도 많고, 뭔가 내 손으로 직접 쓸만한 걸 만들었다는 뿌듯함도 컸다.
앞으로 이 시스템을 어떻게 더 발전시켜 나갈까나.
추후 더 버젼업 된 팟캐스트 형 유튜브 영상의 제작을 진행하게 되는데 이에 대한 작업과정은 아래 아티클을 읽어보길.
KR-how-i-made-podcast-interview-with-ai-virtual-idol
다른 리모션 포스팅을 보고 싶다면
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-making-first-time-youtube-vid-with-remotion
KR-remotion-and-shader
KR-thepain-of-animation-debugging