SSGOI

바운스 애니메이션

요소가 통통 튀는 듯한 생동감 있는 효과를 만듭니다

바운스 애니메이션

바운스(Bounce) 애니메이션은 요소가 튀어오르는 듯한 효과를 만들어 재미있고 생동감 있는 인터랙션을 제공합니다. 주목을 끌거나 즐거운 사용자 경험을 만들 때 효과적입니다.

기본 사용법

import { transition } from '@ssgoi/react';
import { bounce } from '@ssgoi/react/transitions';

function Component() {
  const [isVisible, setIsVisible] = useState(true);
  
  return (
    <div>
      {isVisible && (
        <div ref={transition({ key: 'bounce-element', ...bounce() })}>
          바운스 애니메이션이 적용된 요소
        </div>
      )}
    </div>
  );
}

옵션

interface BounceOptions {
  height?: number;         // 바운스 높이 (기본값: 20)
  intensity?: number;      // 바운스 강도 (기본값: 1)
  scale?: boolean;         // 스케일 효과 추가 (기본값: true)
  fade?: boolean;          // 페이드 효과 추가 (기본값: false)
  direction?: 'up' | 'down';  // 바운스 방향 (기본값: 'up')
  spring?: {
    stiffness?: number;    // 스프링 강도 (기본값: 800)
    damping?: number;      // 감쇠 계수 (기본값: 15)
  };
}

옵션 설명

  • height: 바운스 최대 높이 (픽셀)
  • intensity: 바운스 횟수와 강도 (1 = 기본, 2 = 두 배)
  • scale: 바운스와 함께 크기 변화 효과
  • fade: 바운스와 함께 페이드 효과
  • direction: 바운스 방향
    • 'up': 위로 튀어오름
    • 'down': 아래로 떨어짐
  • spring: 스프링 물리 설정 (높은 stiffness = 빠른 바운스)

사용 예시

바운스 강도 조절

// 부드러운 바운스
const softBounce = bounce({ 
  height: 10,
  intensity: 0.5,
  spring: { stiffness: 600, damping: 20 }
});

// 강한 바운스
const strongBounce = bounce({ 
  height: 30,
  intensity: 2,
  spring: { stiffness: 1000, damping: 10 }
});

// 미세한 바운스
const subtleBounce = bounce({ 
  height: 5,
  intensity: 0.3
});

방향 변경

// 위로 바운스 (기본)
const bounceUp = bounce({ 
  direction: 'up' 
});

// 아래로 바운스 (떨어지는 효과)
const bounceDown = bounce({ 
  direction: 'down',
  height: 25
});

복합 효과

// 바운스 + 페이드
const bounceFade = bounce({ 
  fade: true,
  height: 20
});

// 바운스만 (스케일 없이)
const bounceOnly = bounce({ 
  scale: false,
  height: 15
});

// 모든 효과 조합
const bounceAll = bounce({ 
  height: 25,
  intensity: 1.5,
  scale: true,
  fade: true,
  spring: { stiffness: 700, damping: 12 }
});

실용적인 활용 예시

좋아요 버튼

function LikeButton() {
  const [isLiked, setIsLiked] = useState(false);
  
  return (
    <button
      onClick={() => setIsLiked(!isLiked)}
      className="relative p-2"
    >
      {isLiked ? (
        <HeartIcon 
          ref={transition({ 
            key: 'heart-filled', 
            ...bounce({ 
              height: 15,
              intensity: 1.2,
              spring: { stiffness: 900, damping: 15 }
            }) 
          })}
          className="w-8 h-8 text-red-500"
        />
      ) : (
        <HeartOutlineIcon className="w-8 h-8 text-gray-400" />
      )}
      
      {/* 좋아요 카운트 애니메이션 */}
      {isLiked && (
        <span 
          ref={transition({ 
            key: 'like-count', 
            ...bounce({ 
              height: 10,
              direction: 'up',
              fade: true
            }) 
          })}
          className="absolute -top-2 -right-2 text-xs text-red-500"
        >
          +1
        </span>
      )}
    </button>
  );
}

알림 벨

function NotificationBell({ hasNew }) {
  return (
    <div className="relative">
      <BellIcon 
        ref={hasNew ? transition({ 
          key: 'bell-bounce', 
          ...bounce({ 
            height: 8,
            intensity: 2,
            spring: { stiffness: 1200, damping: 20 }
          }) 
        }) : undefined}
        className="w-6 h-6"
      />
      
      {hasNew && (
        <div 
          ref={transition({ 
            key: 'notification-dot', 
            ...bounce({ 
              height: 5,
              scale: true
            }) 
          })}
          className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"
        />
      )}
    </div>
  );
}

성공 메시지

function SuccessMessage({ show, message }) {
  return (
    <>
      {show && (
        <div 
          ref={transition({ 
            key: 'success-message', 
            ...bounce({ 
              height: 20,
              direction: 'down',
              fade: true,
              spring: { stiffness: 600, damping: 18 }
            }) 
          })}
          className="fixed top-4 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-2"
        >
          <CheckCircleIcon className="w-5 h-5" />
          {message}
        </div>
      )}
    </>
  );
}

드롭 애니메이션

function DroppableItem({ item, onDrop }) {
  const [isDragging, setIsDragging] = useState(false);
  const [isDropped, setIsDropped] = useState(false);
  
  const handleDrop = () => {
    setIsDropped(true);
    onDrop(item);
  };
  
  return (
    <div
      draggable
      onDragStart={() => setIsDragging(true)}
      onDragEnd={() => {
        setIsDragging(false);
        handleDrop();
      }}
      className={isDragging ? 'opacity-50' : ''}
    >
      {isDropped ? (
        <div 
          ref={transition({ 
            key: 'dropped-item', 
            ...bounce({ 
              height: 30,
              direction: 'down',
              intensity: 1.5,
              spring: { stiffness: 500, damping: 12 }
            }) 
          })}
          className="p-4 bg-blue-100 rounded-lg"
        >
          {item.name} (Dropped!)
        </div>
      ) : (
        <div className="p-4 bg-gray-100 rounded-lg cursor-move">
          {item.name}
        </div>
      )}
    </div>
  );
}

고급 활용

연속 바운스

function ContinuousBounce() {
  const [bounceCount, setBounceCount] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setBounceCount(c => c + 1);
    }, 2000);
    
    return () => clearInterval(interval);
  }, []);
  
  return (
    <div 
      ref={transition({ 
        key: `bounce-${bounceCount}`, 
        ...bounce({ 
          height: 15,
          intensity: 0.8
        }) 
      })}
      className="w-20 h-20 bg-purple-500 rounded-full"
    />
  );
}

탄성 메뉴

function ElasticMenu({ items }) {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div className="relative">
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="p-3 bg-blue-500 text-white rounded-full"
      >
        <MenuIcon />
      </button>
      
      {isOpen && (
        <div className="absolute top-full mt-2 space-y-2">
          {items.map((item, index) => (
            <button
              key={item.id}
              ref={transition({ 
                key: `menu-item-${item.id}`, 
                ...bounce({ 
                  height: 20 - index * 3,  // 점진적으로 감소
                  intensity: 1,
                  spring: { 
                    stiffness: 800 - index * 50,  // 순차적 효과
                    damping: 15 
                  }
                }) 
              })}
              className="block w-full px-4 py-2 bg-white rounded-lg shadow hover:bg-gray-50"
              style={{ 
                transitionDelay: `${index * 50}ms`  // 스태거 효과
              }}
            >
              {item.label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

공 튀기기 시뮬레이션

function BouncingBall() {
  const [position, setPosition] = useState({ x: 50, y: 0 });
  const [bounceKey, setBounceKey] = useState(0);
  
  const handleClick = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    setPosition({
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    });
    setBounceKey(k => k + 1);
  };
  
  return (
    <div 
      className="relative w-full h-96 bg-gray-100 overflow-hidden"
      onClick={handleClick}
    >
      <div
        ref={transition({ 
          key: `ball-${bounceKey}`, 
          ...bounce({ 
            height: 150,
            direction: 'down',
            intensity: 2,
            scale: true,
            spring: { stiffness: 400, damping: 10 }
          }) 
        })}
        className="absolute w-12 h-12 bg-red-500 rounded-full"
        style={{ 
          left: position.x - 24,
          top: position.y - 24
        }}
      />
    </div>
  );
}

성능 최적화

  • 바운스는 transform: translateY()를 사용하여 GPU 가속됩니다
  • 높은 intensity 값은 더 많은 계산을 필요로 합니다
  • 여러 요소의 동시 바운스는 성능을 고려해야 합니다

성능 팁

// 모바일 최적화
const isMobile = window.innerWidth < 768;
const optimizedBounce = bounce({
  height: isMobile ? 10 : 20,
  intensity: isMobile ? 0.8 : 1,
  spring: { 
    stiffness: isMobile ? 900 : 800,
    damping: 15 
  }
});

접근성 고려사항

<button
  ref={transition({ 
    key: 'accessible-bounce', 
    ...bounce() 
  })}
  aria-label="새 알림 있음"
  aria-live="polite"
  aria-atomic="true"
>
  <NotificationIcon />
</button>

권장 사용 사례

  • 인터랙션 피드백: 버튼 클릭, 좋아요
  • 알림: 새 메시지, 업데이트 알림
  • 성공/완료 상태: 작업 완료 표시
  • 게임 요소: 점수 획득, 아이템 수집
  • 튜토리얼: 주목할 요소 강조