애니메이션 디버깅 툴 만들기-for-gsap
나를 위한 애니메이션 디버깅 툴 만들기
문제의 발단: 완벽한 툴이 내 환경에는 쓸모없다는 현실
Theatre.js, anime.js, p5.js, gsap를 이리저리 둘러보았다. 그런데 직관적으로 애프터 이펙트마냥 키프레임을 삽입 후 내가 이리저리 드래그 해보며 맘에 드는 위치값을 지정하지 못한다는 것은 생각보다 더 번거롭다.
드래그 앤 드롭으로 요소들을 이리저리 옮기고, 타임라인을 시각적으로 조작하고, 실시간으로 애니메이션을 미세 조정할 수 있는 걸 구현한 건 theatre.js라서 이걸 좀 파봤다.
graph TD
A[Theatre.js Studio] --> B[시각적 편집]
A --> C[타임라인 제어]
A --> D[실시간 미리보기]
E[내 스택] --> F[Remotion]
E --> G[GSAP]
E --> H[P5.js]
E --> I[Three.js]
B -.-> J[변환 작업 필요]
C -.-> J
D -.-> J
J --> K[비디오 렌더링]
문제는 이 완벽한 툴이 내 개발 환경과는 상극이라는 점이다. 내가 만드는 애니메이션들은 최종적으로 Remotion을 통해 비디오로 렌더링되어야 한다. Theatre.js에서 아무리 이상적인 위치값과 이징값을 찾아도, 그걸 Remotion + GSAP + P5 + Three.js 스택에 맞춰 변환하는 작업이 필요하다.
이게 얼마나 품이 드는 일인지…ㅠㅠ
그래서 만들었다: 맞춤형 디버깅 시스템
🎯 핵심 요구사항 정리
내가 필요로 하는 기능들을 정리해보면 아래와 같다.
| 기능 | 설명 | 우선순위 |
|---|---|---|
| 드래그 앤 드롭 | 요소를 마우스로 직접 이동 | 🔥🔥🔥 |
| 실시간 값 표시 | 변경되는 CSS 값을 팝업으로 표시 | 🔥🔥🔥 |
| 타임라인 제어 | 정확한 타이밍에 정지/재생 | 🔥🔥 |
| 디버그 박스 | 모든 요소에 경계선 표시 | 🔥 |
🛠️ 상태 관리부터 시작하자
먼저 디버깅에 필요한 상태들을 Zustand로 관리하는 스토어를 만들었다:
// debugStore.ts
import { create } from 'zustand';
interface DebugState {
selectedElement: HTMLElement | null;
currentTime: number;
isPlaying: boolean;
showDebugBoxes: boolean;
elementProperties: {
top: string;
left: string;
scale: number;
rotation: number;
};
}
export const useDebugStore = create<DebugState>((set) => ({
selectedElement: null,
currentTime: 0,
isPlaying: false,
showDebugBoxes: false,
elementProperties: {
top: '0%',
left: '0%',
scale: 1,
rotation: 0
},
// ... 액션들
}));
상태 관리는 단순하게 가져갔다. 복잡한 건 나중에 필요할 때 추가하면 된다.
🎮 타임라인 컨트롤러: 프레임 단위로 조작하기
const TimelineControls = () => {
const { currentTime, isPlaying, setCurrentTime, togglePlay } = useDebugStore();
return (
<div className={styles.timelineControls}>
<input
type="range"
min="0"
max="5"
step="0.1"
value={currentTime}
onChange={(e) => setCurrentTime(parseFloat(e.target.value))}
className={styles.timelineSlider}
/>
<div className={styles.controlButtons}>
<button onClick={() => setCurrentTime(Math.max(0, currentTime - 0.1))}>
⏪
</button>
<button onClick={togglePlay}>
{isPlaying ? '⏸️' : '▶️'}
</button>
<button onClick={() => setCurrentTime(Math.min(5, currentTime + 0.1))}>
⏩
</button>
</div>
<span className={styles.timeDisplay}>
{currentTime.toFixed(1)}s
</span>
</div>
);
};
이 부분에서 중요한 건 GSAP 타임라인과의 동기화다. 디버그 컨트롤에서 시간을 조작하면 실제 애니메이션도 해당 지점으로 이동해야 한다.
GSAP와의 동기화: 여기서 진짜 문제가 시작된다
타임라인 동기화 구현
useEffect(() => {
const timeline = gsap.timeline({ paused: true });
// 기존 애니메이션 설정
timeline
.to(textRef.current, {
duration: 1.5,
top: '30%',
left: '20%',
scale: 1.5,
fontSize: '30px',
ease: 'power2.out'
})
.to(textRef.current, {
duration: 1.5,
top: '10%',
left: '20%',
scale: 1,
fontSize: '20px',
rotation: 360,
ease: 'power2.inOut'
});
// 디버그 컨트롤과 동기화
if (debugStore.isPlaying) {
timeline.play();
} else {
timeline.pause();
}
timeline.seek(debugStore.currentTime);
}, [debugStore.isPlaying, debugStore.currentTime]);
여기서 핵심은 timeline.seek(debugStore.currentTime)이다. 이 한 줄로 타임라인의 특정 지점으로 점프할 수 있다.
드래그 앤 드롭: 직관적 조작의 구현
드래그 기능은 생각보다 까다롭다. 단순히 마우스 이벤트만 처리하면 되는 게 아니라, 애니메이션 중인 요소의 현재 상태를 정확히 파악해야 한다.
const makeDraggable = (element: HTMLElement) => {
let isDragging = false;
let startX: number, startY: number;
let elementX: number, elementY: number;
const handleMouseDown = (e: MouseEvent) => {
isDragging = true;
debugStore.setSelectedElement(element);
// 현재 transform 값 파싱
const computedStyle = window.getComputedStyle(element);
const matrix = new DOMMatrix(computedStyle.transform);
startX = e.clientX;
startY = e.clientY;
elementX = matrix.m41; // translateX
elementY = matrix.m42; // translateY
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newX = elementX + deltaX;
const newY = elementY + deltaY;
// 퍼센트로 변환 (1080p 기준)
const leftPercent = (newX / 1920) * 100;
const topPercent = (newY / 1080) * 100;
// 실시간 업데이트
gsap.set(element, {
left: `${leftPercent}%`,
top: `${topPercent}%`
});
// 상태 업데이트
debugStore.updateElementProperties({
left: `${leftPercent.toFixed(1)}%`,
top: `${topPercent.toFixed(1)}%`
});
};
};
디버그 박스: 모든 요소를 한눈에
// debug.module.scss
.debug {
* {
border: 2px solid #{randomColor()} !important;
position: relative;
&::before {
content: attr(class);
position: absolute;
top: -20px;
left: 0;
background: rgba(0, 0, 0, 0.8);
color: white;
font-size: 10px;
padding: 2px 4px;
z-index: 9999;
}
}
}
@function randomColor() {
$colors: #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #dda0dd, #98d8c8;
@return nth($colors, random(length($colors)));
}
이 CSS 트릭으로 모든 요소에 랜덤한 색상의 경계선을 표시할 수 있다. 디버그 모드를 켜면 레이아웃이 한눈에 보인다.
실제 사용 예시: Day55Sketch 개선하기
const Day55Sketch = () => {
const containerRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLHeadingElement>(null);
const debugStore = useDebugStore();
return (
<>
<div
className={`${styles.scaleContainer} ${debugStore.showDebugBoxes ? styles.debug : ''}`}
ref={containerRef}
>
<div className={styles.videoContainer}>
<GsapControlWrapper targetRef={textRef}>
<h1
ref={textRef}
className={styles.mainHeading}
style={{
position: 'absolute',
top: '10%',
left: '20%',
fontSize: '20px'
}}
>
애니메이션 테스트
</h1>
</GsapControlWrapper>
</div>
</div>
<DebugPanel />
</>
);
};
한계와 개선점
🚨 현재 문제점들
- 복잡한 Transform 계산: CSS transform과 GSAP의 transform이 겹치면 계산이 복잡해진다
- 성능 이슈: 실시간 드래그 시 렌더링 부하
- 중첩된 애니메이션: 여러 타임라인이 겹치면 동기화가 어렵다
💡 개선 방향
// 향후 개선 계획
interface EnhancedDebugState {
// 다중 선택 지원
selectedElements: HTMLElement[];
// 키프레임 편집
keyframes: {
time: number;
properties: CSSProperties;
}[];
// 애니메이션 프리셋
presets: {
name: string;
timeline: GSAPTimeline;
}[];
}
결론: 완벽하지 않아도 일단은 내가 쓸 만하니 걍 쓰기로
Theatre.js만큼 완벽하지는 않지만, 내 워크플로우에는 딱 맞는 툴이 나왔다. 가장 중요한 건 실제로 사용할 수 있다는 점이다.
다른 팀 멤버가 “여기 텍스트 좀 더 위로 올려주세요”라고 하면, 이제 코드를 뒤져가며 픽셀값을 찾을 필요가 없다. 그냥 드래그해서 옮기고, 나온 값을 복사해서 붙여넣으면 끝이다.
완벽한 툴을 기다리느라 시간을 낭비하지 말고, 지금 당장 필요한 기능부터 만들어 쓰는 게 답이다. 어차피 개발이란 게 그런 거 아닌가.