jQuery기반의 코드들을 React로 변환하기: 웹 비디오 에디터 현대화 삽질기

My subtle dyslexic arse with a lack of dopamine gave up proofreading after multiple tries. If you see grammatical errors or some awkward sentences, move on. Use the context clues to understand my post, thanks.

우연히 발견한 웹 기반 모션 그래픽 오픈소스…근데 코드가 왜 이래?

어느 날 우연히 오픈소스로 굴려지다 2년 전 멈춰진 웹 기반 모션 그래픽 에디터를 발견했다. 브라우저에서 돌아가는 비디오 에디터라니, 보자마자 “오, 이거 꽤 괜찮네?” 싶었다. 뭔가 미래적이고 쿨해 보였거든. 당장이라도 이걸로 뭔가 만들어보고 싶다는 생각이 들 정도였으니까.

근데 코드를 까보는 순간… 아, 이게 바로 그 유명한 “겉바속촉”이 아니라 “겉만 번지르르한” 케이스구나 싶었다. 쓰여진 디펜던시도 대부분 업데이트가 종료된 것들이고 그나마 유지가 되는 것들도 꽤나 옛날 버젼이었다. 무엇보다…

생짜로 html에 jquery로 기동하는 코드라고? 엣?

// 실제로 마주하고 뒷목 잡게 만든 레거시 코드의 일부
$('#timeline').on('click', function() {
    var currentTime = parseFloat($(this).data('time'));
    // 전역 변수 파티 시작
    window.globalTimelineData.currentFrame = currentTime * 30;
    updateAllLayers(); // 이게 뭘 하는 함수인지는 아무도 모른다
    refreshPreview();
});

some-old-file.js

[이슈 1: 레거시 코드 복잡성 - 재배치된 위치]

코드는 그야말로 jQuery와 바닐라 JS의 대환장 파티였다. 전역 변수는 기본 옵션이고, DOM 조작은 여기저기 흩어져 있고, 타입 안전성? 그게 뭔가요, 먹는 건가요? UI.js, functions.js 같은 파일들은 서로가 서로를 물고 뜯는, 스파게티 코드의 정석을 보여주고 있었다. 이걸 보고 있자니 ‘이걸 만든 사람은 대체 무슨 생각이었을까?’ 하는 고찰과 함께 깊은 빡침이 동시에 밀려왔다. 근데 뭐, 어차피 오픈소스 중 대부분의 엔딩은 이런 거지만. 누굴 탓하리오. 내가 이 종료된 오픈소스 에디터의 개발자도 아니고 스폰서도 아닌데.

이 종결된 프로젝트을 내가 다시 재활용 해서 재구축 할 수 있을까?

“이거 그냥 다시 짜는 게 낫지 않나?” 라는 생각이 뇌리를 1초 정도 스쳤지만, 이왕 시작한 거 제대로 길들여보고 싶다는 오기가 생겼다. 그래서 거대한 마이그레이션 계획을 세웠지.

[기술 스택 결론 - 재배치된 위치]

일단 낡아빠진 연장부터 최신으로 바꾸기로 했다.

  • Before: jQuery + Vanilla JS + 전역변수 지옥
  • After: React + TypeScript + Vite + Remotion + GSAP + Zustand

React로 컴포넌트 기반 아키텍처를 만들고, TypeScript로 지긋지긋한 undefined is not a function 에러와 작별하기로 했다. 상태 관리는 가볍고 직관적인 Zustand가 딱이었고, 애니메이션은 역시 GSAP, 비디오 렌더링은 React와 찰떡궁합인 Remotion을 선택했다. 단순한 리팩토링이 아니라, 거의 재창조 수준의 계획이었다. 아니다. 말은 바로하자. 걍 재창조다.

[프로젝트 구조 및 데이터 흐름 - 재배치된 위치]

스파게티 같던 구조를 아래처럼 깔끔하게 정리하는 게 목표였다. 보기만 해도 마음이 편안해지지 않나?

src/editor/
├── components/           # UI 컴포넌트 (이제 각자 방이 있다)
│   ├── timeline/
│   ├── layers/
│   └── preview/
├── core/                 # 핵심 로직 (두뇌 역할)
│   ├── TimelineController.ts
│   └── AnimationManager.ts
├── store/                # 상태 관리 (모든 정보는 여기로)
│   ├── editorStore.ts
│   └── layerStore.ts
├── hooks/                # 재사용 가능한 커스텀 훅
└── types/                # 타입 정의 (안전제일)

본격적인 삽질 지옥의 문이 열리다

계획은 완벽(했을진 의문이지만), 현실은 언제나 상상을 초월한다.

[이슈 2: 불완전한 초기 가이드 - 재배치된 위치]

가장 큰 문제는, 참고할 만한 제대로 된 문서나 가이드가 거의 없었다는 거다. 제공된 초기 코드는 그냥 뼈대만 앙상한 스켈레톤 수준이었고, 실제 동작하는 로직은 거의 없었다. 이건 뭐 ‘알아서 잘 해봐’ 수준? 결국 기존 코드를 한 줄 한 줄 해독하면서 의도를 파악해야 했다. 마치 고대 상형문자를 해석하는 고고학자가 된 기분이었다.

[이슈 3: 복잡한 타임라인 로직 - 재배치된 위치]

특히 타임라인 로직은…말해 뭣하나. 키프레임 관리, 실시간 애니메이션 동기화, 줌/스크럽 기능… 하나하나가 다 복잡성의 끝판왕이었다. 이걸 jQuery와 전역 변수로 어떻게 만들 생각을 했는지, 새삼 원작자에게 경외심이 들 정도였다.

이 암호 같은 코드를 해독해서, 아래처럼 명확한 React 코드로 바꾸는 과정은 정말… 쉽지 않았다.

// Before: 이게 대체 뭔 소리야...
function updateTimelinePosition(t) {
    var pos = (t / totalDuration) * timelineWidth;
    $('#playhead').css('left', pos + 'px');
}

// After: 훨씬 명확하고 예측 가능해졌다.
const useTimelinePosition = () => {
  const { currentTime, totalDuration } = useTimelineStore();
  const timelineWidth = useEditorStore(s => s.timelineWidth);

  const position = useMemo(() => {
    if (totalDuration === 0) return 0;
    return (currentTime / totalDuration) * timelineWidth;
  }, [currentTime, totalDuration, timelineWidth]);

  return position;
};

useTimelinePosition.ts

광명 찾기: 해결책과 깨달음

[아키텍처 결론 - 재배치된 위치] 수많은 삽질 끝에, 드디어 빛이 보이기 시작했다.

  • 모듈화된 컴포넌트: 기능별로 컴포넌트를 쪼개니 의존성이 줄고 유지보수가 미친 듯이 편해졌다. 레고 블록처럼 필요할 때마다 가져다 쓰면 되니까.
  • 중앙 집중화된 상태 관리: Zustand로 상태를 한 곳에서 관리하니 데이터 흐름이 명확해졌다. 더 이상 “대체 이 값은 어디서 바뀐 거야?” 하면서 온갖 파일을 다 뒤지는 짓은 안 해도 됐다.
  • 타입 안전성: TypeScript 덕분에 런타임 에러가 확 줄었고, 복잡한 객체도 자신 있게 다룰 수 있게 됐다. 이건 정말 혁명이다.

데이터 흐름도 이렇게 명확하게 정리됐다. 누가 봐도 어디서 무슨 일이 일어나는지 알 수 있겠지?

graph TD
    A[사용자 인터랙션] --> B[React 컴포넌트]
    B --> C[커스텀 훅]
    C --> D[Zustand 스토어]
    D --> E[핵심 컨트롤러]
    E --> F[GSAP 애니메이션] & G[Remotion 렌더링]

또 다른 삽질의 시작

이 운영이 종료된 고대 렐릭 프로젝트인 웹 기반 모션 그래픽 에디터를 발견했을 때의 첫 느낌은 “오, 이거 꽤 괜찮네?” 였다. 브라우저에서 돌아가는 비디오 에디터라니, 뭔가 내 작업 플로우에 잘 활용할 수 있을 거 같은 그런 기분.

근데 코드를 까보는 순간… 아…

// 실제로 마주한 레거시 코드의 일부
$('#timeline').on('click', function() {
    var currentTime = parseFloat($(this).data('time'));
    window.globalTimelineData.currentFrame = currentTime * 30;
    updateAllLayers();
    refreshPreview();
});

jQuery 전역 변수는 기본이고, DOM 조작은 여기저기 흩어져 있고, 타입 안전성? 그게 뭔가요?

Phase 1: 코드베이스를 알고 나를 알아야 백전백승

먼저 코드베이스 분석부터 시작했다. UI.js, functions.js 같은 파일들을 하나씩 뜯어보면서 이 괴물이 어떻게 돌아가는지 파악하는 게 우선이었다.

Legacy Codebase 구조 (대충 이런 느낌)
├── UI.js (2000줄짜리 괴물)
├── functions.js (온갖 유틸리티 함수들)
├── timeline.js (타임라인 로직)
└── layers.js (레이어 관리)

문제는 이 파일들이 서로 엄청나게 의존하고 있다는 거였다. A 함수가 B 함수를 호출하고, B 함수는 C의 전역 변수를 수정하고, C는 다시 A의 DOM을 건드리는… 꼬일대로 꼬인 스파게티. 머치라잌 내 머릿속처럼…ADHD인에게 이건 너무 가혹하다.

Phase 2: 탈출 계획 수립

“이거 그냥 다시 짜는 게 나을 것 같은데?” 라는 생각이 들었지만, 기존 기능은 유지하면서 현대화해야 한다는 조건이 있었다. 그래서 마이그레이션 전략을 세웠다.

기술 스택 선택

Before: jQuery + Vanilla JS + 전역변수 지옥
After: React + TypeScript + Vite + 상태관리
  • React: 컴포넌트 기반으로 UI 모듈화
  • TypeScript: 타입 안전성 확보 (더 이상 undefined is not a function 같은 에러는 싫다)
  • Vite: 빠른 개발 서버 (HMR 짱짱맨)
  • Zustand: 가벼운 상태 관리 (Redux는 너무 무겁고, Context API는 너무 원시적)
  • GSAP: 고성능 애니메이션 (기존에도 GSAP 썼는데 제대로 활용 못함)
  • Remotion: 비디오 렌더링 (React로 비디오 만들기)

아키텍처 설계

새로운 구조 (훨씬 깔끔)
src/editor/
├── components/           # UI 컴포넌트들
│   ├── timeline/        # 타임라인 관련
│   ├── layers/          # 레이어 관리
│   ├── preview/         # 프리뷰 캔버스
│   └── properties/      # 속성 패널
├── core/                # 핵심 비즈니스 로직
│   ├── TimelineController.ts
│   ├── AnimationManager.ts
│   └── VideoExporter.ts
├── store/               # 상태 관리
│   ├── editorStore.ts
│   ├── layerStore.ts
│   └── timelineStore.ts
├── hooks/               # 커스텀 훅들
├── types/               # 타입 정의
└── utils/               # 유틸리티

Phase 3: 실제 구현 - 여기서부터가 진짜 고난의 시작

이슈 1: 레거시 코드 해독

기존 코드를 보면서 “이게 대체 뭘 하는 코드지?” 하면서 몇 시간씩 보낸 적이 한두 번이 아니다. 특히 타임라인 관련 로직은 정말 암호 같았다.

// 기존 코드 (뭔 소린지 모르겠다)
function updateTimelinePosition(t) {
    var pos = (t / totalDuration) * timelineWidth;
    if (pos > maxPos) pos = maxPos;
    $('#playhead').css('left', pos + 'px');
    currentTimeGlobal = t;
    window.dispatchEvent(new CustomEvent('timeupdate'));
}

이걸 React로 바꾸면:

// 새로운 코드 (훨씬 명확하다)
const useTimelinePosition = () => {
  const { currentTime, totalDuration, timelineWidth } = useEditorStore();
  
  const position = useMemo(() => {
    return Math.min(
      (currentTime / totalDuration) * timelineWidth,
      timelineWidth
    );
  }, [currentTime, totalDuration, timelineWidth]);
  
  return position;
};

이슈 2: 상태 관리의 복잡성

기존에는 전역 변수로 때려박았던 상태들을 제대로 관리해야 했다. 타임라인 상태, 레이어 상태, 애니메이션 상태… 이것들이 서로 복잡하게 얽혀있었다.

// Zustand 스토어 설계
interface EditorState {
  // 타임라인 관련
  currentTime: number;
  totalDuration: number;
  isPlaying: boolean;
  
  // 레이어 관련
  layers: Layer[];
  selectedLayerId: string | null;
  
  // 애니메이션 관련
  keyframes: Keyframe[];
  
  // 액션들
  setCurrentTime: (time: number) => void;
  addLayer: (layer: Layer) => void;
  addKeyframe: (keyframe: Keyframe) => void;
}

이슈 3: 타임라인 로직의 지옥

가장 머리 아팠던 부분이 타임라인 로직이었다. 키프레임 관리, 실시간 애니메이션 동기화, 줌/스크럽 기능… 하나하나가 다 복잡했다.

// TimelineController 클래스 일부
class TimelineController {
  private gsapTimeline: gsap.core.Timeline;
  
  constructor(private store: EditorStore) {
    this.gsapTimeline = gsap.timeline({ paused: true });
    this.setupEventListeners();
  }
  
  seek(time: number) {
    this.gsapTimeline.seek(time);
    this.store.setCurrentTime(time);
    this.updatePreview();
  }
  
  play() {
    this.gsapTimeline.play();
    this.store.setIsPlaying(true);
  }
  
  // ... 더 많은 메서드들
}

데이터 흐름 설계

복잡한 상태들이 어떻게 흘러가는지 정리하는 것도 중요했다.

graph TD
    A[User Interaction] --> B[React Components]
    B --> C[Custom Hooks]
    C --> D[Zustand Stores]
    D --> E[Core Controllers]
    E --> F[GSAP Animation]
    E --> G[Remotion Rendering]
    F --> H[DOM Updates]
    G --> I[Video Preview]
    
    subgraph "State Management"
        D1[EditorStore]
        D2[LayerStore] 
        D3[TimelineStore]
        D1 <--> D2
        D2 <--> D3
    end
    
    subgraph "Animation Pipeline"
        E1[TimelineController]
        E2[AnimationManager]
        E1 --> E2
        E2 --> F
    end

해결책, 그리고 Lesson

1. 모듈화의 힘

기존 코드의 가장 큰 문제는 모든 게 한 덩어리로 뭉쳐있다는 거였다. 이걸 기능별로 쪼개고, 각각을 독립적인 컴포넌트로 만드니까 훨씬 관리하기 쉬워졌다.

2. 타입 안전성의 중요함

TypeScript 도입하면서 런타임 에러가 확실히 줄어들었다. 특히 복잡한 데이터 구조를 다룰 때 타입 체크가 있으니까 안심이 됐다.

interface Keyframe {
  id: string;
  time: number;
  value: any;
  easingType: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
  layerId: string;
}

3. 상태 중앙 집중화

Zustand로 상태를 중앙에서 관리하니까 디버깅도 쉽고, 데이터 흐름도 예측 가능해졌다. 더 이상 “어디서 이 값이 바뀌는 거지?” 하면서 헤매지 않아도 된다.

4. 성능 최적화

GSAP + Remotion 조합으로 애니메이션 성능이 확실히 좋아졌다. 기존에는 CSS transition으로 때워놨던 부분들을 제대로 된 애니메이션 엔진으로 바꾸니까 부드러워졌다.

컴포넌트 관계도

최종적으로 만들어진 컴포넌트 구조는 이런 식이다.

파일 구조 (ASCII)

src/editor/
├── components/           # UI 컴포넌트
   ├── timeline/ 
 	├── Timeline
 	   ├── TimelineControls (재생/정지/등등)
 	   ├── TimelineTrack[] ( 레이어별 트랙)
 	   └── Keyframe[] (키프레임들)# 타임라인 관련
   ├── layers/ 
 	├── LayerManager
 	   └── LayerList
 	       └── LayerItem[] (개별 레이어)# 레이어 관리
   ├── preview/         # 프리뷰 캔버스
	├── PreviewCanvas
	   └── RemotionPlayer (비디오 프리뷰)
   └── properties/      # 속성 패널
	└── PropertyPanel
	    └── PropertyControls[] (선택된 요소 속성)
├── core/                # 핵심 로직
   ├── TimelineController.ts
   ├── AnimationManager.ts
   └── VideoExporter.ts
├── store/               # 상태 관리
   ├── editorStore.ts
   ├── layerStore.ts
   └── timelineStore.ts
├── hooks/               # 커스텀 
├── types/               # 타입 정의
└── utils/               # 유틸리티

아직 남은 숙제들

물론 완벽하지는 않다. 아직 해결해야 할 이슈들이 좀 있다:

  • 성능: 레이어가 많아지면 여전히 버벅임
  • UX: 키프레임 편집 인터페이스가 좀 불편함
  • 익스포트: 비디오 렌더링 속도 개선 필요
  • 브라우저 호환성: Safari에서 일부 기능 안 됨

결론

이번 프로젝트를 통해서 느낀 건, 레거시 코드 마이그레이션이 생각보다 쉽지는 않았다는 것.

기술 스택 결론

Frontend: React + TypeScript + Vite
Animation: GSAP + @gsap/react
Video: Remotion + @remotion/player
State: Zustand
UI: @dnd-kit (드래그앤드롭)

아키텍처 결론

  • 모듈화된 컴포넌트 구조: 각 기능별로 독립적인 컴포넌트 분리
  • Type-Safe 개발: TypeScript를 통한 타입 안전성 확보
  • 상태 중앙 집중화: Zustand를 통한 예측 가능한 상태 관리
  • 성능 최적화: GSAP + Remotion을 통한 고성능 애니메이션 렌더링

여기까진 고려중이나 현재 이 프로젝트 구축은 미완성이다. 애초에 자체툴을 만들려는 목적이 좀 더 원할한 Remotion 영상 제작 워크플로우를 위한 것이었으니 툴을 만드는 데 시간을 쏟는 게 어느 선까지 효용성이 있는지 확신이 없어서인 이유도 있고…왠지 이거 만들다가 어디서의 갓개발자가 딱 내 입맛에 맞는 statement timeline management를 쏙쏙 담아낸 에디터를 내지 않을까…

그럼 난 그냥 그걸 줏어다 쓰면 되니까 굳이 이거에 집중해야할까 싶기도 하다(ㅋㅋㅋ)

아니, 차라리 이럴 시간에 걍 다빈치 리졸브나 애프터 이펙트를 배울까…아니 근데 비트에 맞게 미디 파일과 음향에 맞춰 컷편집 다이나믹하게 하려면 결국 리액트 코드로 하는 게 짱이잖아…아, 모르겠다!

여튼 만약, 혹시 이 프로젝트가 계속 진행이 된다면 다음에 해야할 일들은 아래의 챌린지겠지.

NEXT CHALLENGES

  • [ ] 타임라인 기반 애니메이션 편집
  • [ ] 레이어 관리 및 키프레임 애니메이션
  • [ ] 실시간 프리뷰 및 비디오 export
  • [ ] 확장 가능하고 유지보수 가능한 아키텍처

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

2025-01-16-implementing-canvas-animation-to-remotion-kr 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-how-i-made-podcast-interview-with-ai-virtual-idol KR-remotion-and-shader [[ _notes/KR/WebDevelopment/KR-thepain-of-animation-debugging ]]