본문으로 건너뛰기
Previous
Next
CSS 애니메이션 | Transition, Animation, Transform

CSS 애니메이션 | Transition, Animation, Transform

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로 넘기는 식. 예쁘기만 하면 끝이 아니고, 눈에 보이는 전환 + 메인 스레드 안 쳐 박기 둘 다 챙기는 쪽이 맞다고 본다.

애니메이션 써도 될 것 같을 때 (나 기준):

  • 체감·피드백: 눌렸다, 열렸다, 돌고 있다
  • 시선: 진짜 중요한 데만 살짝
  • 성능: 여기 안 맞으면 “멋짐” 포기하는 편

뼈대만 세면 이 세 가지:

  1. Transition: A → B 한 방
  2. Transform: 레이아웃 안 뜯고 눈에만 움직이기
  3. 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:

  1. Elements 패널
  2. Animations 탭
  3. 애니메이션 타임라인 확인

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 가속이 답)

  1. Transition: A→B. 피드백용.
  2. Transform: 눈에만 옮기기. 레이아웃 뜯지 말기.
  3. Animation: 키프레임·루프. 스피너 같은 거.
  4. 성능: 난 transform·opacity 믿는 편. 60fps 안 나오면 여기부터.
  5. will-change: 힌트. 남발 금지(위에도 씀).
  6. 타이밍: UI는 ease-out 많이 씀. 스피너는 linear.
  7. 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 박지 말기

실전 팁

  1. ease-out: 클릭·호버 느낌 좋음
  2. 0.2~0.35s 정도가 많이 쓰는 구간(팀마다 다름)
  3. forwards로 끝 자세 고정
  4. transform 곱하는 순서 = 결과 다름, 헷갈리면 한 줄씩
  5. 또 말함: GPU 쪽 = transform, opacity
  6. JS는 클래스 토글이 제일 덜 지저분함
  7. 모바일은 효과 줄이는 편이 손님한테 잘 먹음
  8. 모션 끄는 사람 생각. prefers-reduced-motion

다음 단계


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「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」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 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 addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. CSS 애니메이션: Transition, Animation, Transform. Transition (전환)·Transform (변형)로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

CSS, 애니메이션, transition, transform 등으로 검색하시면 이 글이 도움이 됩니다.