Remotion Studio가 답답해서 직접 만들기로 했는데, 옛날 코드 분석하다가 깨달은 것들. 의존성 지옥부터 아키텍처 설계까지 기록

Remotion 기반 커스텀 에디터 만들기: 남의 코드 뜯어보며 깨달은 현실

시작은 언제나 불만에서

Remotion에서 제공하는 Studio가 내 용도에 맞지 않아서 결국 직접 만들기로 했다. 뭐, 세상에 완벽한 도구는 없으니까. 그런데 막상 시작하려니 막막하더라. 그래서 찾아낸 게 옛날에 만들어진 Remotion 기반 에디터의 참고 자료였다.

문제는 이 코드가 구석기 시대에 만들어진 것 같다는 점이다. 의존성부터 시작해서 아키텍처까지, 현재 내가 설치한 패키지들과는 천지차이다.

의존성 분석: 옛날 것 vs 현재 것

🗑️ 버릴 것들 (jQuery 시대의 유물들)

참고 코드를 보니 jQuery부터 시작해서 온갖 레거시 라이브러리들이 잔뜩 들어있다:

<script src="jquery/3.5.1/jquery.min.js"></script>
<script src="js/libraries/sortable.min.js"></script>
<script src="js/libraries/range-slider.min.js"></script>
<script src="js/libraries/anime.min.js"></script>

이런 걸 보면 정말 격세지감이 든다. 2024년에 jQuery를 쓴다고? React의 가상 DOM이 있는데 왜 직접 DOM을 조작하겠는가.

옛날 라이브러리 현재 대체재 비고
jQuery React + React DOM 당연히 React 쓸 거다
sortable.min.js react-draggable, react-moveable 이미 설치되어 있음
range-slider.min.js HTML5 input[type=“range”] 네이티브가 최고다
anime.min.js GSAP 이미 GSAP 있으니 굳이?

고민해볼 것들

// Fabric.js - 캔버스 라이브러리
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/460/fabric.min.js"></script>

Fabric.js는 강력한 캔버스 라이브러리다. 하지만 Remotion에서는 필요 없다. Remotion이 렌더링을 담당하니까. 캔버스 조작 대신 Remotion 컴포넌트(<AbsoluteFill><Sequence> 등)와 GSAP을 쓸 거다.

graph TD
    A[기존 Motionity] --> B[Fabric.js Canvas]
    B --> C[직접 캔버스 조작]
    
    D[내 Remotion 에디터] --> E[Remotion Components]
    E --> F[GSAP 애니메이션]
    F --> G[Remotion 렌더링]

HTML 구조 분석: 이게 뭘 하는 건지 알아보자

참고 코드의 HTML 구조를 뜯어보니 전형적인 비디오 에디터 레이아웃이다:

<div id="toolbar" class="noselect">
    <!-- 도구 버튼들 -->
</div>

<div id="properties">
    <!-- 속성 패널 -->
    <div class="property-name" data-property="position">
        <span class="property-keyframe"></span>Position
        <img class="freeze-prop" src="assets/freeze.svg">
    </div>
</div>

<div id="canvas-area">
    <canvas id="canvas"></canvas>
</div>

<div id="bottom-area">
    <!-- 타임라인, 레이어 리스트 -->
</div>

핵심 컴포넌트들

  1. Toolbar: 도구 선택 (텍스트, 도형, 미디어 등)
  2. Properties Panel: 선택된 객체의 속성 편집
  3. Canvas Area: 메인 작업 영역 (내 경우엔 Remotion Player)
  4. Timeline: 키프레임과 애니메이션 관리

키프레임 시스템: 이게 진짜 핵심이다

참고 코드에서 가장 중요한 부분은 키프레임 시스템이다. 어떻게 작동하는지 보자:

// ui.js - 패널 업데이트
function updatePanel(selection) {
    if (selection && canvas.getActiveObjects().length == 1) {
        $('#object-specific').html(object_panel);
        updatePanelValues(); // 현재 값으로 입력 필드 업데이트
    }
}

// events.js - 객체 수정 시 자동 키프레임
canvas.on('object:modified', function(e) {
    autoKeyframe(canvas.getActiveObject(), e, selection);
});

이 구조를 React + Remotion으로 옮기면:

// useKeyframes.ts
export const useKeyframes = () => {
    const { currentFrame } = useCurrentFrame();
    
    const addKeyframe = (property: string, value: any) => {
        // 현재 프레임에 키프레임 추가
        keyframeStore.addKeyframe(currentFrame, property, value);
    };
    
    const autoKeyframe = (object: any, property: string) => {
        // 객체 수정 시 자동으로 키프레임 생성
        if (shouldCreateKeyframe(currentFrame, property)) {
            addKeyframe(property, object[property]);
        }
    };
};

아키텍처 설계: 현실적인 접근

기존 코드를 분석한 결과, 다음과 같은 구조로 가는 게 좋겠다:

src/
├── editor/
│   ├── components/
│   │   ├── toolbar/          # 도구 모음
│   │   ├── properties/       # 속성 패널 (Leva 활용)
│   │   ├── timeline/         # 타임라인 + 키프레임
│   │   └── preview/          # Remotion Player
│   ├── store/               # Zustand 상태 관리
│   ├── hooks/               # 커스텀 훅들
│   └── utils/               # 유틸리티 함수들
└── remotion/
    ├── components/          # Remotion 컴포넌트들
    └── compositions/        # 메인 컴포지션

데이터 플로우

sequenceDiagram
    participant U as User
    participant E as Editor
    participant S as Store
    participant R as Remotion
    
    U->>E: 객체 수정
    E->>S: 상태 업데이트
    S->>E: 키프레임 생성
    E->>R: 컴포지션 업데이트
    R->>U: 미리보기 렌더링

현실적인 구현 순서

  1. 기본 구조 잡기: Remotion Player + 기본 UI
  2. 속성 패널: Leva로 간단한 컨트롤부터
  3. 타임라인: 스크러버와 기본 재생 기능
  4. 키프레임: 수동 추가부터 시작
  5. 자동 키프레임: 객체 수정 시 자동 생성
  6. 렌더링: Remotion API로 최종 출력

Remotion 기반 커스텀 에디터 만들기: 남의 코드 뜯어보며 깨달은 현실

시작은 언제나 불만에서

Remotion에서 제공하는 Studio가 내 용도에 맞지 않아서 결국 직접 만들기로 했다. 뭐, 세상에 완벽한 도구는 없으니까. 그런데 막상 시작하려니 막막하더라. 그래서 찾아낸 게 옛날에 만들어진 Remotion 기반 에디터의 참고 자료였다.

문제는 이 코드가 구석기 시대에 만들어진 것 같다는 점이다. 의존성부터 시작해서 아키텍처까지, 현재 내가 설치한 패키지들과는 천지차이다.

의존성 분석: 옛날 것 vs 현재 것

🗑️ 버릴 것들 (jQuery 시대의 유물들)

참고 코드를 보니 jQuery부터 시작해서 온갖 레거시 라이브러리들이 잔뜩 들어있다:

<script src="jquery/3.5.1/jquery.min.js"></script>
<script src="js/libraries/sortable.min.js"></script>
<script src="js/libraries/range-slider.min.js"></script>
<script src="js/libraries/anime.min.js"></script>

이런 걸 보면 정말 격세지감이 든다. 2024년에 jQuery를 쓴다고? React의 가상 DOM이 있는데 왜 직접 DOM을 조작하겠는가.

옛날 라이브러리 현재 대체재 비고
jQuery React + React DOM 당연히 React 쓸 거다
sortable.min.js react-draggable, react-moveable 이미 설치되어 있음
range-slider.min.js HTML5 input[type=“range”] 네이티브가 최고다
anime.min.js GSAP 이미 GSAP 있으니 굳이?

고민해볼 것들

// Fabric.js - 캔버스 라이브러리
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/460/fabric.min.js"></script>

Fabric.js는 강력한 캔버스 라이브러리다. 하지만 Remotion에서는 필요 없다. Remotion이 렌더링을 담당하니까. 캔버스 조작 대신 Remotion 컴포넌트(<AbsoluteFill><Sequence> 등)와 GSAP을 쓸 거다.

graph TD
    A[기존 Motionity] --> B[Fabric.js Canvas]
    B --> C[직접 캔버스 조작]
    
    D[내 Remotion 에디터] --> E[Remotion Components]
    E --> F[GSAP 애니메이션]
    F --> G[Remotion 렌더링]

HTML 구조 분석: 이게 뭘 하는 건지 알아보자

참고 코드의 HTML 구조를 뜯어보니 전형적인 비디오 에디터 레이아웃이다:

<div id="toolbar" class="noselect">
    <!-- 도구 버튼들 -->
</div>

<div id="properties">
    <!-- 속성 패널 -->
    <div class="property-name" data-property="position">
        <span class="property-keyframe"></span>Position
        <img class="freeze-prop" src="assets/freeze.svg">
    </div>
</div>

<div id="canvas-area">
    <canvas id="canvas"></canvas>
</div>

<div id="bottom-area">
    <!-- 타임라인, 레이어 리스트 -->
</div>

핵심 컴포넌트들

  1. Toolbar: 도구 선택 (텍스트, 도형, 미디어 등)
  2. Properties Panel: 선택된 객체의 속성 편집
  3. Canvas Area: 메인 작업 영역 (내 경우엔 Remotion Player)
  4. Timeline: 키프레임과 애니메이션 관리

키프레임 시스템: 이게 진짜 핵심이다

참고 코드에서 가장 중요한 부분은 키프레임 시스템이다. 어떻게 작동하는지 보자:

// ui.js - 패널 업데이트
function updatePanel(selection) {
    if (selection && canvas.getActiveObjects().length == 1) {
        $('#object-specific').html(object_panel);
        updatePanelValues(); // 현재 값으로 입력 필드 업데이트
    }
}

// events.js - 객체 수정 시 자동 키프레임
canvas.on('object:modified', function(e) {
    autoKeyframe(canvas.getActiveObject(), e, selection);
});

실제 구현: 이론과 현실의 괴리

처음엔 이 구조를 그대로 React로 옮기면 될 줄 알았다. 완전히 틀렸다.

jQuery 기반 코드는 DOM을 직접 조작하는 명령형 프로그래밍이다. 반면 React는 상태 기반의 선언형 프로그래밍이다. 이 둘의 패러다임 차이를 무시하고 단순 포팅을 시도했다가 며칠을 날렸다.

// 첫 번째 시도 - 실패작
const PropertyPanel = () => {
    const [selectedObject, setSelectedObject] = useState(null);
    
    // jQuery 스타일로 DOM 직접 조작 시도
    useEffect(() => {
        if (selectedObject) {
            document.getElementById('position-input').value = selectedObject.position;
            document.getElementById('scale-input').value = selectedObject.scale;
        }
    }, [selectedObject]);
    
    // 이런 식으로 하면 안 된다!
};

문제는 React의 상태 관리와 DOM 직접 조작이 충돌한다는 점이다. 입력 필드의 값이 상태와 동기화되지 않아서 예상치 못한 버그들이 속출했다.

올바른 접근: Leva + Zustand 조합

결국 Leva를 활용한 속성 패널과 Zustand로 전역 상태 관리를 하는 방향으로 선회했다:

// 올바른 접근
const PropertyPanel = () => {
    const { selectedObject, updateObject } = useEditorStore();
    const { addKeyframe } = useKeyframes();
    
    const propertyControls = useMemo(() => {
        if (!selectedObject) return {};
        
        return {
            position: {
                value: selectedObject.position,
                min: -1000,
                max: 1000,
                onChange: (value) => {
                    updateObject(selectedObject.id, { position: value });
                    addKeyframe(selectedObject.id, 'position', value);
                }
            },
            scale: {
                value: selectedObject.scale,
                min: 0.1,
                max: 5,
                step: 0.1,
                onChange: (value) => {
                    updateObject(selectedObject.id, { scale: value });
                    addKeyframe(selectedObject.id, 'scale', value);
                }
            }
        };
    }, [selectedObject]);
    
    return <Leva controls={propertyControls} />;
};

이렇게 하니까 상태 동기화 문제가 말끔히 해결됐다. Leva가 입력 필드와 상태를 자동으로 동기화해주니까.

타임라인 구현: 생각보다 복잡한 놈

참고 코드의 타임라인을 보면 단순해 보인다. 그냥 시간축에 키프레임 점들이 찍혀있는 것 같은데, 막상 구현하려니 지옥이었다.

첫 번째 난관: 시간과 프레임의 변환

// 이런 간단한 계산도 버그 투성이였다
const timeToFrame = (time: number, fps: number) => {
    return Math.round(time * fps);
};

const frameToTime = (frame: number, fps: number) => {
    return frame / fps;
};

문제는 부동소수점 연산의 정밀도다. 0.1 + 0.2 !== 0.3 같은 JavaScript의 고질적인 문제가 타임라인에서 키프레임 위치 계산을 망쳤다.

두 번째 난관: 드래그 앤 드롭

키프레임을 드래그해서 시간을 조정하는 기능을 구현하려고 했는데, 이게 생각보다 까다롭다:

const KeyframeComponent = ({ keyframe, onTimeChange }) => {
    const [isDragging, setIsDragging] = useState(false);
    const [dragStart, setDragStart] = useState(0);
    
    const handleMouseDown = (e) => {
        setIsDragging(true);
        setDragStart(e.clientX);
        
        // 전역 마우스 이벤트 리스너 등록
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
    };
    
    const handleMouseMove = (e) => {
        if (!isDragging) return;
        
        const deltaX = e.clientX - dragStart;
        const timelineWidth = timelineRef.current.offsetWidth;
        const totalDuration = composition.durationInFrames / composition.fps;
        
        // 픽셀을 시간으로 변환
        const deltaTime = (deltaX / timelineWidth) * totalDuration;
        const newTime = Math.max(0, keyframe.time + deltaTime);
        
        onTimeChange(keyframe.id, newTime);
    };
    
    // 메모리 누수 방지를 위한 cleanup
    const handleMouseUp = () => {
        setIsDragging(false);
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
    };
    
    return (
        <div 
            className="keyframe"
            onMouseDown={handleMouseDown}
            style={{ left: `${(keyframe.time / totalDuration) * 100}%` }}
        />
    );
};

이런 식으로 구현했는데, 실제로 써보니 끔찍했다. 드래그할 때 버벅거리고, 가끔 키프레임이 엉뚱한 곳으로 튀어나가고, 마우스를 빠르게 움직이면 드래그가 끊어지기도 했다.

해결책: react-draggable 활용

결국 직접 구현하는 걸 포기하고 react-draggable을 썼다:

import Draggable from 'react-draggable';

const KeyframeComponent = ({ keyframe, onTimeChange }) => {
    const timelineWidth = useTimelineWidth();
    const totalDuration = useCompositionDuration();
    
    const handleDrag = (e, data) => {
        const timelineRect = timelineRef.current.getBoundingClientRect();
        const relativeX = data.x;
        const newTime = (relativeX / timelineWidth) * totalDuration;
        
        onTimeChange(keyframe.id, Math.max(0, newTime));
    };
    
    return (
        <Draggable
            axis="x"
            bounds="parent"
            onDrag={handleDrag}
            position={{ x: (keyframe.time / totalDuration) * timelineWidth, y: 0 }}
        >
            <div className="keyframe" />
        </Draggable>
    );
};

훨씬 깔끔하고 안정적이다. 역시 바퀴를 다시 발명할 필요는 없다.

사용자 경험: 이론과 현실의 또 다른 괴리

코드 구현은 어찌저찌 됐는데, 실제로 써보니 UX가 재앙이었다.

키프레임 추가: 언제, 어떻게?

처음엔 After Effects처럼 속성을 변경할 때마다 자동으로 키프레임을 추가하도록 했다:

const handlePropertyChange = (property, value) => {
    updateObject(selectedObject.id, { [property]: value });
    addKeyframe(selectedObject.id, property, value); // 항상 키프레임 추가
};

결과는? 키프레임 지옥이었다. 슬라이더를 조금만 움직여도 키프레임이 수십 개씩 생성됐다. 타임라인이 점들로 도배되더라.

해결책: 의도적 키프레임 생성

결국 사용자가 명시적으로 키프레임을 추가할 때만 생성하도록 바꿨다:

const PropertyPanel = () => {
    const [isKeyframeMode, setIsKeyframeMode] = useState(false);
    
    const handlePropertyChange = (property, value) => {
        updateObject(selectedObject.id, { [property]: value });
        
        // 키프레임 모드일 때만 키프레임 추가
        if (isKeyframeMode) {
            addKeyframe(selectedObject.id, property, value);
        }
    };
    
    return (
        <div>
            <button 
                className={`keyframe-toggle ${isKeyframeMode ? 'active' : ''}`}
                onClick={() => setIsKeyframeMode(!isKeyframeMode)}
            >
                🔑 키프레임 모드
            </button>
            
            <Leva controls={propertyControls} />
        </div>
    );
};

이렇게 하니까 훨씬 제어 가능해졌다. 사용자가 애니메이션을 만들고 싶을 때만 키프레임 모드를 켜고 작업하면 된다.

성능 최적화: 렌더링 지옥 탈출기

초기 버전을 테스트해보니 끔찍하게 느렸다. 속성 하나 바꿀 때마다 전체 컴포지션이 다시 렌더링됐다.

문제 진단: 불필요한 리렌더링

// 문제가 된 코드
const VideoEditor = () => {
    const [objects, setObjects] = useState([]);
    const [currentFrame, setCurrentFrame] = useState(0);
    
    // 이렇게 하면 objects가 바뀔 때마다 전체 리렌더링
    const composition = useMemo(() => {
        return (
            <Composition
                id="main"
                component={MainComposition}
                durationInFrames={300}
                fps={30}
                width={1920}
                height={1080}
                defaultProps={{ objects, currentFrame }}
            />
        );
    }, [objects, currentFrame]); // 의존성이 너무 자주 바뀜
    
    return <Player component={composition} />;
};

objects 배열이 바뀔 때마다 전체 컴포지션이 새로 생성됐다. 객체 하나의 위치만 바뀌어도 말이다.

해결책: 세밀한 상태 분리

// 개선된 코드
const VideoEditor = () => {
    const composition = useMemo(() => (
        <Composition
            id="main"
            component={MainComposition}
            durationInFrames={300}
            fps={30}
            width={1920}
            height={1080}
        />
    ), []); // 의존성 없음 - 한 번만 생성
    
    return <Player component={composition} />;
};

// 컴포지션 내부에서 상태 구독
const MainComposition = () => {
    const objects = useEditorStore(state => state.objects);
    const currentFrame = useCurrentFrame();
    
    return (
        <AbsoluteFill>
            {objects.map(obj => (
                <ObjectRenderer 
                    key={obj.id} 
                    object={obj} 
                    frame={currentFrame} 
                />
            ))}
        </AbsoluteFill>
    );
};

이렇게 하니까 컴포지션 자체는 한 번만 생성되고, 내부에서만 상태 변화에 반응하게 됐다.

React.memo로 추가 최적화

const ObjectRenderer = React.memo(({ object, frame }) => {
    const animatedProps = useAnimatedProperties(object, frame);
    
    return (
        <AbsoluteFill style={animatedProps}>
            {object.type === 'text' && <TextObject {...object} />}
            {object.type === 'image' && <ImageObject {...object} />}
            {object.type === 'shape' && <ShapeObject {...object} />}
        </AbsoluteFill>
    );
}, (prevProps, nextProps) => {
    // 객체나 프레임이 바뀌지 않으면 리렌더링 안 함
    return prevProps.object === nextProps.object && 
           prevProps.frame === nextProps.frame;
});

애니메이션 보간: GSAP과 Remotion의 만남

키프레임 사이의 애니메이션을 어떻게 처리할지가 가장 큰 고민이었다. Remotion의 interpolate를 쓸지, GSAP의 gsap.utils.interpolate를 쓸지.

Remotion 방식

import { interpolate, useCurrentFrame } from 'remotion';

const AnimatedObject = ({ keyframes }) => {
    const frame = useCurrentFrame();
    
    const position = interpolate(
        frame,
        keyframes.map(kf => kf.frame),
        keyframes.map(kf => kf.position),
        { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
    );
    
    return <div style={{ transform: `translateX(${position}px)` }} />;
};

GSAP 방식

import { gsap } from 'gsap';

const useAnimatedProperties = (object, frame) => {
    return useMemo(() => {
        const timeline = gsap.timeline();
        
        object.keyframes.forEach((keyframe, index) => {
            if (index === 0) {
                timeline.set({}, keyframe.properties, keyframe.time);
            } else {
                timeline.to({}, keyframe.properties, keyframe.time);
            }
        });
        
        // 현재 프레임에서의 값 계산
        timeline.progress(frame / totalFrames);
        return timeline.getChildren()[0].vars;
    }, [object.keyframes, frame]);
};

결론: 하이브리드 접근

결국 둘 다 쓰기로 했다:

  • 단순한 보간: Remotion의 interpolate (성능이 좋음)
  • 복잡한 애니메이션: GSAP (이징, 베지어 곡선 등)
const getAnimatedValue = (keyframes, frame, property) => {
    // 키프레임이 2개 이하면 Remotion interpolate 사용
    if (keyframes.length <= 2) {
        return interpolate(
            frame,
            keyframes.map(kf => kf.frame),
            keyframes.map(kf => kf[property])
        );
    }
    
    // 복잡한 애니메이션은 GSAP 사용
    return gsapInterpolate(keyframes, frame, property);
};

실제 사용해보니: 예상치 못한 문제들

1. 메모리 누수

타임라인을 스크럽할 때마다 메모리 사용량이 계속 증가했다. 원인은 GSAP 타임라인을 제대로 정리하지 않았기 때문이었다:

// 문제가 된 코드
const useAnimatedProperties = (object, frame) => {
    const timeline = gsap.timeline(); // 매번 새로 생성
    // ... 애니메이션 설정
    return animatedProps;
};

// 해결책
const useAnimatedProperties = (object, frame) => {
    const timelineRef = useRef();
    
    useEffect(() => {
        if (timelineRef.current) {
            timelineRef.current.kill(); // 이전 타임라인 정리
        }
        timelineRef.current = gsap.timeline();
        // ... 애니메이션 설정
        
        return () => {
            if (timelineRef.current) {
                timelineRef.current.kill();
            }
        };
    }, [object.keyframes]);
    
    // ...
};

2. 키프레임 정렬 문제

사용자가 키프레임을 드래그해서 순서를 바꿀 때, 시간 순으로 정렬되지 않으면 애니메이션이 이상하게 작동했다:

const addKeyframe = (objectId, property, value) => {
    const newKeyframe = {
        id: generateId(),
        time: currentTime,
        [property]: value
    };
    
    setObjects(objects => 
        objects.map(obj => {
            if (obj.id === objectId) {
                const updatedKeyframes = [...obj.keyframes, newKeyframe]
                    .sort((a, b) => a.time - b.time); // 시간 순 정렬 필수
                
                return { ...obj, keyframes: updatedKeyframes };
            }
            return obj;
        })
    );
};

3. 실시간 미리보기 성능

속성을 조정할 때 실시간으로 미리보기를 보여주려고 했는데, 너무 자주 업데이트되면 브라우저가 버벅거렸다:

// 디바운싱으로 해결
const useDebounce = (value, delay) => {
    const [debouncedValue, setDebouncedValue] = useState(value);
    
    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);
        
        return () => clearTimeout(handler);
    }, [value, delay]);
    
    return debouncedValue;
};

const PropertyPanel = () => {
    const [tempValue, setTempValue] = useState(0);
    const debouncedValue = useDebounce(tempValue, 100);
    
    useEffect(() => {
        updateObject(selectedObject.id, { position: debouncedValue });
    }, [debouncedValue]);
    
    // ...
};

결론: 완벽한 도구는 없다

몇 주간의 삽질 끝에 어느 정도 쓸 만한 에디터가 나왔다. 하지만 여전히 부족한 점이 많다:

잘된 점들

  • 모듈화된 구조: 각 기능이 독립적으로 작동
  • 성능 최적화: 불필요한 리렌더링 최소화
  • 사용자 경험: 직관적인 키프레임 관리

아직 부족한 점들

  • 복잡한 애니메이션: 베지어 곡선, 이징 함수 지원 부족
  • 실행 취소/다시 실행: 상태 관리가 복잡해서 미완성
  • 파일 저장/불러오기: JSON 직렬화 문제

깨달은 점들

  1. 남의 코드의 구조를 그대로 차용하면 안 된다: 패러다임이 다르면 완전히 다시 설계해야 함
  2. 성능은 나중에 생각하면 안 된다: 처음부터 고려해야 함
  3. 사용자 경험이 가장 중요하다: 기능이 많아도 쓰기 어려우면 의미 없음

결국 완벽한 도구는 없다는 걸 다시 한 번 깨달았다. Remotion Studio가 답답해서 시작한 프로젝트였는데, 막상 만들어보니 왜 기존 도구들이 그런 식으로 만들어졌는지 이해가 됐다.

그래도 이 과정에서 Remotion과 React, GSAP에 대해 많이 배웠으니 의미 있는 삽질이었다고 생각한다. 언젠가는 정말 쓸 만한 에디터를 만들 수 있을 거다… 아마도.

아.마.도