웹 페이지에서 요소가 부드럽게 움직이는 건 어떻게 가능한 걸까요? 그리고 왜 어떤 애니메이션은 버벅이고, 어떤 건 매끄러울까요?

CSS 애니메이션 — Transition, Keyframes, 그리고 성능 최적화

CSS 애니메이션은 프론트엔드 개발에서 빠질 수 없는 영역입니다. 버튼에 호버 효과를 넣는 것부터 로딩 스피너, 페이지 전환 효과까지 — 결국 다 CSS 애니메이션이에요. 공부하다 보니 "그냥 움직이게만 하면 되는 거 아닌가?" 싶었는데, 성능까지 고려하면 알아야 할 게 꽤 있었습니다.


1. transition — 상태 변화를 부드럽게

transition은 CSS 속성이 변할 때 그 변화를 부드럽게 보간해주는 기능입니다. 가장 흔한 예가 :hover에서 색상이나 크기가 서서히 바뀌는 효과예요.

기본 문법

CSS
/* 개별 속성 */
.box {
  transition-property: background-color;  /* 어떤 속성을 */
  transition-duration: 0.3s;              /* 얼마나 걸려서 */
  transition-timing-function: ease;       /* 어떤 속도 곡선으로 */
  transition-delay: 0s;                   /* 얼마 뒤에 시작할지 */
}

/* 단축 속성 — 실무에서는 이 방식을 더 많이 씁니다 */
.box {
  transition: background-color 0.3s ease 0s;
}

핵심은 transition상태 변화가 트리거 되어야 동작한다는 점입니다. hover, focus, class 추가/제거 같은 이벤트가 없으면 아무 일도 일어나지 않아요.

여러 속성을 동시에 전환

CSS
.card {
  /* 각 속성별로 다른 duration 지정 가능 */
  transition: transform 0.3s ease,
              box-shadow 0.3s ease 0.1s,   /* 0.1초 지연 */
              opacity 0.2s linear;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  opacity: 0.95;
}

transition: all 0.3s ease도 동작하지만, 의도하지 않은 속성까지 전환될 수 있어서 구체적으로 명시하는 게 좋습니다. 성능 측면에서도 all보다 개별 속성 지정이 유리해요.


2. timing-function — 속도 곡선 이해하기

애니메이션이 "자연스럽다"고 느끼는 건 속도 곡선 덕분입니다. CSS는 cubic-bezier 곡선을 기반으로 속도를 제어해요.

주요 키워드

키워드cubic-bezier 값특징
linear(0, 0, 1, 1)일정한 속도 — 기계적인 느낌
ease(0.25, 0.1, 0.25, 1)기본값. 부드럽게 시작하고 부드럽게 끝남
ease-in(0.42, 0, 1, 1)느리게 시작, 빠르게 끝남
ease-out(0, 0, 0.58, 1)빠르게 시작, 느리게 끝남
ease-in-out(0.42, 0, 0.58, 1)느리게 시작, 느리게 끝남

커스텀 cubic-bezier

CSS
.bounce-effect {
  /* cubic-bezier로 바운스 느낌 구현 */
  transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

cubic-bezier의 네 값은 베지어 곡선의 두 제어점 좌표입니다. cubic-bezier.com에서 시각적으로 조정하면서 원하는 곡선을 찾을 수 있어요. 직접 만져보면 각 값이 어떤 영향을 주는지 감이 빨리 옵니다.

steps() — 프레임 단위 애니메이션

CSS
/* 스프라이트 시트 애니메이션에 유용 */
.sprite {
  animation: walk 0.6s steps(6) infinite;
}

steps()는 부드러운 보간 대신 뚝뚝 끊어지는 프레임 전환을 만듭니다. 스프라이트 애니메이션이나 타이핑 효과를 만들 때 유용해요.


3. @keyframes 애니메이션

transition이 A → B 두 상태 사이의 전환이라면, @keyframes는 여러 단계를 거치는 복잡한 애니메이션을 정의할 수 있습니다.

기본 구조

CSS
/* 키프레임 정의 */
@keyframes fadeIn {
  from {  /* 0%와 같음 */
    opacity: 0;
    transform: translateY(20px);
  }
  to {    /* 100%와 같음 */
    opacity: 1;
    transform: translateY(0);
  }
}

/* 적용 */
.element {
  animation-name: fadeIn;
  animation-duration: 0.6s;
  animation-timing-function: ease-out;
  animation-delay: 0s;
  animation-iteration-count: 1;         /* infinite면 무한 반복 */
  animation-direction: normal;          /* reverse, alternate 등 */
  animation-fill-mode: forwards;        /* 종료 후 상태 유지 */
}

/* 단축 속성 */
.element {
  animation: fadeIn 0.6s ease-out forwards;
}

여러 단계가 있는 키프레임

CSS
@keyframes pulse {
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.5);
  }
  50% {
    transform: scale(1.05);
    box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
  }
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
  }
}

주요 속성 정리

  • animation-iteration-count: 1, 3, infinite — 반복 횟수
  • animation-direction: normal(정방향), reverse(역방향), alternate(정↔역 교대)
  • animation-fill-mode: none(기본, 애니메이션 전후 원래 상태), forwards(마지막 프레임 유지), backwards(시작 전 첫 프레임 적용), both(둘 다)
  • animation-play-state: running / paused — JS로 일시정지할 때 유용

fill-mode를 모르면 "애니메이션 끝나고 원래대로 돌아가는데 왜 그런 거지?" 하는 상황을 겪게 됩니다. forwards를 빼먹은 거예요.


4. transform — 레이아웃을 건드리지 않는 변환

transform은 요소의 시각적 표현만 바꿉니다. 문서 흐름에 영향을 주지 않기 때문에 ** 애니메이션에 가장 적합한 속성 **이에요.

주요 함수들

CSS
.box {
  /* 이동 — 원래 위치에서 상대적으로 */
  transform: translate(50px, 20px);    /* X, Y */
  transform: translateX(50px);
  transform: translateY(-20px);

  /* 회전 */
  transform: rotate(45deg);            /* 시계 방향 45도 */
  transform: rotate(-90deg);           /* 반시계 방향 */

  /* 크기 조정 */
  transform: scale(1.5);              /* 1.5배 확대 */
  transform: scale(0.8, 1.2);         /* X축 0.8배, Y축 1.2배 */

  /* 기울이기 */
  transform: skew(10deg, 5deg);       /* X축, Y축 */
}

복합 변환

CSS
.card:hover {
  /* 여러 변환을 한 줄에 — 순서가 결과에 영향을 줍니다 */
  transform: translateY(-8px) rotate(2deg) scale(1.02);
}

복합 변환에서 순서가 중요한 이유는, CSS가 변환을 오른쪽에서 왼쪽으로 적용하기 때문입니다. rotatetranslate하면 회전된 좌표계 기준으로 이동하게 돼요.

transform-origin

CSS
/* 변환의 기준점 설정 — 기본값은 요소의 중심 */
.door {
  transform-origin: left center;  /* 왼쪽 가운데를 축으로 */
  transition: transform 0.5s;
}

.door:hover {
  transform: rotateY(-90deg);     /* 문이 열리는 효과 */
}

transform이 레이아웃에 영향을 주지 않는다는 건, 요소를 translate로 이동시켜도 주변 요소들이 밀리지 않는다는 뜻입니다. 시각적으로만 이동하고 원래 자리는 그대로 남아 있어요. 이게 바로 성능상 유리한 이유이기도 합니다.


5. will-change와 GPU 가속

브라우저는 특정 CSS 속성의 변경을 감지하면 해당 요소를 별도의 Composite Layer(합성 레이어)로 분리해서 GPU에서 처리합니다. 이게 "GPU 가속"이에요.

자동으로 GPU 가속되는 속성

  • transform
  • opacity
  • filter

이 속성들은 요소의 레이아웃이나 페인팅을 다시 하지 않고 GPU가 레이어를 합성하는 것만으로 처리할 수 있어서 매우 빠릅니다.

will-change — 브라우저에게 미리 알려주기

CSS
/* 호버 전에 부모에서 미리 힌트를 줌 */
.card-container:hover .card {
  will-change: transform;
}

.card:active {
  transform: scale(0.98);
}

will-change는 브라우저에게 "이 속성이 곧 바뀔 거야"라고 알려주는 힌트입니다. 브라우저는 이 정보를 바탕으로 미리 합성 레이어를 만들어 둡니다.

will-change 남용 주의

CSS
/* 이렇게 하면 안 됩니다 */
* {
  will-change: transform, opacity;  /* 모든 요소에 적용 — 메모리 폭발 */
}

.element {
  will-change: all;                 /* 'all'은 의미 없는 값 */
}

남용하면 안 되는 이유:

  • 합성 레이어 하나당 GPU 메모리를 추가로 소모합니다
  • 너무 많은 레이어를 만들면 오히려 성능이 악화 됩니다
  • 이미 transform이나 opacity를 사용 중이면 브라우저가 알아서 최적화하므로 will-change가 불필요한 경우가 많습니다

가장 좋은 패턴은 애니메이션 직전에 추가하고, 끝나면 제거하는 것 입니다.

JAVASCRIPT
// JS에서 동적으로 관리하는 패턴
element.addEventListener('mouseenter', () => {
  element.style.willChange = 'transform';
});

element.addEventListener('animationend', () => {
  element.style.willChange = 'auto';  // 원복
});

6. Reflow vs Repaint vs Composite — 렌더링 파이프라인

브라우저가 화면을 그리는 과정을 이해하면 왜 transformleft보다 빠른지 알 수 있습니다.

렌더링 파이프라인

PLAINTEXT
JavaScript → Style → Layout(Reflow) → Paint(Repaint) → Composite

각 단계에서 하는 일:

  1. Layout(Reflow): 요소의 크기와 위치를 계산합니다
  2. Paint(Repaint): 계산된 레이아웃을 바탕으로 픽셀을 그립니다
  3. Composite: 그려진 레이어들을 합성하여 최종 화면을 만듭니다

속성별 비용 차이

변경 속성트리거되는 단계비용
width, height, margin, paddingLayout → Paint → Composite높음
color, background, box-shadowPaint → Composite중간
transform, opacityComposite만낮음

같은 결과, 다른 성능

CSS
/* 나쁜 예 — Layout을 매 프레임마다 다시 계산 */
.box-slow {
  transition: left 0.3s, top 0.3s;
  position: absolute;
  left: 0;
}
.box-slow:hover {
  left: 100px;
  top: 50px;
}

/* 좋은 예 — Composite만 발생 */
.box-fast {
  transition: transform 0.3s;
}
.box-fast:hover {
  transform: translate(100px, 50px);
}

결과는 같지만 성능 차이는 큽니다. left를 바꾸면 브라우저가 해당 요소와 주변 요소들의 레이아웃을 모두 다시 계산하는 반면, transform은 GPU가 레이어만 이동시키면 끝이에요.

이 차이가 데스크톱에서는 잘 안 느껴질 수 있지만, 모바일이나 저사양 기기에서는 체감이 확실합니다.


7. 실전 예제

버튼 호버 효과

CSS
.button {
  padding: 12px 24px;
  background-color: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;

  /* transform과 box-shadow만 전환 — 성능 최적화 */
  transition: transform 0.2s ease,
              box-shadow 0.2s ease;
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

.button:active {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}

로딩 스피너

CSS
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid #e5e7eb;
  border-top-color: #3b82f6;  /* 위쪽만 색상 변경 */
  border-radius: 50%;

  /* transform만 사용 — GPU 가속 */
  animation: spin 0.8s linear infinite;
}

페이드인 애니메이션

CSS
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.fade-in {
  animation: fadeInUp 0.6s ease-out forwards;
}

/* 순차적으로 나타나게 하려면 delay 활용 */
.fade-in:nth-child(1) { animation-delay: 0s; }
.fade-in:nth-child(2) { animation-delay: 0.1s; }
.fade-in:nth-child(3) { animation-delay: 0.2s; }

순차 페이드인에서 animation-fill-mode: forwards를 빼먹으면 delay 동안 요소가 보였다가 사라졌다 나타나는 깜빡임이 발생합니다. backwardsboth를 함께 쓰면 delay 동안에도 from 상태를 유지할 수 있어요.

CSS
/* 깜빡임 방지: delay 동안 from 상태 유지 + 종료 후 to 상태 유지 */
.fade-in {
  animation: fadeInUp 0.6s ease-out both;  /* both = forwards + backwards */
}

정리

  • transition 은 상태 변화(hover, focus, class 토글)에 반응하는 단순한 애니메이션에 적합합니다
  • @keyframes 는 여러 단계를 거치는 복잡한 애니메이션이나 자동 실행이 필요할 때 사용합니다
  • transform과 opacity 를 우선적으로 사용하세요 — Composite 단계만 거치므로 성능이 가장 좋습니다
  • left, top, width 같은 Layout 속성 애니메이션은 피하고, transform: translate()로 대체하세요
  • will-change 는 꼭 필요한 곳에만 — 남용하면 메모리 낭비로 역효과가 납니다
  • animation-fill-mode를 이해하면 "애니메이션 끝나면 원래대로 돌아가는" 문제를 해결할 수 있습니다
댓글 로딩 중...