들어가기: JavaScript 애니메이션 개념
1. 애니메이션이란?
시간에 따라 상태(예: 위치, 크기, 색상 등)가 변하는 것입니다. 브라우저에서 이걸 구현하려면 setInterval 또는 requestAnimationFrame 같은 타이머/프레임 기반 API를 사용합니다. React-Spring은 requestAnimationFrame을 이용해 동작합니다.
requestAnimationFrame은 브라우저에 최적화된 애니메이션 실행 방식
- 반드시 setInterval 대신 사용할 것
- react-spring도 이 방식을 내부에서 사용함
2. requestAnimationFrame() 개념
requestAnimationFrame(callback)은 다음 브라우저의 리페인트 시점에 콜백 함수를 실행시켜 주는 API입니다. 초당 60프레임(fps)으로 실행되며 부드럽고 효율적인 애니메이션을 구현할 수 있습니다.
왜 중요할까?
- setInterval은 정확하지 않음 (프레임 드랍, CPU 부하)
- requestAnimationFrame은 브라우저가 최적의 시점에 실행하므로 더 효율적
- 브라우저 탭이 비활성화되면 실행 중단되어 리소스를 아낌
3. 기본 예제: 박스를 오른쪽으로 이동시키기
<div id="box" style="width: 50px; height: 50px; background: red; position: absolute;"></div>
<script>
const box = document.getElementById("box");
let pos = 0;
function animate() {
pos += 2;
box.style.transform = `translateX(${pos}px)`;
if (pos < 300) {
requestAnimationFrame(animate); // 계속 애니메이션 실행
}
}
animate(); // 시작
</script>
설명:
- animate()는 매 프레임마다 pos를 증가시키고 박스를 이동시킵니다.
- requestAnimationFrame(animate)가 재귀 호출되면서 애니메이션이 반복됨.
4. 시간 기반 애니메이션 (프레임과 무관한 속도 제어)
let lastTime = null;
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const delta = timestamp - lastTime; // ms 단위 시간 경과
pos += delta * 0.1; // 초당 100px 속도로 이동
box.style.transform = `translateX(${pos}px)`;
lastTime = timestamp;
if (pos < 300) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
이 방식의 장점:
- 프레임 속도(fps)에 상관없이 일정한 속도 유지
- GPU 부하나 컴퓨터 성능이 낮아도 자연스러움
5. 가속도/감속 애니메이션 예시
function easeOut(t) {
return t * (2 - t); // easeOut quad
}
function animate(timestamp) {
const progress = Math.min((timestamp - start) / duration, 1);
const eased = easeOut(progress);
box.style.transform = `translateX(${eased * 300}px)`;
if (progress < 1) requestAnimationFrame(animate);
}
- progress는 0~1 범위에서 애니메이션 비율
- easeOut은 느리게 감속하면서 멈추는 곡선
requestAnimationFrame을 쓰는 이유
| 성능 | 브라우저 최적 시점에 실행되어 부드러움 |
| 효율 | 탭 비활성화 시 중지됨 (자원 절약) |
| 정확도 | 프레임 기반이 아닌, 시간 기반 계산 가능 |
| 제어성 | cancelAnimationFrame()으로 정지 가능 |
1. easeIn, easeOut, easeInOut 수학 공식
애니메이션에서 "easing"은 속도의 변화를 수학적으로 제어하는 방법입니다.
- 애니메이션이 갑자기 시작하거나 멈추지 않고, 자연스럽게 감속·가속되게 만듦
1.1 easeIn (느리게 시작 → 빠르게 끝)
function easeInQuad(t) {
return t * t;
}
- 입력 t는 0~1 범위의 시간 진행률
- 초반에 느리게, 끝으로 갈수록 빠르게 진행
- 시각적으로는 점점 빨라지는 느낌
1.2 easeOut (빠르게 시작 → 느리게 끝)
function easeOutQuad(t) {
return t * (2 - t);
}
- 초반에 빠르고, 끝으로 갈수록 느려짐
- 자연스럽게 멈추는 동작에 적합 (ex. 스크롤 감속, 팝업 닫힘 등)
1.3 easeInOut (느리게 시작 → 빨라졌다가 → 다시 느려짐)
function easeInOutQuad(t) {
return t < 0.5
? 2 * t * t
: -1 + (4 - 2 * t) * t;
}
- 중간이 가장 빠름
- 시작과 끝이 자연스럽고 부드러운 애니메이션에 적합
2. requestAnimationFrame vs setInterval 비교 실습
같은 애니메이션을 두 방식으로 구현하고 프레임 드랍, 부드러움, 성능을 비교
공통 HTML 코드
<div style="display: flex; gap: 40px; margin-top: 20px;">
<div id="rafBox" style="width: 50px; height: 50px; background: blue; position: relative;"></div>
<div id="intervalBox" style="width: 50px; height: 50px; background: red; position: relative;"></div>
</div>
2.1 requestAnimationFrame 구현
let rafPos = 0;
function moveWithRAF(timestamp) {
rafPos += 2;
document.getElementById('rafBox').style.left = `${rafPos}px`;
if (rafPos < 300) requestAnimationFrame(moveWithRAF);
}
requestAnimationFrame(moveWithRAF);
2.2 setInterval 구현
let intervalPos = 0;
const interval = setInterval(() => {
intervalPos += 2;
document.getElementById('intervalBox').style.left = `${intervalPos}px`;
if (intervalPos >= 300) clearInterval(interval);
}, 16); // 약 60fps
requestAnimationFrame | setInterval
| 타이밍 정확성 | 브라우저가 직접 제어 | 일정하지 않음 |
| FPS 보장 | 브라우저에 최적화 (60fps) | CPU 부하, 탭 비활성화 시 렌더링 지연 |
| 리소스 효율성 | 비활성 탭일 때 중지 | 계속 실행 |
| 부드러움 | 매우 부드러움 | 끊김 현상 있을 수 있음 |
1단계: React-Spring 기초 개념
1. React-Spring 설치
npm install @react-spring/web
# 또는
yarn add @react-spring/web
최신 버전은 React 18 이상과 호환되며, 패키지가 @react-spring/web으로 분리되어 있습니다.
2. 핵심 개념 정리
디버깅: 애니메이션이 안 되면 to, from, reset, immediate 값을 꼭 확인
성능 이슈 주의: 너무 많은 animated 요소는 렌더링 비용이 큼. useSprings 최적화 필요
최신 문법 주의: v9부터 @react-spring/web으로 분리됨
2.1 Spring Physics란?
- react-spring은 전통적인 duration 기반 애니메이션이 아니라 스프링 물리 모델(Spring physics) 을 사용합니다.
- 속도, 마찰, 질량, 탄성 계수를 기반으로 자연스러운 모션을 만듭니다.
useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
config: { tension: 170, friction: 26 }
})
→ tension(장력), friction(마찰) 등을 조절하여 속도감 조절
2.2 Declarative Animation (선언형 애니메이션)
- react-spring은 "어떻게 움직일지"를 선언하는 방식
- imperative하게 DOM을 직접 제어하지 않음
// 선언형 방식
const [style, api] = useSpring(() => ({ opacity: 0 }));
api.start({ opacity: 1 });
requestAnimationFrame을 직접 호출할 필요 없이, 상태값 변화만 선언해주면 react-spring이 알아서 처리함.
3. useSpring과 animated 기본 구조
예제: 간단한 Fade-In 박스
import { useSpring, animated } from '@react-spring/web';
function FadeInBox() {
const styles = useSpring({
from: { opacity: 0 },
to: { opacity: 1 },
});
return (
<animated.div style={{ ...styles, width: 100, height: 100, background: 'tomato' }} />
);
}
구조 설명:
- useSpring: 애니메이션 값 정의 (from → to)
- animated.div: 일반 div에 애니메이션을 적용할 수 있게 만든 컴포넌트
- style={styles}: 애니메이션 상태가 적용된 스타일 객체를 전달
4. 상호작용 기반 애니메이션 (Toggle)
import { useSpring, animated } from '@react-spring/web';
import { useState } from 'react';
function ToggleBox() {
const [open, setOpen] = useState(false);
const styles = useSpring({
to: { height: open ? 200 : 100, backgroundColor: open ? 'lightblue' : 'tomato' },
config: { tension: 200, friction: 20 },
});
return (
<animated.div
style={{ ...styles, width: 200 }}
onClick={() => setOpen(!open)}
>
Click Me
</animated.div>
);
}
- open 상태 변화에 따라 height와 backgroundColor가 스프링 애니메이션으로 변함
- config를 조절하면 반응 속도와 탄력감을 바꿀 수 있음
5. animated로 감쌀 수 있는 요소
React-Spring에서는 거의 모든 DOM 요소 및 컴포넌트를 animated로 감쌀 수 있습니다:
animated.div
animated.span
animated.button
animated.svg
→ 내부적으로는 forwardRef와 style만 래핑하기 때문에, 성능 저하 없음
1. 버튼 클릭 시 박스의 크기와 색상이 변화하는 애니메이션
import { useState } from 'react';
import { useSpring, animated } from '@react-spring/web';
export function ToggleBox() {
const [toggled, setToggled] = useState(false);
const styles = useSpring({
to: {
width: toggled ? 200 : 100,
height: toggled ? 200 : 100,
backgroundColor: toggled ? 'skyblue' : 'salmon',
},
config: { tension: 170, friction: 26 }, // 스프링 반응 속도
});
return (
<animated.div
style={styles}
onClick={() => setToggled(!toggled)}
className="cursor-pointer"
/>
);
}
클릭 시 부드럽게 크기와 색상이 바뀝니다. animated.div는 스타일 값을 애니메이션으로 처리합니다.
2. Hover 시 이미지 스케일이 부드럽게 확대/축소
import { useSpring, animated } from '@react-spring/web';
import { useState } from 'react';
export function HoverImage() {
const [hovered, setHovered] = useState(false);
const style = useSpring({
transform: hovered ? 'scale(1.1)' : 'scale(1)',
config: { tension: 300, friction: 20 },
});
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
/>
);
}
transform: scale(...) 을 애니메이션으로 처리하여 자연스러운 확대/축소 효과를 구현합니다.
3. Fade-in + Scale-in으로 컴포넌트 등장
import { useSpring, animated } from '@react-spring/web';
import { useEffect, useState } from 'react';
export function AppearBox() {
const [visible, setVisible] = useState(false);
const style = useSpring({
from: { opacity: 0, transform: 'scale(0.8)' },
to: { opacity: visible ? 1 : 0, transform: visible ? 'scale(1)' : 'scale(0.8)' },
config: { tension: 200, friction: 20 },
});
useEffect(() => {
const timer = setTimeout(() => setVisible(true), 200);
return () => clearTimeout(timer);
}, []);
return (
<animated.div
style={{
...style,
width: 150,
height: 150,
background: 'palegreen',
borderRadius: 8,
}}
>
Hello!
</animated.div>
);
}
이 컴포넌트는 마운트된 후 일정 시간 뒤에 부드럽게 투명도와 크기를 증가시키며 등장합니다.
보충 개념
| 클릭 시 상태 변경 | useState, onClick, useSpring |
| hover 이벤트 처리 | onMouseEnter, onMouseLeave |
| 등장 애니메이션 | from → to 패턴 + useEffect |
2단계: 고급 Hook과 제어 흐름
1. useTrail – 여러 요소에 같은 애니메이션을 순차적으로 적용
- 여러 개의 아이템이 순차적으로 하나씩 지연(delay) 되면서 애니메이션됩니다.
- 리스트의 등장, 이동, 사라짐 효과를 자연스럽게 만들 수 있습니다.
import { useTrail, animated } from '@react-spring/web';
const items = ['React', 'Spring', 'is', 'awesome'];
export function TrailList() {
const trail = useTrail(items.length, {
from: { opacity: 0, transform: 'translateY(20px)' },
to: { opacity: 1, transform: 'translateY(0)' },
});
return (
<div>
{trail.map((style, index) => (
<animated.div key={index} style={style}>
{items[index]}
</animated.div>
))}
</div>
);
}
- 리스트를 .map()으로 렌더링하면서 각각 style을 animated.div에 적용
- 각 항목의 등장 시점이 살짝씩 늦음
2. useTransition – 조건부 렌더링 시 등장/사라짐 애니메이션
- 요소가 생성되거나 제거될 때 부드럽게 등장/사라지게 만듭니다.
- React의 conditional rendering과 매우 잘 어울립니다.
import { useTransition, animated } from '@react-spring/web';
import { useState } from 'react';
export function ToggleTransition() {
const [show, setShow] = useState(false);
const transition = useTransition(show, {
from: { opacity: 0, transform: 'scale(0.9)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.9)' },
});
return (
<div>
<button onClick={() => setShow((s) => !s)}>Toggle</button>
{transition((style, item) =>
item ? <animated.div style={style}>Hello!</animated.div> : null
)}
</div>
);
}
- from, enter, leave을 각각 설정
- show가 true → false로 바뀌면 자연스럽게 사라짐
3. useChain – 애니메이션의 타이밍을 연결
- 두 개 이상의 애니메이션을 순서대로 이어서 실행하고 싶을 때 사용
- ref를 통해 각 애니메이션의 시작 시점을 명시적으로 지정
import {
useSpring,
useSpringRef,
useChain,
animated,
} from '@react-spring/web';
export function ChainedAnimation() {
const boxRef = useSpringRef();
const textRef = useSpringRef();
const boxStyle = useSpring({
ref: boxRef,
from: { opacity: 0, transform: 'scale(0.8)' },
to: { opacity: 1, transform: 'scale(1)' },
});
const textStyle = useSpring({
ref: textRef,
from: { opacity: 0, color: 'gray' },
to: { opacity: 1, color: 'black' },
});
// 순서: box → text
useChain([boxRef, textRef], [0, 0.5]);
return (
<div>
<animated.div style={{ ...boxStyle, width: 100, height: 100, background: 'skyblue' }} />
<animated.div style={textStyle}>Chained Text</animated.div>
</div>
);
}
- useSpringRef()로 각각의 애니메이션을 명시적으로 제어
- useChain([ref1, ref2], [delay1, delay2])로 순서를 연결
1. 리스트 항목이 순차적으로 슬라이드 인/아웃 되도록 만들기 (useTrail)
import { useTrail, animated } from '@react-spring/web';
import { useState } from 'react';
const items = ['Apple', 'Banana', 'Orange', 'Grape'];
export function TrailList() {
const [open, setOpen] = useState(false);
const trail = useTrail(items.length, {
from: { opacity: 0, transform: 'translateX(-30px)' },
to: {
opacity: open ? 1 : 0,
transform: open ? 'translateX(0)' : 'translateX(-30px)',
},
config: { mass: 1, tension: 200, friction: 20 },
});
return (
<div>
<button onClick={() => setOpen((prev) => !prev)}>Toggle List</button>
<div style={{ marginTop: 20 }}>
{trail.map((style, idx) => (
<animated.div key={idx} style={style}>
{items[idx]}
</animated.div>
))}
</div>
</div>
);
}
- useTrail을 이용하여 항목들이 하나씩 순차적으로 슬라이드 되어 등장하거나 사라집니다.
2. 모달이 부드럽게 열리고 닫히는 애니메이션 구현 (useTransition)
import { useState } from 'react';
import { useTransition, animated } from '@react-spring/web';
export function ModalExample() {
const [show, setShow] = useState(false);
const transitions = useTransition(show, {
from: { opacity: 0, transform: 'translateY(-20px)' },
enter: { opacity: 1, transform: 'translateY(0)' },
leave: { opacity: 0, transform: 'translateY(-20px)' },
config: { tension: 210, friction: 22 },
});
return (
<div>
<button onClick={() => setShow((s) => !s)}>Toggle Modal</button>
{transitions((style, item) =>
item ? (
<animated.div style={{ ...style, padding: 20, background: '#fff', border: '1px solid #ccc' }}>
This is a modal
</animated.div>
) : null
)}
</div>
);
}
- useTransition을 사용해 모달이 등장/제거될 때 애니메이션이 적용됨
- 진입(enter)과 퇴장(leave) 스타일을 명확히 구분
3. 로딩 바 후 컨텐츠 등장 애니메이션 (useChain)
import {
useSpring,
useSpringRef,
useChain,
animated,
} from '@react-spring/web';
import { useState, useEffect } from 'react';
export function LoadingThenContent() {
const loaderRef = useSpringRef();
const contentRef = useSpringRef();
const [loaded, setLoaded] = useState(false);
const loaderStyle = useSpring({
ref: loaderRef,
from: { width: '0%' },
to: async (next) => {
await next({ width: '100%' });
setLoaded(true); // 로딩 끝나면 컨텐츠 애니메이션 시작
},
config: { duration: 1000 },
});
const contentStyle = useSpring({
ref: contentRef,
from: { opacity: 0, transform: 'translateY(10px)' },
to: { opacity: loaded ? 1 : 0, transform: loaded ? 'translateY(0)' : 'translateY(10px)' },
config: { tension: 180, friction: 18 },
});
useChain([loaderRef, contentRef], [0, 0.3]);
return (
<div style={{ padding: 20 }}>
<animated.div
style={{
...loaderStyle,
height: 4,
background: '#4f46e5',
marginBottom: 20,
}}
/>
<animated.div style={contentStyle}>
<h2>Loaded Content 🎉</h2>
<p>This content appears after the loading bar is complete.</p>
</animated.div>
</div>
);
}
- useChain을 이용해 로딩 바 → 컨텐츠 등장 순서대로 애니메이션이 실행
- ref와 useChain()의 타이밍 인덱스를 활용한 연결
3단계: 실제 프로젝트에 적용
1. 실사용 프로젝트에서의 통합 방식
기본 개념
- react-spring은 "스타일 상태 기반"의 애니메이션 처리
- 상태에 따라 to 값을 바꾸면, 해당 스타일이 자동으로 애니메이션됨
실제 프로젝트에서 사용하는 방식 예시
- 페이지 진입 시 등장 애니메이션 (useEffect)
- 전역 상태로 특정 컴포넌트의 열림/닫힘 제어
- 사용자 이벤트 (drag, toggle, hover)에 반응하는 UI 애니메이션
- 리스트 렌더링 (useTrail, useTransition)
- 로딩 단계별 애니메이션 시퀀스 (useChain)
2. Zustand와 react-spring 연동 예제
전역 상태에 따라 애니메이션 자동 제어
Zustand store 정의
// useUIStore.ts
import { create } from 'zustand';
interface UIState {
modalOpen: boolean;
toggleModal: () => void;
}
export const useUIStore = create<UIState>((set) => ({
modalOpen: false,
toggleModal: () => set((state) => ({ modalOpen: !state.modalOpen })),
}));
애니메이션과 연동
import { useTransition, animated } from '@react-spring/web';
import { useUIStore } from '@/stores/useUIStore';
export function Modal() {
const modalOpen = useUIStore((state) => state.modalOpen);
const transition = useTransition(modalOpen, {
from: { opacity: 0, transform: 'translateY(-20px)' },
enter: { opacity: 1, transform: 'translateY(0)' },
leave: { opacity: 0, transform: 'translateY(-20px)' },
});
return transition(
(style, show) =>
show && (
<animated.div style={style} className="modal">
Zustand + React Spring Modal
</animated.div>
)
);
}
zustand의 전역 상태가 react-spring 애니메이션의 트리거 역할을 합니다.
React Spring + Zustand로 로그인 트랜지션 구현
상태 정의 (Zustand)
// useAuthUI.ts
import { create } from 'zustand';
interface AuthUI {
showLogin: boolean;
toggleLogin: () => void;
}
export const useAuthUI = create<AuthUI>((set) => ({
showLogin: false,
toggleLogin: () => set((s) => ({ showLogin: !s })),
}));
애니메이션 컴포넌트
import { useSpring, animated } from '@react-spring/web';
import { useAuthUI } from '@/stores/useAuthUI';
export function LoginSpringBox() {
const showLogin = useAuthUI((s) => s.showLogin);
const style = useSpring({
opacity: showLogin ? 1 : 0,
transform: showLogin ? 'scale(1)' : 'scale(0.95)',
config: { tension: 220, friction: 18 },
});
return (
<animated.div style={style}>
<h3>Login</h3>
<input placeholder="email" />
<input placeholder="password" type="password" />
</animated.div>
);
}
토글 버튼
function LoginToggle() {
const toggleLogin = useAuthUI((s) => s.toggleLogin);
return <button onClick={toggleLogin}>Toggle Login Form</button>;
}
상태가 전역으로 관리되므로 어떤 페이지든 로그인 박스를 토글할 수 있습니다.
3. React-Spring vs Framer Motion 비교
React Spring
| 기반 | Spring physics 기반 |
| 제어 방식 | 스타일 상태 기반 (animated, useSpring 등) |
| 컴포넌트 수 | 적음 (기본은 DOM을 감싼 animated.div 등) |
| 장점 | 자유도 높고 물리 기반 애니메이션, React-native 호환 |
| 단점 | 복잡한 인터랙션은 직접 구현 필요 |
Framer Motion
| 기반 | time-based easing (easeIn, easeOut) |
| 제어 방식 | 선언형 props (animate, initial, exit) |
| 컴포넌트 수 | motion.div, motion.button 등 |
| 장점 | layout, whileHover, exit 등 고수준 인터랙션 내장 |
| 단점 | 커스텀 제어는 제한적일 수 있음 |
선언형 제어와 고급 인터랙션이 필요하면 Framer Motion,
정밀한 물리 제어와 커스터마이징이 필요하면 React Spring이 적합합니다.
Framer Motion 실전 예제 : 로그인 폼 애니메이션
import { motion } from 'framer-motion';
export function LoginForm() {
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
style={{ background: 'white', padding: 20, borderRadius: 8 }}
>
<h2>Login</h2>
<input type="email" placeholder="Email" style={{ display: 'block', marginBottom: 10 }} />
<input type="password" placeholder="Password" style={{ display: 'block', marginBottom: 10 }} />
<button>Submit</button>
</motion.div>
);
}
- initial / animate / exit props로 등장/퇴장 애니메이션
- transition으로 타이밍 제어
- AnimatePresence를 상위에 두면 조건부 렌더링에서도 애니메이션 유지 가능
4. 성능 최적화 포인트
1) config: 물리 파라미터 조절
config: { tension: 170, friction: 26 }
- tension: 더 크면 빠르게 반응
- friction: 더 크면 더 천천히 멈춤
2) immediate: 애니메이션 없이 즉시 반영
to: { scale: 1.2 },
immediate: true
- 초기 렌더에서 스타일 즉시 적용이 필요할 때 유용
3) reset: 애니메이션을 초기화 후 재시작
useSpring({
to: { opacity: 1 },
from: { opacity: 0 },
reset: true
});
- 같은 스타일로 다시 애니메이션을 재실행하고 싶을 때
| 모달, 토스트, 팝업 등 전역 상태 제어 | Zustand + useTransition | modalOpen 상태에 따라 등장/사라짐 |
| 컴포넌트 진입 시 애니메이션 | useSpring / useTrail | 페이지 또는 컴포넌트 mount 시 적용 |
| 리스트 순차 등장 | useTrail | 텍스트, 버튼 리스트 등장 효과 |
| UI 반응 애니메이션 | Framer Motion (hover, tap) | whileHover, whileTap 등의 API |
| 복잡한 시퀀스 제어 | useChain | 로딩 후 애니메이션 등 |
React Native에서 React-Spring 활용
React Native 환경에서는 @react-spring/native 패키지를 사용합니다.
설치
npm install @react-spring/native
기본 예제
import React, { useState } from 'react';
import { View, Button } from 'react-native';
import { useSpring, animated } from '@react-spring/native';
const AnimatedView = animated(View);
export default function SpringBox() {
const [open, setOpen] = useState(false);
const style = useSpring({
to: {
opacity: open ? 1 : 0.5,
transform: open ? 'scale(1)' : 'scale(0.9)',
},
config: { tension: 180, friction: 12 },
});
return (
<View style={{ padding: 40 }}>
<Button title="Toggle" onPress={() => setOpen((prev) => !prev)} />
<AnimatedView
style={[
{
width: 100,
height: 100,
backgroundColor: 'skyblue',
marginTop: 20,
},
style,
]}
/>
</View>
);
}
차이점:
- animated.View, animated.Text 등 React Native 컴포넌트를 사용
- 스타일은 배열 형태로 적용 가능 ([baseStyle, animatedStyle])
- 대부분의 스타일 속성은 react-native에 맞춰져 있음 (예: transform, opacity, scale 등)
애니메이션 디버깅 체크리스트
1. 버벅임(FPS Drop) — 렌더링 병목
2. 애니메이션 미실행/중단 — 로직 또는 상태 오류
3. 불연속적인 이동/깜빡임 — layout, transform 오류
4. 중복 실행, memory leak — 상태 변화 감지 실패, 무한 루프
5. UX 문제 — 너무 빠르거나 느림, 예기치 않은 트리거
1. FPS 성능 디버깅
방법: Chrome DevTools > Performance > FPS Meter
- chrome://flags에서 "FPS Meter" 활성화
- Performance 탭에서 애니메이션 구간을 녹화 → Frames, JS Heap, Recalculate Style 확인
자주 보는 병목 원인
| Layout thrashing | DOM을 반복적으로 읽고 쓰는 경우 (e.g. getBoundingClientRect + style write) |
| 애니메이션 중 상태 변경 | setState가 너무 자주 호출되면 리렌더링 비용 증가 |
| opacity/transform 이외 속성 사용 | height, width, left, top은 리플로우 유발 |
2. React-Spring 디버깅 팁
2.1 애니메이션이 실행되지 않음
- from과 to 사이 차이가 없는 경우 (→ 애니메이션 생략됨)
- immediate: true가 설정된 경우 → 확인 필요
- reset: true 없이 같은 값 반복 실행 → 무시될 수 있음
2.2 애니메이션이 무한 반복
- to를 함수로 선언했지만 외부 상태가 매번 변할 때
const style = useSpring({
to: { x: open ? 100 : 0 },
});
open이 매번 새로 렌더링되면 계속 애니메이션이 재실행됨 → useMemo, useCallback 등으로 방지
3. Framer Motion 디버깅 팁
3.1 exit 애니메이션이 작동하지 않음
- AnimatePresence가 상위에 없거나 key가 빠진 경우
<AnimatePresence>
{show && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
3.2 layout 애니메이션이 튀는 경우
- motion.div layout은 DOM 위치 추적을 기반으로 함 → position: absolute 사용 시 깨짐
- 형제 요소가 동기화되지 않으면 layout shift 발생
4. 상태와 애니메이션 연결 문제
증상: 상태는 바뀌는데 애니메이션은 동작 안 함
- Zustand, Redux, Context 같은 전역 상태를 직접 쓰지 않고, subscribe 없이 접근한 경우
해결:
- 상태 바뀔 때마다 useSpring, useTransition이 재구성되도록 dependency를 정확히 설정
- console.log(style) 혹은 onRest로 상태 변화 감지
5. 성능 최적화 팁 (모든 애니메이션 공통)
| transform, opacity 중심 사용 | GPU 가속 가능, repaint만 유발 |
| layout shift 방지 | 크기 변경 대신 scale 사용 |
| conditional render 대신 display: none 활용 | DOM 제거는 트리거 애매할 수 있음 |
| config 조정 | 불필요하게 긴 애니메이션은 UX 저하, 짧게 조정 |
| onRest 또는 onResolve 활용 | 다음 동작 연결 또는 디버깅 시 유용 |
디버깅 시 유용한 도구
- Chrome DevTools > Performance 탭
- React DevTools Profiler
- why-did-you-render (리렌더링 확인)
- React Spring Devtools (실험적, 일부 상태 확인 가능)
'JavaScript > React' 카테고리의 다른 글
| 상태 관리 라이브러리 valtio (0) | 2025.06.09 |
|---|---|
| wouter (0) | 2025.06.09 |
| dnd-kit(Drag-and-Drop 라이브러리) (0) | 2025.05.20 |
| React Hook Form (1) | 2025.05.15 |
| React Query (1) | 2025.05.15 |