CSS 애니메이션 | Transition, Animation, Transform
이 글의 핵심
CSS 애니메이션: Transition, Animation, Transform. Transition (전환)·Transform (변형).
들어가며
60fps 안 나와서…
예전에 대시보드랑 랜딩 둘 다, 스크롤할 때 프레임이 40대로 떨어지는 걸 봤다. 맥에선 “어? 쫀듯한데?” 싶은데, 구형 안드로이드나 저전력 랩탑에선만 손님이 툭툭 튄다. Performance 패널 열어 보니 width·left·box-shadow·filter가 한 프레임에 몰아치고 있었음. 솔직히 난 답이 하나라고 본다. GPU 가속이 답. transform이랑 opacity로 흉내낼 수 있으면 거기로 옮기고, 아니면 애초에 그 모션을 빼는 쪽. 그렇게 정리하니 60에 가깝게 돌아왔다. (완벽한 60에 목숨 걸진 않는다. 근데 체감이 완전 다름.)
transition·animation은 상태(호버, 로딩, 열림/닫힘)를 보여 주는 데 쓰고, 복잡한 로직은 JS로 넘기는 식. 예쁘기만 하면 끝이 아니고, 눈에 보이는 전환 + 메인 스레드 안 쳐 박기 둘 다 챙기는 쪽이 맞다고 본다.
애니메이션 써도 될 것 같을 때 (나 기준):
- 체감·피드백: 눌렸다, 열렸다, 돌고 있다
- 시선: 진짜 중요한 데만 살짝
- 성능: 여기 안 맞으면 “멋짐” 포기하는 편
뼈대만 세면 이 세 가지:
- Transition: A → B 한 방
- Transform: 레이아웃 안 뜯고 눈에만 움직이기
- Animation: 키프레임·반복·자동 재생
실제로는 이렇게 터졌음
DB 쿼리 이야기가 아니라 스크롤·리플로우 쪽이었다. 문서는 다들 “가능하면 GPU” 정도로만 쓰는데, 현장에선 transition: all + width 흔드는 조합이 너무 많다. 나도 처음엔 그랬고. Framerate가 안 나올 때 GPU 가속이 답이라는 건, DevTools에 한번 찍어 보고 Layout/Paint가 붉게 뜨는 걸 눈으로 봤을 때 제대로 박힌다. 아래 코드랑 섹션은 그 기준으로 골랐다. 트러블슈팅 밑에도 비슷한 케이스 몇 개 있음.
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;
}
성능비 — 표 말고 감으로
나는 합성(Composite) 쪽으로만 가는 애를 기본으로 둔다. 그게 곧 GPU 가속 루트랑 잘 맞는다.
transform·opacity: 레이아웃 안 건드리고 합성 위주 → 빠르다고 믿고 간다.background-color같은 건 페인트 → 괜찮은데, 동시에 많이 돌리면 부담.width·height·left·top·margin류: 레이아웃·페인트·합성 다 탄다 → 60fps 안 나올 때 제일 먼저 의심.
즉, “표로 암기”보다 DevTools에 빨간 게 어디냐를 보는 게 빠름. 내 취향으론 의심스러우면 transform으로 우회가 1순위.
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. 브라우저 지원
지원 현황 (표 대신 짧게)
요즘 쓰는 크로미움·파이어폭스·사파리·엣지면 transition / transform / animation은 그냥 있다고 봐도 됨(구형은 각각 26·36·43대 이후쯤부터였던 걸로 기억). will-change는 IE는 빼고, 사파리는 9.1+부터. 버전 외우느니 caniuse 한 번이 낫다. 나는 표 외우는 대신 “프로젝트가 지원하는 최소 브라우저”에만 맞춘다.
벤더 프리픽스 (구형 브라우저)
.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); }
}
정리
핵심 요약 (한 번 더: GPU 가속이 답)
- Transition: A→B. 피드백용.
- Transform: 눈에만 옮기기. 레이아웃 뜯지 말기.
- Animation: 키프레임·루프. 스피너 같은 거.
- 성능: 난
transform·opacity믿는 편. 60fps 안 나오면 여기부터. - will-change: 힌트. 남발 금지(위에도 씀).
- 타이밍: UI는
ease-out많이 씀. 스피너는linear. - fill-mode: 멈출 프레임 정할 때
forwards같이 씀.
transition이냐 animation이냐 (표 싫어서 리스트)
transition: 두 상태 사이만 부드럽게. hover·focus·클래스 토글. 한 번에 끝나는 거.animation: 키프레임 여러 박스, 자동 재생·반복. 로드하자마자 돌아가야 하면 이쪽.
성능 최적화 체크리스트
transform·opacity먼저. 이게 1번. (GPU 가속이 답이라 했죠)will-change는 짧게, 필요한 요소만translateZ(0)류 꼼수는 남용 말고, 병목 찍힐 때- 60이 안 나오면 16.67ms 떠올리기보다 Performance 찍기
prefers-reduced-motion은 진짜로 켤 수 있음width·left로 모션? 가능하면transform으로 우회*에will-change박지 말기
실전 팁
- ease-out: 클릭·호버 느낌 좋음
- 0.2~0.35s 정도가 많이 쓰는 구간(팀마다 다름)
- forwards로 끝 자세 고정
- transform 곱하는 순서 = 결과 다름, 헷갈리면 한 줄씩
- 또 말함: GPU 쪽 = transform, opacity
- JS는 클래스 토글이 제일 덜 지저분함
- 모바일은 효과 줄이는 편이 손님한테 잘 먹음
- 모션 끄는 사람 생각.
prefers-reduced-motion
다음 단계
- JavaScript DOM 조작
- JavaScript 애니메이션 라이브러리
- React 애니메이션
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「CSS 애니메이션 | Transition, Animation, Transform」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴 (표 대신 말로)
- 관측성: 상관 ID, p95/p99, 타임아웃·재시도가 대시보드에 붙어 있나.
- 안전성: 검증·권한·시크릿·감사 로그가 경로마다 일관적인가.
- 신뢰성: 멱등 아닌 데 재시도 안 하고, 서킷·백오프·DLQ 쓰는지.
- 성능: 캐시, 풀, 인덱스, 백프레셔가 스케일에 맞는지.
- 배포: 롤백 루트, 카나리, 마이그레이션이 문서돼 있나.
- 용량: 피크·FD·스레드 상한을 가끔은 찌르는지.
스테이징은 데이터 양·RTT·동시성을 프로덕에 가깝게 맞출수록 삽질이 줄어듦.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「CSS 애니메이션 | Transition, Animation, Transform」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting) — 이것도 표 아님
- 간헐 실패 → 레이스, DNS, 외부 지연. 최소 재현, 트레이스, 재시도·서킷 설정.
- 느려짐 → N+1, 동기 I/O, 락, 캐시 미스. 핫스팟 하나씩.
- 메모리만 커짐 → 캐시 무한, 리스너 누수, 버퍼, 커넥션. TTL·스냅샷.
- CI만 깨짐 → env, 권한, lockfile, 이미지 핀. 로컬 diff.
- 환경끼리 다름 → 시크릿·리전. 단일 소스.
- 데이터 꼬임 → 멱등 재시도, 캐시 무효화. 아웃박스.
권장 순서: (1) 최소 재현 (2) 최근 diff (3) 환경 차이 (4) 메트릭으로 검증 (5) 부하·회귀.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. CSS 애니메이션: Transition, Animation, Transform. Transition (전환)·Transform (변형)로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Algorithm Generate | ‘생성 알고리즘’ 가이드
- C++ Algorithm Replace | ‘치환 알고리즘’ 가이드
- C++ 비트 연산 | ‘비트마스크’ 완벽 가이드
이 글에서 다루는 키워드 (관련 검색어)
CSS, 애니메이션, transition, transform 등으로 검색하시면 이 글이 도움이 됩니다.