본문 바로가기
JavaScript/React

React-Spring

by curious week 2025. 5. 23.

 

 

 

들어가기: 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