이 글은 누구를 위한 것인가
- 애니메이션을 추가했는데 오히려 UX가 답답해진 팀
- 모션 설계를 처음 시작하는 디자이너·개발자
- 접근성(모션 민감도)을 고려한 애니메이션을 만들고 싶은 팀
들어가며
좋은 애니메이션은 "보이지 않는다". 사용자가 애니메이션을 의식하지 않고 자연스럽게 흐름을 따라가게 만드는 것이 목표다. 주의를 끌기 위한 애니메이션이 아닌, 상태 변화를 이해하게 돕는 애니메이션이 좋은 애니메이션이다.
이 글은 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를 기준으로 더 빠르게 시작하라. 느린 애니메이션은 빠른 것보다 훨씬 더 나쁘다.