CSS 애니메이션 | Transition, Animation, Transform
이 글의 핵심
CSS 애니메이션에 대한 실전 가이드입니다. Transition, Animation, Transform 등을 예제와 함께 상세히 설명합니다.
들어가며
”움직임으로 생명을 불어넣다”
CSS transition·animation은 상태 변화(호버, 로딩 등)를 GPU 친화적인 방식으로 보여 주기 좋습니다. 단순 피드백은 CSS만으로 처리하고, 복잡한 로직은 JavaScript와 나누는 식으로 쓰입니다.
애니메이션을 쓰면 좋은 경우:
- ✅ 사용자 경험: 자연스러운 전환
- ✅ 피드백: 클릭, 호버 등의 반응
- ✅ 주의 집중: 중요한 요소 강조
- ✅ 브랜드: 독특한 개성 표현
- ✅ 성능: GPU 가속으로 부드러운 60fps
CSS 애니메이션 3요소:
- Transition: 상태 변화 (A → B)
- Transform: 변형 (이동, 회전, 크기)
- Animation: 복잡한 애니메이션 (@keyframes)
1. Transition (전환)
기본 사용
Transition은 CSS 속성이 부드럽게 변화하도록 합니다.
.box {
width: 100px;
height: 100px;
background: #3498db;
/* 속성 | 시간 | 타이밍 함수 | 지연 */
transition: all 0.3s ease 0s;
}
.box:hover {
width: 200px;
background: #2ecc71;
}
동작 원리:
상태 A (초기) 상태 B (hover)
width: 100px → width: 200px
background: blue → background: green
transition이 0.3초 동안 부드럽게 보간
개별 속성 지정
.box {
/* 여러 속성 */
transition: width 0.3s ease,
background-color 0.5s linear,
transform 0.2s ease-out;
}
/* 또는 개별 지정 */
.box {
transition-property: width, background-color;
transition-duration: 0.3s, 0.5s;
transition-timing-function: ease, linear;
transition-delay: 0s, 0.1s;
}
타이밍 함수 (Timing Function)
.box {
/* 기본 함수 */
transition-timing-function: linear; /* 일정한 속도 */
transition-timing-function: ease; /* 천천히-빠르게-천천히 (기본값) */
transition-timing-function: ease-in; /* 천천히 시작 */
transition-timing-function: ease-out; /* 천천히 끝 */
transition-timing-function: ease-in-out; /* 천천히 시작과 끝 */
/* 커스텀 베지어 곡선 */
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* 단계 함수 */
transition-timing-function: steps(4, end);
}
시각적 비교:
linear: ────────────
ease: ╱‾‾‾‾‾‾‾╲
ease-in: ╱─────────
ease-out: ─────────╲
ease-in-out: ╱‾‾‾‾‾‾╲
실전 예제
<!DOCTYPE html>
<html lang="ko">
<head>
<style>
.demo-container {
display: flex;
gap: 20px;
padding: 20px;
flex-wrap: wrap;
}
.box {
width: 100px;
height: 100px;
background: #3498db;
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
cursor: pointer;
}
.box-1 {
transition: background 0.3s ease;
}
.box-1:hover {
background: #e74c3c;
}
.box-2 {
transition: transform 0.3s ease;
}
.box-2:hover {
transform: scale(1.2);
}
.box-3 {
transition: all 0.3s ease;
}
.box-3:hover {
transform: rotate(45deg);
background: #2ecc71;
}
.box-4 {
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.box-4:hover {
transform: scale(1.3) rotate(10deg);
}
</style>
</head>
<body>
<div class="demo-container">
<div class="box box-1">색상</div>
<div class="box box-2">크기</div>
<div class="box box-3">회전</div>
<div class="box box-4">바운스</div>
</div>
</body>
</html>
2. Transform (변형)
2D Transform
.box {
/* 이동 (translate) */
transform: translate(50px, 100px); /* x, y */
transform: translateX(50px); /* x만 */
transform: translateY(100px); /* y만 */
/* 크기 조절 (scale) */
transform: scale(1.5); /* 1.5배 */
transform: scale(2, 0.5); /* x 2배, y 0.5배 */
transform: scaleX(2); /* x만 */
transform: scaleY(0.5); /* y만 */
/* 회전 (rotate) */
transform: rotate(45deg); /* 45도 회전 */
transform: rotate(-90deg); /* -90도 회전 */
/* 기울이기 (skew) */
transform: skew(10deg, 20deg); /* x, y */
transform: skewX(10deg); /* x만 */
transform: skewY(20deg); /* y만 */
/* 여러 개 조합 (순서 중요!) */
transform: translate(50px, 50px) rotate(45deg) scale(1.5);
}
조합 순서의 중요성:
/* 다른 결과 */
.box-1 {
transform: rotate(45deg) translate(100px, 0);
/* 회전 후 이동 → 대각선 이동 */
}
.box-2 {
transform: translate(100px, 0) rotate(45deg);
/* 이동 후 회전 → 오른쪽 이동 + 회전 */
}
3D Transform
.box {
/* 3D 이동 */
transform: translateZ(100px); /* z축 */
transform: translate3d(50px, 50px, 100px); /* x, y, z */
/* 3D 회전 */
transform: rotateX(45deg); /* x축 회전 */
transform: rotateY(45deg); /* y축 회전 */
transform: rotateZ(45deg); /* z축 회전 (= rotate) */
transform: rotate3d(1, 1, 1, 45deg); /* 벡터 기준 회전 */
/* 3D 크기 조절 */
transform: scaleZ(2);
transform: scale3d(1.5, 1.5, 2);
}
Perspective (원근감)
/* 부모에 적용 */
.container {
perspective: 1000px; /* 원근 거리 */
perspective-origin: 50% 50%; /* 시점 */
}
/* 또는 자식에 적용 */
.box {
transform: perspective(1000px) rotateY(45deg);
}
예제:
<style>
.perspective-demo {
display: flex;
gap: 50px;
padding: 50px;
}
.container {
perspective: 1000px;
width: 200px;
height: 200px;
}
.box {
width: 100%;
height: 100%;
background: #3498db;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
transition: transform 0.6s;
}
.container:hover .box {
transform: rotateY(180deg);
}
.container-1 { perspective: 500px; } /* 가까움 */
.container-2 { perspective: 1000px; } /* 중간 */
.container-3 { perspective: 2000px; } /* 멀음 */
</style>
<div class="perspective-demo">
<div class="container container-1">
<div class="box">500px</div>
</div>
<div class="container container-2">
<div class="box">1000px</div>
</div>
<div class="container container-3">
<div class="box">2000px</div>
</div>
</div>
Transform Origin (기준점)
.box {
/* 기준점 변경 */
transform-origin: center center; /* 기본값 */
transform-origin: top left; /* 왼쪽 위 */
transform-origin: bottom right; /* 오른쪽 아래 */
transform-origin: 50% 50%; /* 퍼센트 */
transform-origin: 0 0; /* 픽셀 */
}
예제:
<style>
.origin-demo {
display: flex;
gap: 50px;
padding: 50px;
}
.box {
width: 100px;
height: 100px;
background: #3498db;
transition: transform 0.3s;
}
.box-1 {
transform-origin: center;
}
.box-1:hover {
transform: rotate(45deg); /* 중심 기준 */
}
.box-2 {
transform-origin: top left;
}
.box-2:hover {
transform: rotate(45deg); /* 왼쪽 위 기준 */
}
.box-3 {
transform-origin: bottom right;
}
.box-3:hover {
transform: rotate(45deg); /* 오른쪽 아래 기준 */
}
</style>
<div class="origin-demo">
<div class="box box-1"></div>
<div class="box box-2"></div>
<div class="box box-3"></div>
</div>
3. Animation (@keyframes)
Keyframes 정의
/* from-to 형식 */
@keyframes slide {
from {
transform: translateX(0);
opacity: 0;
}
to {
transform: translateX(300px);
opacity: 1;
}
}
/* 퍼센트 형식 (더 유연) */
@keyframes bounce {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-100px);
}
100% {
transform: translateY(0);
}
}
/* 복잡한 애니메이션 */
@keyframes complex {
0% {
transform: translateX(0) rotate(0deg);
background: #3498db;
}
25% {
transform: translateX(100px) rotate(90deg);
background: #2ecc71;
}
50% {
transform: translateX(200px) rotate(180deg);
background: #e74c3c;
}
75% {
transform: translateX(100px) rotate(270deg);
background: #f39c12;
}
100% {
transform: translateX(0) rotate(360deg);
background: #3498db;
}
}
Animation 적용
.box {
/* 개별 속성 */
animation-name: slide; /* 애니메이션 이름 */
animation-duration: 2s; /* 지속 시간 */
animation-timing-function: ease-in-out; /* 타이밍 함수 */
animation-delay: 0s; /* 지연 시간 */
animation-iteration-count: infinite; /* 반복 횟수 */
animation-direction: alternate; /* 방향 */
animation-fill-mode: forwards; /* 채우기 모드 */
animation-play-state: running; /* 재생 상태 */
/* 축약형 (권장) */
animation: slide 2s ease-in-out 0s infinite alternate forwards;
}
Animation 속성 상세
.box {
/* 반복 횟수 */
animation-iteration-count: 1; /* 1번 */
animation-iteration-count: 3; /* 3번 */
animation-iteration-count: infinite; /* 무한 */
/* 방향 */
animation-direction: normal; /* 정방향 (기본값) */
animation-direction: reverse; /* 역방향 */
animation-direction: alternate; /* 정방향-역방향 반복 */
animation-direction: alternate-reverse;/* 역방향-정방향 반복 */
/* 채우기 모드 */
animation-fill-mode: none; /* 기본값, 원래 상태 */
animation-fill-mode: forwards; /* 마지막 상태 유지 */
animation-fill-mode: backwards; /* 첫 상태로 시작 */
animation-fill-mode: both; /* forwards + backwards */
/* 재생 상태 */
animation-play-state: running; /* 재생 (기본값) */
animation-play-state: paused; /* 일시정지 */
}
실전 예제
<style>
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
.notification {
position: relative;
display: inline-block;
padding: 10px 20px;
background: #3498db;
color: white;
border-radius: 5px;
}
.notification::after {
content: '';
position: absolute;
top: -5px;
right: -5px;
width: 20px;
height: 20px;
background: #e74c3c;
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
}
</style>
<div class="notification">
새 메시지
</div>
4. 실전 예제
예제 1: 버튼 효과 모음
<!DOCTYPE html>
<html lang="ko">
<head>
<style>
.button-demo {
display: flex;
gap: 20px;
padding: 50px;
flex-wrap: wrap;
}
.btn {
padding: 15px 30px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 1rem;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
/* 1. 호버 리프트 */
.btn-lift {
transition: all 0.3s ease;
}
.btn-lift:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.btn-lift:active {
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
}
/* 2. 확대 */
.btn-scale {
transition: transform 0.3s ease;
}
.btn-scale:hover {
transform: scale(1.1);
}
/* 3. 그라데이션 이동 */
.btn-gradient {
background: linear-gradient(90deg, #3498db, #2ecc71);
background-size: 200% 100%;
background-position: 0% 0%;
transition: background-position 0.5s ease;
}
.btn-gradient:hover {
background-position: 100% 0%;
}
/* 4. 테두리 애니메이션 */
.btn-border {
position: relative;
overflow: hidden;
transition: color 0.3s ease;
}
.btn-border::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: #2ecc71;
transition: left 0.3s ease;
z-index: -1;
}
.btn-border:hover {
color: white;
}
.btn-border:hover::before {
left: 0;
}
/* 5. 흔들기 */
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.btn-shake:hover {
animation: shake 0.3s ease;
}
</style>
</head>
<body>
<div class="button-demo">
<button class="btn btn-lift">리프트</button>
<button class="btn btn-scale">확대</button>
<button class="btn btn-gradient">그라데이션</button>
<button class="btn btn-border">테두리</button>
<button class="btn btn-shake">흔들기</button>
</div>
</body>
</html>
예제 2: 로딩 애니메이션
<style>
/* 스피너 */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 점 3개 */
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.dots {
display: flex;
gap: 10px;
}
.dot {
width: 15px;
height: 15px;
background: #3498db;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out;
}
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
.dot:nth-child(3) { animation-delay: 0s; }
/* 바 */
@keyframes progress {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
.progress-bar {
width: 200px;
height: 4px;
background: #ecf0f1;
border-radius: 2px;
overflow: hidden;
}
.progress-bar::after {
content: '';
display: block;
width: 20%;
height: 100%;
background: #3498db;
animation: progress 1.5s ease-in-out infinite;
}
</style>
<div style="display: flex; gap: 50px; padding: 50px;">
<div class="spinner"></div>
<div class="dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<div class="progress-bar"></div>
</div>
예제 3: 카드 플립
<style>
.flip-container {
perspective: 1000px;
width: 300px;
height: 200px;
}
.flip-card {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.flip-container:hover .flip-card {
transform: rotateY(180deg);
}
.flip-front, .flip-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-size: 1.5rem;
font-weight: bold;
}
.flip-front {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.flip-back {
background: linear-gradient(135deg, #f093fb, #f5576c);
color: white;
transform: rotateY(180deg);
}
</style>
<div class="flip-container">
<div class="flip-card">
<div class="flip-front">앞면</div>
<div class="flip-back">뒷면</div>
</div>
</div>
예제 4: 페이드 인 애니메이션
<style>
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-on-scroll {
animation: fadeIn 1s ease-out;
}
.fade-in-1 { animation: fadeIn 1s ease-out 0s both; }
.fade-in-2 { animation: fadeIn 1s ease-out 0.2s both; }
.fade-in-3 { animation: fadeIn 1s ease-out 0.4s both; }
.fade-in-4 { animation: fadeInLeft 1s ease-out 0.6s both; }
.fade-in-5 { animation: fadeInScale 1s ease-out 0.8s both; }
.content-box {
padding: 20px;
margin: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
</style>
<div class="content-box fade-in-1">첫 번째 요소</div>
<div class="content-box fade-in-2">두 번째 요소</div>
<div class="content-box fade-in-3">세 번째 요소</div>
<div class="content-box fade-in-4">네 번째 요소 (왼쪽에서)</div>
<div class="content-box fade-in-5">다섯 번째 요소 (확대)</div>
예제 5: 무한 애니메이션
<style>
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
.float-box {
width: 100px;
height: 100px;
background: #3498db;
border-radius: 10px;
animation: float 3s ease-in-out infinite;
}
.rotate-box {
width: 100px;
height: 100px;
background: #e74c3c;
animation: rotate 4s linear infinite;
}
.pulse-box {
width: 100px;
height: 100px;
background: #2ecc71;
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
</style>
<div style="display: flex; gap: 50px; padding: 50px;">
<div class="float-box"></div>
<div class="rotate-box"></div>
<div class="pulse-box"></div>
</div>
5. 성능 최적화
GPU 가속 속성
빠른 속성 (GPU 가속):
.box {
/* ✅ 권장: 레이아웃에 영향 없음 */
transform: translateX(100px);
transform: scale(1.5);
transform: rotate(45deg);
opacity: 0.5;
}
느린 속성 (레이아웃 변경):
.box {
/* ❌ 비권장: 레이아웃 재계산 */
width: 200px;
height: 200px;
left: 100px;
top: 100px;
margin: 20px;
padding: 20px;
}
성능 비교
| 속성 | 레이아웃 | 페인트 | 합성 | 성능 |
|---|---|---|---|---|
transform | ❌ | ❌ | ✅ | 빠름 |
opacity | ❌ | ❌ | ✅ | 빠름 |
background-color | ❌ | ✅ | ❌ | 보통 |
width, height | ✅ | ✅ | ✅ | 느림 |
left, top | ✅ | ✅ | ✅ | 느림 |
will-change
애니메이션 전에 브라우저에 미리 알립니다:
.box {
/* 애니메이션 전 최적화 힌트 */
will-change: transform, opacity;
}
.box:hover {
transform: scale(1.2);
opacity: 0.8;
}
주의사항:
/* ❌ 나쁨: 모든 요소에 적용 */
* {
will-change: transform; /* 메모리 낭비 */
}
/* ✅ 좋음: 필요한 요소만 */
.animated-element {
will-change: transform;
}
/* ✅ 더 좋음: JavaScript로 동적 추가/제거 */
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('animationend', () => {
element.style.willChange = 'auto';
});
transform3d 강제 GPU 가속
.box {
/* 2D transform도 GPU 가속 */
transform: translateZ(0); /* 또는 translate3d(0, 0, 0) */
}
6. 자주 발생하는 문제
1. 애니메이션이 끊김
문제: 60fps를 유지하지 못함
해결:
/* ❌ 나쁨 */
.box {
transition: width 0.3s; /* 레이아웃 재계산 */
}
.box:hover {
width: 200px;
}
/* ✅ 좋음 */
.box {
transition: transform 0.3s; /* GPU 가속 */
}
.box:hover {
transform: scaleX(2);
}
2. 애니메이션이 즉시 시작됨
문제: 페이지 로드 시 애니메이션이 바로 실행
해결:
/* ❌ 나쁨 */
.box {
animation: slide 2s ease-in-out infinite;
}
/* ✅ 좋음: 클래스로 제어 */
.box.animate {
animation: slide 2s ease-in-out infinite;
}
// JavaScript로 클래스 추가
setTimeout(() => {
document.querySelector('.box').classList.add('animate');
}, 100);
3. 애니메이션 후 원래 상태로 돌아감
문제: 애니메이션 종료 후 초기 상태로 복귀
해결:
.box {
animation: slide 2s ease-in-out forwards;
/* ↑ 마지막 상태 유지 */
}
4. transform 조합 순서
문제: 예상과 다른 결과
/* 다른 결과 */
transform: rotate(45deg) translate(100px, 0);
/* vs */
transform: translate(100px, 0) rotate(45deg);
해결: 순서를 이해하고 의도에 맞게 작성
5. z-index가 작동하지 않음
문제: transform 사용 시 z-index 무시
해결:
.box {
position: relative; /* 또는 absolute */
z-index: 10;
transform: translateX(50px);
}
7. 실전 팁
1. 자주 사용하는 애니메이션
/* 페이드 인 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 슬라이드 인 */
@keyframes slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
/* 바운스 */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
/* 흔들기 */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
/* 회전 */
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
2. 타이밍 함수 선택
/* 자연스러운 움직임 */
.natural { transition: all 0.3s ease-out; }
/* 바운스 효과 */
.bounce { transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }
/* 부드러운 시작 */
.smooth { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
3. 반응형 애니메이션
/* 데스크톱: 애니메이션 활성화 */
.box {
transition: transform 0.3s ease;
}
/* 모바일: 애니메이션 비활성화 (성능) */
@media (max-width: 768px) {
.box {
transition: none;
}
}
/* 사용자 설정 존중 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
4. 디버깅
/* 애니메이션 느리게 */
.box {
animation: slide 10s ease-in-out infinite;
/* ↑ 10초로 느리게 */
}
/* 일시정지 */
.box {
animation-play-state: paused;
}
Chrome DevTools:
- Elements 패널
- Animations 탭
- 애니메이션 타임라인 확인
8. 고급 기법
1. 다중 애니메이션
.box {
animation:
slide 2s ease-in-out infinite,
rotate 3s linear infinite,
pulse 1s ease-in-out infinite;
}
2. 애니메이션 체이닝
@keyframes sequence {
0% {
transform: translateX(0);
background: #3498db;
}
33% {
transform: translateX(100px);
background: #2ecc71;
}
66% {
transform: translateX(100px) translateY(100px);
background: #e74c3c;
}
100% {
transform: translateX(0) translateY(0);
background: #3498db;
}
}
.box {
animation: sequence 3s ease-in-out infinite;
}
3. JavaScript 제어
<style>
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(300px); }
}
.box {
width: 100px;
height: 100px;
background: #3498db;
}
.box.animate {
animation: slide 1s ease-in-out forwards;
}
</style>
<div class="box" id="box"></div>
<button id="startBtn">시작</button>
<button id="pauseBtn">일시정지</button>
<button id="resetBtn">리셋</button>
<script>
const box = document.getElementById('box');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
startBtn.addEventListener('click', () => {
box.classList.add('animate');
box.style.animationPlayState = 'running';
});
pauseBtn.addEventListener('click', () => {
box.style.animationPlayState = 'paused';
});
resetBtn.addEventListener('click', () => {
box.classList.remove('animate');
void box.offsetWidth; /* 리플로우 강제 */
box.classList.add('animate');
});
box.addEventListener('animationend', () => {
console.log('애니메이션 완료');
});
</script>
9. 실전 프로젝트
프로젝트: 인터랙티브 카드
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>인터랙티브 카드</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea, #764ba2);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
max-width: 1200px;
}
.card {
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}
.card-image {
width: 100%;
height: 200px;
background: linear-gradient(135deg, #667eea, #764ba2);
position: relative;
overflow: hidden;
}
.card-image::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(
45deg,
transparent,
rgba(255,255,255,0.3),
transparent
);
transform: translateX(-100%);
transition: transform 0.6s;
}
.card:hover .card-image::before {
transform: translateX(100%);
}
.card-content {
padding: 25px;
}
.card h3 {
margin: 0 0 10px 0;
color: #2c3e50;
font-size: 1.5rem;
}
.card p {
margin: 0 0 20px 0;
color: #7f8c8d;
line-height: 1.6;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.price {
font-size: 1.8rem;
font-weight: bold;
color: #3498db;
}
.btn {
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255,255,255,0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.btn:hover::before {
width: 300px;
height: 300px;
}
.btn:hover {
background: #2980b9;
transform: scale(1.05);
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fadeInUp 0.6s ease-out;
}
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
</style>
</head>
<body>
<div class="card-grid">
<div class="card">
<div class="card-image"></div>
<div class="card-content">
<h3>상품 1</h3>
<p>멋진 상품입니다. 지금 구매하세요!</p>
<div class="card-footer">
<span class="price">29,000원</span>
<button class="btn">구매</button>
</div>
</div>
</div>
<div class="card">
<div class="card-image"></div>
<div class="card-content">
<h3>상품 2</h3>
<p>최고의 품질을 자랑합니다.</p>
<div class="card-footer">
<span class="price">39,000원</span>
<button class="btn">구매</button>
</div>
</div>
</div>
<div class="card">
<div class="card-image"></div>
<div class="card-content">
<h3>상품 3</h3>
<p>특별한 할인 중입니다.</p>
<div class="card-footer">
<span class="price">49,000원</span>
<button class="btn">구매</button>
</div>
</div>
</div>
</div>
</body>
</html>
10. 브라우저 지원
지원 현황
| 속성 | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
transition | 26+ | 16+ | 9+ | 12+ | 10+ |
transform | 36+ | 16+ | 9+ | 12+ | 10+ |
animation | 43+ | 16+ | 9+ | 12+ | 10+ |
will-change | 36+ | 36+ | 9.1+ | 79+ | ❌ |
벤더 프리픽스 (구형 브라우저)
.box {
-webkit-transition: all 0.3s ease; /* Safari, Chrome */
-moz-transition: all 0.3s ease; /* Firefox */
-o-transition: all 0.3s ease; /* Opera */
transition: all 0.3s ease; /* 표준 */
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
@-webkit-keyframes slide {
from { -webkit-transform: translateX(0); }
to { -webkit-transform: translateX(300px); }
}
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(300px); }
}
정리
핵심 요약
- Transition: A→B 상태 변화 애니메이션
- Transform: 이동, 회전, 크기, 기울이기
- Animation: @keyframes로 복잡한 애니메이션
- 성능: transform, opacity만 사용 (GPU 가속)
- will-change: 애니메이션 최적화 힌트
- 타이밍 함수: ease, ease-in-out, cubic-bezier
- fill-mode: forwards로 마지막 상태 유지
애니메이션 속성 비교
| 속성 | 용도 | 트리거 | 반복 | 복잡도 |
|---|---|---|---|---|
transition | 상태 변화 | hover, focus 등 | 1회 | 간단 |
animation | 복잡한 애니메이션 | 자동 또는 클래스 | 설정 가능 | 복잡 |
성능 최적화 체크리스트
- ✅
transform,opacity만 사용 - ✅
will-change로 최적화 힌트 - ✅
transform3d(0,0,0)로 GPU 가속 강제 - ✅ 60fps 유지 (16.67ms/프레임)
- ✅
prefers-reduced-motion존중 - ❌
width,height,left,top애니메이션 피하기 - ❌ 모든 요소에
will-change적용 금지
실전 팁
- ease-out: 대부분의 경우 자연스러움
- 0.3s: 일반적인 transition 시간
- forwards: 애니메이션 마지막 상태 유지
- transform 조합: 순서 중요
- GPU 가속: transform, opacity 사용
- JavaScript 제어: 클래스 추가/제거
- 반응형: 모바일에서 애니메이션 줄이기
- 접근성: prefers-reduced-motion 존중
다음 단계
- JavaScript DOM 조작
- JavaScript 애니메이션 라이브러리
- React 애니메이션
관련 글
- HTML/CSS 시작하기 | 웹 개발 첫걸음
- CSS 기초 | 선택자, 속성, 색상, 폰트