AI 도구 목록

Remotion 쇼츠를 만들다 타이밍과 루프를 고친 기록

2026년 10월 6일Importants Studio

쇼츠를 만들 때마다 편집기를 열어 자막 위치와 효과를 다시 맞추는 일이 반복됐고, 대사만 달라져도 장면을 복제했고, 숫자 하나를 고치면 효과 타이밍까지 다시 확인해야 했습니다. 그래서 장면 목록과 대사를 입력으로 받기로 했습니다. 영상은 코드로 렌더합니다.

Remotion은 React 컴포넌트를 프레임 단위로 렌더해 영상으로 뽑는 도구입니다. 처음에는 장면마다 화면을 갈아 끼우려 했습니다. 만들고 나니 카메라만 움직이면 되는 구조였습니다. 다만 곡선 이름을 믿었다가 큐가 밀렸고, 자막 숫자는 깨졌으며, 마지막 장면은 처음으로 돌아갈 때 튀었습니다.

먼저 결론부터

  • 큰 캔버스에 장면 타일을 놓고, 장면 전환은 새 화면을 그리는 대신 카메라 좌표를 옮깁니다.
  • 타일은 4열 5행이고, 슬롯 20개가 장면 목록에 연결됩니다.
  • 구간은 Sequence로 나누고, 카메라 위치와 확대값은 interpolate로 이어 붙입니다.
<Sequence from={scene.start} durationInFrames={scene.duration}>
  <SceneCard {...scene} />
</Sequence>
  • 이름이 inOut인 곡선은 실제로 앞부분에서 진행이 끝나는 모양이고, 효과보다 큐가 먼저 끝난 이유도 여기 있었습니다.
  • 숫자 자막의 패턴은 \d+가 아니라 (\d[\d,]*). 그래야 13,000이 갈라지지 않습니다.
  • 루프 끝의 확대값은 1.9에서 튀었고 1.14로 낮춘 뒤 이어졌습니다.
  • 설정 파일은 마지막 장면 유지 시간을 계산한 다음에 써야 합니다. 원인은 파일 저장 순서.

장면을 새로 그리지 않고 카메라를 움직였습니다

처음에는 장면마다 별도 컴포넌트를 만들고 화면 전체를 전환하려 했는데, 장면이 늘어날수록 전환 코드도 같이 늘었습니다. 배경, 자막, 아이콘이 비슷한 구성을 반복하는데도 매번 같은 화면을 다시 만드는 셈입니다.

그래서 큰 캔버스 위에 4열 5행으로 장면 타일을 배치했고, 장면 하나가 슬롯 하나를 씁니다. 카메라가 현재 장면 타일 위치로 이동하면 화면에는 해당 장면만 보이는 구조입니다.

const cameraX = interpolate(frame, inputRange, outputRange);
const cameraY = interpolate(frame, inputRange, outputRange);

이렇게 바꾸니 대사와 장면 목록을 교체하는 흐름이 단순해졌고, 장면을 추가할 때는 새 화면 전환을 만들지 않고 빈 슬롯에 내용을 넣으면 됩니다. Sequence는 장면의 등장 구간만 나눕니다.

영상 소재는 OffthreadVideo로 넣고 프로젝트 안의 파일은 staticFile로 읽었는데, 소재를 어디서 가져오는지와 장면을 언제 보여 주는지를 분리하니 렌더 코드가 덜 엉켰습니다.

inOut이라는 이름만 보고 곡선을 믿었습니다

가장 오래 붙잡고 있던 문제는 효과가 큐보다 먼저 끝나 보이는 현상이었습니다. 처음에는 interpolate 범위를 잘못 잡았다고 생각해 범위를 바꾸고 다시 렌더했지만, 비슷하게 어긋났습니다.

문제는 곡선.

이름이 inOut이라 가속과 감속이 대칭일 것이라 생각했는데, 곡선을 열어 보니 진행의 90%가 앞 25% 구간에서 끝나는 모양입니다. 이름은 부드럽게 들어오고 나가는 효과처럼 보였습니다. 실제 움직임은 거의 초반에 끝나는 out 쪽이었습니다.

그 상태에서 큐 변수도 24프레임 일찍 시작하고 있었고, 곡선의 진행과 큐 시작점을 함께 추측으로 고치니 같은 문제를 반복했습니다. 프레임을 이미지로 뽑아 효과가 멈추는 지점을 보자마자 원인이 보였습니다.

값을 더 바꾸기 전에 이미지를 봤어야 했습니다.

이후에는 타이밍이 어긋나면 먼저 문제 구간의 프레임을 저장합니다. 코드에서 곡선을 읽는 것과 화면에서 움직임을 보는 일은 다릅니다.

13,000이 13,0원으로 나온 이유

카운트업 자막도 예상보다 쉽게 깨졌는데, 숫자만 찾아 증가시키려고 \d+로 문자열을 나누자 쉼표가 들어간 값은 숫자 덩어리가 아니라 여러 조각으로 처리됐습니다.

13,000은 13과 000으로 갈렸고, 렌더된 화면에는 13,0원이 찍혔습니다. 자막 컴포넌트 문제처럼 보였지만, 값을 쪼갠 원인은 정규식이었습니다.

const amountPattern = /(\d[\d,]*)/;

숫자는 쉼표까지 포함해 통째로 잡고, 계산할 때만 쉼표를 뗍니다. 계산이 끝나면 다시 자막 형식으로 되돌립니다. 원인은 숫자 표시와 숫자 계산을 같은 문자열 처리에 묶어 둔 것.

아이콘에도 비슷한 시행착오가 있었는데, lucide-react 1.49.0을 넣고 물음표만 단독으로 있는 아이콘을 찾다가 시간을 썼고, 실제로는 원, 방패, 말풍선 안에 물음표가 들어간 형태였습니다.

선으로 된 아이콘은 오히려 코드 효과에 잘 맞았습니다. stroke-dashoffset을 움직여 선이 그려지는 효과를 만듭니다. 라이선스도 ISC라 결과물에 출처를 따로 적지 않는 흐름으로 정리했습니다.

마지막 장면이 처음으로 돌아갈 때 튀었습니다

루프 영상은 끝 장면만 자연스러우면 된다고 생각했습니다. 그러나 끝에서 카메라를 1.9배까지 확대한 상태로 처음으로 돌아가니 되감기는 순간이 눈에 띄었고, 마지막 장면 자체는 멈춰 보이지 않았지만 반복 재생에서는 확대 변화가 이음매를 드러냈습니다.

확대값을 1.14로 줄이자 튀는 느낌이 사라졌습니다. 여기서 배경까지 암전시키면 전환이 감춰질 것 같았습니다. 결과는 반대였습니다. 배경이 어두워졌다가 처음 장면의 배경으로 돌아오면서 이음매가 더 보였습니다.

암전은 글자에만 걸었습니다. 시작의 페이드인도 뺐습니다. 시작 화면이 이미 완성된 상태로 보여야 끝에서 돌아온 화면과 연결됩니다.

계산은 맞았는데 파일에는 옛값이 남았습니다

마지막으로 남은 문제는 장면 유지 시간. 빌드 스크립트에서 설정 파일을 먼저 쓰고, 뒤에서 마지막 장면의 유지 시간을 계산하고 있었습니다. 콘솔에서 계산값을 확인하면 맞았습니다. 그런데 렌더 결과는 계속 이전 길이.

파일에는 이미 옛 설정이 저장된 뒤였습니다.

파일 쓰기를 계산 뒤로 옮겼습니다. 계산 로직은 건드리지 않았고 저장 순서만 바꿨는데, 이 문제는 영상 타이밍 오류처럼 보였지만 실제 원인은 렌더 이전 단계의 파일 기록 순서.

이 파이프라인에서는 장면 목록을 바꾸는 일보다, 바뀐 값이 어느 시점에 프레임과 설정 파일에 들어가는지 확인하는 일이 더 자주 남았습니다. 화면이 이상할 때 효과 코드부터 고치지 않고 프레임, 문자열, 저장된 설정 순서로 확인하니 문제가 어느 단계에서 생겼는지 가려졌습니다.