시각적 피드백과 애니메이션: 인터랙션을 생동감 있게 만드는 모션 설계

UX 디자인

애니메이션 UX모션 디자인마이크로인터랙션Framer Motion시각적 피드백

이 글은 누구를 위한 것인가

  • 애니메이션을 추가했는데 오히려 UX가 답답해진 팀
  • 모션 설계를 처음 시작하는 디자이너·개발자
  • 접근성(모션 민감도)을 고려한 애니메이션을 만들고 싶은 팀

들어가며

좋은 애니메이션은 "보이지 않는다". 사용자가 애니메이션을 의식하지 않고 자연스럽게 흐름을 따라가게 만드는 것이 목표다. 주의를 끌기 위한 애니메이션이 아닌, 상태 변화를 이해하게 돕는 애니메이션이 좋은 애니메이션이다. 지속 시간을 정할 때는 응답 시간의 3가지 한계를 기준선으로 삼고, prefers-reduced-motion을 반드시 존중한다.

이 글은 bluefoxdev.kr의 UX 모션 디자인 가이드 를 참고하여 작성했습니다.


1. 목적 있는 애니메이션 원칙

[UX 애니메이션의 목적]

1. 상태 변화 전달:
   버튼 클릭 → 눌리는 느낌 (scale down)
   로딩 → 스피너 또는 스켈레톤
   완료 → 체크마크 애니메이션

2. 공간적 관계 설명:
   사이드 패널이 어디서 나오는지 (오른쪽에서 슬라이드)
   모달이 어떻게 나타나는지 (아래서 위로)

3. 다음 단계 예측:
   페이지 전환 방향 → 브레드크럼과 일치
   목록 항목 추가 → 아래에서 나타남

4. 응답성 전달:
   클릭 즉시 피드백 (100ms 이내)

[애니메이션 타이밍 가이드]
  즉각 피드백: 50-100ms (버튼 hover, 클릭)
  상태 전환: 150-300ms (메뉴 열기, 토글)
  페이지 전환: 200-400ms (라우팅)
  복잡한 애니메이션: 400-600ms
  
  ❌ 600ms 이상: 너무 느림, 답답함
  ❌ 50ms 미만: 너무 빨라서 못 봄

[이징(Easing) 함수 선택]
  ease-in-out: 시작과 끝이 부드러움 (가장 자연스러움)
  ease-out: 빠르게 시작해 천천히 멈춤 (요소 등장)
  ease-in: 천천히 시작해 빠르게 끝남 (요소 퇴장)
  spring: 탄성감 (드래그앤드롭, 스와이프)

2. 마이크로인터랙션 구현

/* 버튼 클릭 피드백 */
.btn {
  transition: transform 0.1s ease, box-shadow 0.1s ease;
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.btn:active {
  transform: translateY(0) scale(0.98);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

/* 카드 hover 효과 */
.card {
  transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
}

/* 스켈레톤 로딩 */
@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.skeleton {
  background: linear-gradient(
    90deg,
    #f0f0f0 25%,
    #e0e0e0 37%,
    #f0f0f0 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
}

/* 접근성: 모션 민감도 대응 */
@media (prefers-reduced-motion: reduce) {
  .btn,
  .card,
  .skeleton {
    transition: none;
    animation: none;
  }
  
  /* 완전히 제거하지 않고 즉각 전환으로 */
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

3. Framer Motion 페이지 전환

import { motion, AnimatePresence } from "framer-motion";

// 페이지 전환 애니메이션
const pageVariants = {
  initial: { opacity: 0, x: 20 },
  animate: { opacity: 1, x: 0 },
  exit: { opacity: 0, x: -20 },
};

function AnimatedPage({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={{ duration: 0.2, ease: "easeInOut" }}
    >
      {children}
    </motion.div>
  );
}

// 리스트 아이템 순차 등장
function AnimatedList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map((item, i) => (
        <motion.li
          key={item.id}
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ delay: i * 0.05, duration: 0.3 }}
        >
          <ItemCard item={item} />
        </motion.li>
      ))}
    </ul>
  );
}

// 성공 체크마크 애니메이션
function SuccessCheck() {
  return (
    <motion.svg
      viewBox="0 0 50 50"
      className="w-16 h-16 text-green-500"
      initial="hidden"
      animate="visible"
    >
      <motion.circle
        cx="25"
        cy="25"
        r="24"
        fill="none"
        stroke="currentColor"
        strokeWidth="2"
        variants={{
          hidden: { pathLength: 0 },
          visible: { pathLength: 1, transition: { duration: 0.5 } },
        }}
      />
      <motion.path
        d="M15 25 L22 32 L35 18"
        fill="none"
        stroke="currentColor"
        strokeWidth="3"
        strokeLinecap="round"
        variants={{
          hidden: { pathLength: 0 },
          visible: { pathLength: 1, transition: { delay: 0.5, duration: 0.4 } },
        }}
      />
    </motion.svg>
  );
}

마무리

애니메이션 설계의 첫 번째 원칙은 "없어도 기능이 동작해야 한다"이다. prefers-reduced-motion을 항상 적용해서 모션 민감도가 있는 사용자를 배려하라. 그 다음에 애니메이션을 추가하되, 200ms-300ms를 기준으로 더 빠르게 시작하라. 느린 애니메이션은 빠른 것보다 훨씬 더 나쁘다.