Remotion Transitions 모듈화 - 코드 재사용의 늪에서 빠져나오기
Remotion Transitions 모듈화 - 코드 재사용의 늪에서 빠져나오기
또 다시 반복된 복붙의 과업
Remotion으로 영상을 만들다 보면 피할 수 없는 순간이 온다. 바로 트랜지션이다. 처음엔 단순했다. 페이드 인, 페이드 아웃 정도면 충분하다고 생각했지. 하지만 영상이 하나둘 늘어나면서 슬라이드, 와이프, 디졸브… 온갖 화려한 효과들이 필요해졌다.
문제는 매번 새로운 에피소드를 만들 때마다 이전 프로젝트에서 트랜지션 코드를 복사해서 붙여넣기를 반복한다는 것이다. 처음엔 ‘뭐 이 정도야’ 했지만, 계속 복붙을 하고 있는 내 모습을 보며 깨달았다. 이건 아니다.
다시 현실을 직시하는 시간
기존 Transitions 폴더를 들여다보니 상황이 생각보다 심각했다. _ref/Transitions 안에는 온갖 컴포넌트들이 뒤섞여 있었다. TransitionSeries, Transition, Sequence는 핵심 로직이고, CircularWipe, Dissolve, FadeThroughColor 같은 건 시각적 효과들이다. 그런데 이 모든 게 한 폴더에 평면적으로 널려있으니 뭐가 뭔지 구분이 안 된다.
더 웃긴 건 매번 필요한 것만 골라서 복사하다 보니, 어떤 프로젝트에는 Slide가 있고 어떤 프로젝트에는 없고… 일관성이라곤 찾아볼 수 없었다는 점이다.
기술 부채라는 이름의 늪
복붙이 편하다고? 천만의 말씀이다. 처음 몇 번은 그럴듯해 보인다. 5분이면 끝나는 일을 왜 몇 시간씩 투자해서 모듈화하냐는 생각이 든다. 하지만 이게 바로 기술 부채의 함정이다.
세 번째 프로젝트쯤 되니 문제가 보이기 시작했다. A 프로젝트에서 복사한 Slide 트랜지션에 버그가 있었는데, B 프로젝트에서는 그 버그를 고쳤지만 C 프로젝트에는 여전히 버그가 있는 버전이 들어가 있었다. 같은 이름의 컴포넌트인데 동작이 다른 것이다. 이게 바로 복붙의 저주다.
더 심각한 건 새로운 트랜지션을 하나 만들면, 기존 프로젝트들에는 적용이 안 된다는 점이다. 멋진 CircularWipe를 만들어놨는데 이전 영상들은 여전히 밋밋한 페이드만 쓰고 있다. 일관성? 그런 건 애초에 포기해야 한다.
코드 품질이라는 허상과 현실
개발자라면 누구나 한 번쯤 들어봤을 것이다. “재사용 가능한 코드를 작성하라”, “DRY 원칙을 지켜라” 같은 말들 말이다. 맞는 말이다. 하지만 현실은 어떤가?
당장 내일까지 영상을 만들어야 하는데 모듈화부터 하고 앉아있을 여유가 있나? 클라이언트는 기다려주지 않고, 마감은 코앞이다. 그러니 일단 복붙하고 나중에 정리하자고 생각한다. 하지만 그 ‘나중에’는 영원히 오지 않는다.
문제는 이런 식으로 쌓인 기술 부채가 결국 더 큰 시간 낭비로 돌아온다는 것이다. 버그 하나 고치려면 여러 프로젝트를 다 뒤져야 하고, 새로운 기능 하나 추가하려면 또 모든 곳에 복붙해야 한다. 결국 단기적 편의를 위해 장기적 고통을 선택한 셈이다.
그렇다면 모듈화는 선택이 아니라 필수다. 코드 품질이라는 추상적인 개념이 아니라, 실제 생산성과 직결되는 문제이기 때문이다.
개인 프로젝트의 성장통
처음 Remotion을 시작할 때는 단순했다. 영상 하나 만드는 게 목표였으니까. 그때는 모든 코드를 한 파일에 때려박아도 상관없었다. 어차피 나 혼자 보는 코드고, 프로젝트도 하나뿐이었으니까.
하지만 프로젝트가 둘, 셋 늘어나면서 상황이 달라졌다. 같은 기능을 여러 번 구현하고 있는 내 모습을 발견했다. 그리고 각각의 구현이 미묘하게 다르다는 것도 깨달았다. 어떤 건 TypeScript를 제대로 적용했고, 어떤 건 any 타입 투성이였다.
이런 상황에서 모듈화는 단순히 코드 재사용을 넘어선 의미를 갖는다. 내 성장을 모든 프로젝트에 반영할 수 있는 방법이기도 하다. 중앙에서 관리하는 모듈을 개선하면, 그걸 사용하는 모든 프로젝트가 함께 발전하는 것이다.
모듈화라는 해답, 그리고 새로운 복잡성
그래서 결심했다. 더 이상 복붙은 없다. 제대로 된 모듈화 시스템을 만들어보자고. 하지만 막상 시작해보니 생각보다 복잡했다.
일단 구조부터 고민이다. 핵심 로직과 시각적 효과를 어떻게 분리할 것인가? 타입 정의는 어디에 둘 것인가? 각 컴포넌트 간의 의존성은 어떻게 관리할 것인가? 단순히 폴더 몇 개 만들고 파일 옮기는 게 아니라, 전체 아키텍처를 다시 설계해야 하는 일이었다.
그리고 기존 코드들을 새로운 구조에 맞춰 리팩토링하는 것도 만만치 않았다. 각각의 트랜지션 컴포넌트들이 서로 다른 방식으로 구현되어 있어서, 일관된 인터페이스로 통합하는 게 쉽지 않았다. 어떤 건 props로 설정을 받고, 어떤 건 하드코딩되어 있고… 이런 차이점들을 모두 정리해야 했다.
하지만 이 과정에서 깨달은 게 있다. 복잡성은 피할 수 없다는 것이다. 복붙으로 숨겨놨던 복잡성이 모듈화 과정에서 드러날 뿐이다. 차이점은 이제 그 복잡성을 체계적으로 관리할 수 있다는 것이다.
실제 구현: 이론 뒤에 숨은 진짜 작업
복잡성을 체계적으로 관리한다는 건 결국 구조를 잡는다는 뜻이다. 그래서 기존의 평면적인 폴더 구조를 완전히 뒤엎었다.
src/remotion/_YT/_shared/transitions/
├── core/ # 핵심 로직
├── presentations/ # 시각적 효과들
├── hooks/ # 재사용 가능한 훅들
├── components/ # 공통 컴포넌트
└── types.ts # 타입 정의
이렇게 나누고 나니 뭐가 뭔지 명확해졌다. core에는 TransitionSeries 같은 핵심 로직만, presentations에는 Slide, Dissolve 같은 시각적 효과만 들어간다. 더 이상 한 폴더에서 온갖 파일들을 뒤질 필요가 없다.
타입 정의: 혼돈을 질서로
가장 먼저 손댄 건 타입 정의였다. 기존 코드들을 보니 각자 다른 방식으로 props를 받고 있었다. 어떤 건 direction을 받고, 어떤 건 angle을 받고… 이런 식으로는 일관된 API를 만들 수 없다.
// types.ts
export interface TransitionImplementationProps {
progress: number;
exitingElement: ReactNode;
enteringElement: ReactNode;
}
export type TransitionImplementation = React.FC<TransitionImplementationProps>;
모든 트랜지션이 따라야 할 기본 인터페이스를 정의했다. progress는 0에서 1 사이의 값으로 트랜지션 진행률을 나타내고, exitingElement와 enteringElement는 전환되는 요소들이다. 단순하지만 강력하다.
각 트랜지션별 특수한 옵션들도 별도로 정의했다:
export interface SlideDirection {
direction?: 'up' | 'down' | 'left' | 'right';
}
export interface CircularWipeProps {
direction?: 'in' | 'out';
}
export interface FadeThroughColorProps {
color?: string;
}
이제 각 트랜지션이 어떤 옵션을 받는지 타입 레벨에서 명확하다. IDE에서 자동완성도 제대로 작동한다.
핵심 로직: TransitionSeries의 마법
가장 복잡했던 건 TransitionSeries 컴포넌트였다. 이 녀석이 모든 트랜지션의 타이밍과 순서를 관리한다. 기존 코드를 보니 온갖 복잡한 로직이 한 곳에 뒤섞여 있었다.
const TransitionSeries: TransitionSeriesComponentType = ({ children }) => {
const currentFrame = useCurrentFrame();
const visibleChildren = useMemo(() => {
const childArray = flattenFirstLevelFragments(children);
let accumulatedDuration = 0;
const activeChildren = React.Children.map(childArray, (child, i) => {
// 복잡한 타이밍 계산 로직...
const startFrame = accumulatedDuration + offset;
const isTransitioning = /* 트랜지션 상태 확인 */;
if (isTransitioning) return null;
return React.cloneElement(child, {
...child.props,
from: startFrame,
});
});
return activeChildren;
}, [children, currentFrame]);
return <>{visibleChildren}</>;
};
이 코드의 핵심은 현재 프레임에서 어떤 요소들이 보여야 하는지 계산하는 것이다. 트랜지션이 진행 중일 때는 기존 요소를 숨기고, 트랜지션 컴포넌트만 렌더링한다. 복잡해 보이지만 실제로는 논리적이다.
사용법: 복잡함 뒤에 숨은 단순함
모든 복잡한 로직을 숨기고 나니 사용법은 오히려 단순해졌다:
const MyEpisode: React.FC = () => {
return (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<div>Scene 1</div>
</TransitionSeries.Sequence>
<TransitionSeries.Transition
durationInFrames={30}
transitionComponent={Slide}
/>
<TransitionSeries.Sequence durationInFrames={80}>
<div>Scene 2</div>
</TransitionSeries.Sequence>
</TransitionSeries>
);
};
이제 새로운 에피소드를 만들 때마다 이 패턴만 따르면 된다. 복붙할 필요도 없고, 버그가 있으면 한 곳만 고치면 모든 프로젝트에 반영된다.
확장성: 미래를 위한 투자
더 중요한 건 새로운 트랜지션을 추가하는 게 쉬워졌다는 점이다:
const CustomZoomTransition: TransitionImplementation = ({
progress,
exitingElement,
enteringElement,
}) => {
const scale = 1 + progress * 0.5;
const opacity = 1 - progress;
return (
<>
<div style={{ transform: `scale(${scale})`, opacity }}>
{exitingElement}
</div>
<div style={{ opacity: progress }}>
{enteringElement}
</div>
</>
);
};
정해진 인터페이스만 따르면 어떤 트랜지션이든 만들 수 있다. 그리고 만든 즉시 모든 프로젝트에서 사용할 수 있다.
결론: 복잡성과의 화해
결국 모듈화는 복잡성을 없애는 게 아니라 관리하는 것이다. 복붙으로 숨겨뒀던 문제들을 표면으로 끌어올려서 체계적으로 해결하는 과정이다. 처음엔 더 복잡해 보이지만, 장기적으로는 훨씬 단순해진다.
이제 새로운 영상을 만들 때마다 트랜지션 걱정은 없다. 필요한 효과를 골라서 쓰기만 하면 되니까. 그리고 더 멋진 트랜지션이 생각나면 한 곳에 추가하면 모든 프로젝트가 함께 발전한다.