Skip to content
This page has been auto-translated and may contain errors.View in English

트랜지션과 애니메이션

docs.scrimba.com

버튼에 :hover 색상을 추가하면 동작은 하지만 변화가 뚝뚝 끊깁니다. 한 프레임에서는 파란색이고 다음 프레임에서는 어두운 색인데, 그 중간 과정이 없기 때문입니다. 아주 살짝만 페이드를 넣어줘도 페이지 전체가 훨씬 덜 뻣뻣하게 느껴집니다. 이 페이드가 바로 트랜지션이며, 두 상태 사이의 움직임을 표현할 수 있게 되면 같은 도구를 확장해서 호버 효과, 위로 떠오르는 카드, 로딩 중에 회전하는 스피너까지 만들 수 있습니다. 이 장에서는 CSS가 요소를 어떻게 움직이는지, 그리고 페이지를 더 쓰기 어렵게 만들지 않으면서 움직이는 방법을 다룹니다.

트랜지션

**트랜지션(transition)**은 속성 값이 바뀔 때 옛 값에서 새 값으로 뚝 끊기지 않고 애니메이션하도록 브라우저에 지시합니다. 평상시 상태와 호버 상태, 이렇게 두 상태를 정해두면 트랜지션이 그 중간 프레임들을 채워줍니다.

호버 시 색이 바뀌는 버튼을 예로 들어보겠습니다. 트랜지션이 없으면 색이 순간적으로 바뀌고, 있으면 서서히 바뀝니다.

css
.btn {
  background: #3b82f6;
  transition: background 0.2s;   /* 0.2초에 걸쳐 색을 서서히 바꿈 */
}
.btn:hover {
  background: #1d4ed8;           /* 애니메이션이 향할 새 상태 */
}

transition은 평상시 상태에 지정하며, :hover에는 넣지 않습니다. 그래야 마우스가 들어올 때와 나갈 때 모두 적용됩니다.

**트랜지션(transition)**은 어떤 속성 값이 바뀔 때마다, 대개 :hover:focus 같은 상태가 작동하거나 클래스가 추가될 때 두 값 사이를 애니메이션합니다. 애니메이션을 프레임 단위로 일일이 지정하는 것이 아니라, 속성과 지속 시간을 지정하면 브라우저가 그 사이 값을 보간해줍니다.

transition 축약 속성은 다음 순서로 네 부분을 가집니다: 지켜볼 속성, 지속 시간, 타이밍 함수(속도 곡선), 그리고 선택적인 지연 시간입니다.

css
.btn {
  background: #3b82f6;
  transition: background 0.2s ease-in-out 0s;
  /* 속성 | 지속 시간 | 타이밍 함수 | 지연 시간 */
}
.btn:hover {
  background: #1d4ed8;
}

여러 속성을 쉼표로 구분해서 한꺼번에 트랜지션할 수 있으며, 트랜지션은 기본 상태에 선언해서 호버에 들어갈 때와 나갈 때를 모두 커버하도록 합니다.

css
.card {
  transition: transform 0.2s, box-shadow 0.2s;   /* 두 속성, 같은 타이밍 */
}

**트랜지션(transition)**은 계산된 속성 값이 바뀔 때마다 시작 값과 끝 값 사이를 시간에 따라 보간합니다. 트리거는 그 속성에 생기는 어떤 변화든 될 수 있습니다: :hover 같은 가상 클래스, JavaScript에서 토글하는 클래스, 인라인 스타일 편집 등입니다. 네 개의 하위 속성은 transition-property, transition-duration, transition-timing-function, transition-delay이며, 축약 표기는 속성, 지속 시간, 타이밍, 지연 순서로 씁니다.

트랜지션이 실행되는지는 두 가지 조건에 달려 있습니다. 먼저 속성이 *애니메이션 가능(animatable)*해야 합니다. 즉 두 값 사이에 정의된 보간 방식이 있어야 합니다. background-color, opacity, transform은 그렇지만, display 같은 속성은 이산적(discrete)이라 전통적으로 트위닝 없이 순간 전환됩니다. 그리고 시작 값과 끝 값이 둘 다 구체적인 값이어야 합니다. auto를 오가는 트랜지션은 불가능하며, 이것이 heightauto로 애니메이션해도 아무 일도 일어나지 않아서 사람들이 대신 max-heighttransform을 쓰는 이유입니다.

css
.btn {
  background: #3b82f6;
  transition: background-color 0.2s ease-in-out;   /* 애니메이션 가능하고 두 값 모두 구체적 */
}
.btn:hover {
  background: #1d4ed8;
}

트랜지션은 :hover 규칙이 아니라 기본 규칙에 선언하세요. 적용되는 트랜지션 속성은 요소의 현재 상태에서 읽어오므로, 기본 상태에 두면 진입과 복귀 모두를 관장하게 됩니다. :hover에만 두면 들어갈 때만 애니메이션되고 나갈 때는 순간적으로 끊깁니다.

Juno트랜지션 트랜지션은 변화를 순간적으로 끊기게 하는 대신 부드럽게 만들어줍니다. 호버 시 버튼 색이 서서히 바뀌는 것처럼요. :hover가 아니라 평상시 상태에 작성해야 들어갈 때와 나갈 때 모두 동작합니다. propertyduration만 있어도 이미 동작하는 페이드가 완성됩니다.
Juno트랜지션transition 축약 표기는 속성, 지속 시간, 타이밍 함수, 지연 순서로 읽으며, 여러 개를 쉼표로 나열할 수 있습니다. 상태에 진입할 때와 벗어날 때를 모두 커버하려면 기본 규칙에 넣으세요. 호버가 들어갈 때는 부드럽고 나갈 때는 뚝 끊긴다면, 트랜지션이 기본이 아니라 :hover에 걸려 있는 경우가 많습니다. 흔히 걸리는 함정이죠.
Juno트랜지션 트랜지션은 애니메이션 가능한 속성이면서 두 끝 값이 모두 구체적일 때만 동작하며, 그래서 height: auto로 트랜지션하면 조용히 아무 일도 일어나지 않습니다. 나가는 방향에서 순간적으로 끊기지 않고 양방향 모두 관장하려면 선언을 기본 상태에 두세요. 네 개의 하위 속성을 알고 나면 축약 표기는 그저 그 순서일 뿐입니다: 속성, 지속 시간, 타이밍, 지연.

Transform

**transform**은 주변에 아무런 영향을 주지 않으면서 요소를 이동, 확대/축소, 회전시킵니다. 대개 애니메이션하고 싶은 대상이 바로 이것인데, 부드럽게 보이고 주변 요소를 밀어내지 않기 때문입니다.

자주 쓰게 될 세 가지가 있습니다:

css
.card:hover {
  transform: translateY(-4px);   /* 위로 4픽셀 이동 */
}
.icon:hover {
  transform: scale(1.1);         /* 원래 크기의 110%로 확대 */
}
.arrow.open {
  transform: rotate(90deg);      /* 시계 방향으로 4분의 1바퀴 회전 */
}

translate는 이동, scale은 크기 조절, rotate는 회전을 담당합니다. 이 중 하나를 트랜지션과 짝지으면 위로 떠오르는 카드, 튀어오르는 아이콘, 활짝 펴지는 화살표를 만들 수 있습니다.

transform은 레이아웃상의 위치는 바꾸지 않으면서 요소를 시각적으로 이동, 크기 조절, 회전시킵니다. 마지막 부분이 핵심입니다. 변형된 요소는 새로운 위치에 그려지지만, 원래 차지하던 공간은 그대로 예약되어 있어서 주변 요소가 리플로우되지 않습니다. 이런 이유로 이동을 애니메이션할 때는 top이나 margin이 아니라 transform을 쓰게 됩니다.

가장 많이 쓰는 세 가지는 translate(), scale(), rotate()이며, 하나의 선언 안에서 조합할 수 있습니다.

css
.card {
  transition: transform 0.2s ease-out;
}
.card:hover {
  transform: translateY(-4px) scale(1.02);   /* 살짝 떠오르면서 커짐 */
}

이 함수들은 왼쪽부터 오른쪽 순으로 적용되며, transform-origin은 그 회전/확대의 기준점을 지정합니다. 기본값은 요소의 중심입니다. 이를 top left로 바꾸면 rotate가 중심이 아니라 모서리를 기준으로 돌아갑니다.

css
.badge {
  transform-origin: top left;
  transform: rotate(-8deg);
}

transform은 문서 흐름에 영향을 주지 않으면서 요소를 자신만의 페인트 레이어 안에서 재배치합니다. 즉 박스가 원래 차지했던 공간은 그대로 예약되어 있어서, transform 값이 바뀌어도 형제 요소가 리플로우되지 않고 레이아웃이 다시 계산되지 않습니다. 바로 이 특성 때문에 transform이 애니메이션하기에 적합한 속성이 되는데, 아래 성능에 관한 Deep Dive에서 이를 정확히 설명합니다.

translate(), scale(), rotate()가 흔히 쓰는 함수들이며, 순서가 중요합니다. 이 함수들은 사실상 오른쪽에서 왼쪽으로 적용되는 행렬 곱셈처럼 합성되기 때문입니다. translateX(50px) rotate(45deg)는 이동한 뒤에 이동된 요소를 회전시키지만, 순서를 뒤집으면 먼저 회전한 뒤 회전된 축을 따라 이동하므로 결과가 달라집니다. transform-originscalerotate의 기준점을 지정하며, 기본값은 요소의 중심(50% 50%)입니다.

css
.panel {
  transform-origin: top center;
  transition: transform 0.25s ease-out;
}
.panel.collapsed {
  transform: scaleY(0);          /* 위쪽 가장자리 방향으로 접힘 */
}

실전에서 알아둘 제약이 하나 있습니다. transform은 이런 방식으로 배치 가능한 박스를 생성하는 요소에만 적용되므로, <span>처럼 아무 설정도 없는 인라인 요소에는 아무 효과가 없습니다. 먼저 요소에 display: inline-block이나 block을 지정하세요. 요소를 함께 배치(positioning)한다면, transform이 포지셔닝 컨텍스트와 어떻게 상호작용하는지도 알아두어야 합니다. 변형된 요소는 절대 위치로 지정된 자식 요소들의 containing block이 되기 때문입니다.

JunoTransformtransform은 주변 요소를 밀어내지 않으면서 요소를 이동, 확대/축소, 회전시킵니다. 기억해야 할 세 가지는 이동하는 translate, 크기를 조절하는 scale, 회전시키는 rotate입니다. 트랜지션과 결합하면 호버 시 떠오르는 카드나 튀어오르는 아이콘을 만들 수 있습니다.
JunoTransformtransform은 요소가 레이아웃에서 차지하는 자리는 바꾸지 않고 보이는 모습만 바꾸므로 주변 요소가 절대 리플로우되지 않습니다. 그래서 top이나 margin 대신 이걸 애니메이션하는 겁니다. translate, scale, rotate를 하나의 선언에 쌓으면 왼쪽에서 오른쪽 순으로 적용됩니다. 회전이나 확대의 기준점을 중심이 아닌 다른 곳으로 옮기고 싶을 때는 transform-origin을 쓰세요.
JunoTransform transform 함수는 순서대로 합성되므로 translateX(50px) rotate(45deg)는 순서를 뒤집은 것과 결과가 다르며, transform-origin은 회전이나 확대의 기준점을 지정합니다. 아무 설정 없는 인라인 요소에는 inline-block으로 바꾸기 전까지 아무 효과가 없습니다. 또한 이 속성은 요소를 절대 위치 자식들의 containing block으로 만들기도 하므로, 나중에 예상 못한 동작을 만날 수 있다는 점도 기억해두세요.

키프레임 애니메이션

트랜지션은 두 상태 사이를 애니메이션합니다. 두 상태보다 더 많은 단계가 필요하거나 동작이 반복되어야 할 때는 **키프레임 애니메이션(keyframe animation)**을 씁니다. @keyframes로 단계를 작성한 다음, animation 속성으로 그 단계들을 요소에 연결합니다.

로딩 스피너가 전형적인 예입니다. 계속 회전해야 하므로 트랜지션으로는 불가능합니다.

css
@keyframes spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
.spinner {
  animation: spin 1s linear infinite;   /* spin 실행, 한 바퀴에 1초, 무한 반복 */
}

from은 시작, to는 끝입니다. animation 줄에는 어떤 키프레임을 실행할지, 한 주기가 얼마나 걸릴지, 속도 곡선, 몇 번 반복할지를 지정합니다. infinite는 절대 멈추지 않는다는 뜻입니다.

@keyframes는 이름이 붙은 단계들의 시퀀스를 정의하고, animation 속성이 이를 요소에서 실행합니다. 트랜지션과 달리 애니메이션은 상태 변화가 트리거로 필요하지 않고 스스로 시작하며, 0%부터 100%까지의 백분율을 써서 원하는 만큼 여러 단계를 가질 수 있습니다.

css
@keyframes pulse {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.05); }   /* 중간 단계 */
  100% { transform: scale(1); }
}
.badge {
  animation: pulse 1.5s ease-in-out infinite;
}

animation 축약 표기는 여러 하위 속성을 하나로 묶습니다: 이름, 지속 시간, 타이밍 함수, 지연, 반복 횟수, 방향입니다. infinite는 무한 반복하고, 숫자를 지정하면 그만큼 반복하며, 방향으로 alternate를 쓰면 매 주기가 역방향으로 진행되어 처음으로 튕겨 돌아가지 않고 왔다 갔다 하는 움직임이 됩니다.

css
.spinner {
  animation: spin 1s linear infinite;
}

@keyframes는 백분율로 표현되는 시점들에서의 속성 값 타임라인을 이름과 함께 선언합니다(fromto는 각각 0%100%의 별칭입니다). animation은 그 타임라인을 요소에 연결합니다. 애니메이션은 스스로 트리거되고 반복될 수 있다는 점이 트랜지션과의 경계선입니다. 상태 변화를 부드럽게 만들 때는 트랜지션을, 스스로 진행되거나 반복되는 동작에는 애니메이션을 쓰세요.

animation 축약 표기는 여덟 개의 하위 속성을 담습니다: animation-name, -duration, -timing-function, -delay, -iteration-count, -direction, -fill-mode, -play-state입니다. 이 중 두 가지가 사람들을 자주 헷갈리게 합니다. animation-fill-mode: forwards를 지정하면 애니메이션이 끝난 뒤 애니메이션 적용 전 스타일로 튕겨 돌아가지 않고 마지막 키프레임 상태를 유지합니다. 한 번만 실행되는 등장 애니메이션에 딱 맞는 설정입니다. 그리고 타이밍 함수는 애니메이션 전체가 아니라 키프레임 구간마다 적용되므로, 세 단계짜리 애니메이션에 ease-in-out을 걸면 처음부터 끝까지 한 번이 아니라 각 구간마다 가속·감속이 일어납니다.

css
@keyframes slide-in {
  from { opacity: 0; transform: translateX(-16px); }
  to   { opacity: 1; transform: translateX(0); }
}
.toast {
  animation: slide-in 0.3s ease-out forwards;   /* 끝 상태를 유지 */
}

트랜지션과 같은 이유로 키프레임 안에서도 transformopacity를 애니메이션하는 쪽을 택하세요. 다음 절에서 그 이유를 풀어보면, 이 두 속성은 애니메이션 비용이 적고 대부분의 다른 속성은 그렇지 않기 때문입니다.

Juno키프레임 애니메이션 두 상태로는 부족하거나 동작이 반복되어야 할 때는 @keyframes에 단계를 작성하고 animation으로 실행하세요. 스피너가 대표적인 예입니다. 0도인 from, 360도인 to, 그리고 infinite로 반복합니다. animation 줄에는 어떤 키프레임, 얼마 동안, 어떤 곡선, 몇 번 반복할지를 지정합니다.
Juno키프레임 애니메이션@keyframes는 단계를 백분율로 담고 animation이 이를 실행하며, 상태 변화가 필요 없습니다. 축약 표기에는 이름, 지속 시간, 타이밍, 지연, 반복 횟수, 방향이 담겨 있어서 infinite는 무한 반복하고 alternate는 매 주기를 왔다 갔다 하게 만듭니다. 여러 단계짜리 동작이나 반복되는 동작은 트랜지션이 아니라 여기에 속합니다.
Juno키프레임 애니메이션 타이밍 함수는 애니메이션 전체에 한 번이 아니라 키프레임 구간마다 적용되는데, 이걸 모르면 이즈드 처리된 다단계 애니메이션이 처음 볼 때 어색하게 뚝뚝 끊기는 것처럼 느껴집니다. 한 번만 실행되는 등장 애니메이션이 끝난 뒤 원래 상태로 튕겨 돌아가지 않고 끝 상태를 유지해야 한다면 animation-fill-mode: forwards를 쓰세요. 그리고 가능하면 키프레임도 transformopacity에만 걸어두세요. 다음에 나올 성능 이야기와 관련이 있습니다.

사용자를 존중하는 동작

모든 사람이 화면에서 움직임을 원하는 건 아닙니다. 어떤 사람에게는 산만하게 느껴지고, 어떤 사람에게는 실제로 불편함이나 멀미를 유발할 수도 있습니다. 브라우저는 시스템 설정에서 동작을 줄여달라고 요청할 수 있게 해주며, CSS는 **prefers-reduced-motion**으로 이 요청을 읽을 수 있습니다.

동작을 줄여달라고 요청하지 않은 사람에게만 애니메이션이 실행되도록 애니메이션을 조건문으로 감싸세요.

css
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.2s;
  }
}

사용자를 존중하는 데 있어 나머지 절반은 절제입니다. 동작을 짧고 은은하게 유지하고, 호버나 클릭처럼 독자가 실제로 한 행동과 연결시키세요. 실제 변화에 주의를 끄는 동작은 도움이 되지만, 그저 장식용 동작은 금방 지겨워집니다.

동작은 중립적이지 않습니다. 어떤 독자에게는 애니메이션이 산만하게 느껴지고, 전정 기관 관련 질환이 있는 사람에게는 크거나 갑작스러운 동작이 어지럼증이나 멀미를 유발할 수 있습니다. 운영체제는 "동작 줄이기" 설정을 제공하며, prefers-reduced-motion 미디어 기능을 쓰면 CSS가 이 설정을 존중할 수 있습니다.

동작은 옵트아웃이 아니라 옵트인으로 다루세요. 차분한 기본 상태를 기본값으로 작성한 다음, no-preference 쿼리 안에 동작을 얹으세요.

css
.card {
  transition: none;              /* 모두에게 적용되는 차분한 기본값 */
}
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.2s ease-out;
  }
}

미디어 쿼리를 넘어서 보면, 좋은 동작과 잡음을 가르는 것은 결국 목적입니다. 동작은 변화를 설명해줄 때 존재 가치가 있습니다. 메뉴가 앵커된 방향에서 슬라이드되어 나오거나, 토스트가 페이드되어 갑자기 나타나지 않는 것처럼요. 빠르고 은은한 동작은 완성도 있게 느껴지지만, 느리고 큰 동작은 방해물처럼 느껴집니다.

prefers-reduced-motion은 독자가 OS 단계에서 선택한 접근성 설정을 반영하는 미디어 기능이며, 이를 존중하는 것은 부가 기능이 아니라 기본 요건입니다. 요청하지 않은 동작이 전정 기관 관련 불편을 유발할 수 있기 때문입니다. 취해야 할 패턴은 동작을 줄인 상태를 기본값으로 작성하고 @media (prefers-reduced-motion: no-preference) 안에서 동작을 활성화하는 것입니다. 이렇게 하면 이 기능을 지원하지 않는 브라우저의 독자도 동작이 아니라 차분한 기본 상태를 받게 됩니다.

"줄인다"는 것이 "없앤다"는 뜻인 경우는 거의 없습니다. 슬라이드나 확대/축소는 적절하지 않아도 페이드나 색상 변화는 대개 괜찮으므로, 모든 피드백을 없애기보다는 큰 위치 이동을 작은 투명도 변화로 바꾸는 것이 유용한 방법입니다.

css
.modal {
  animation: slide-in 0.3s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .modal {
    animation: fade-in 0.3s ease-out;   /* 피드백은 유지하되 움직임은 제거 */
  }
}

목적과 이징(easing)이 나머지를 결정합니다. 등장 동작은 빠르게 시작해서 서서히 안정되는 ease-out이 가장 잘 맞습니다. 요소가 의도를 가지고 도착해서 멈춰 서는 느낌을 주기 때문입니다. 퇴장 동작은 대개 ease-in이 적합합니다. 지속 시간은 짧게(인터페이스 피드백이라면 대략 150ms에서 300ms), 동작은 작게 유지하세요. 흔한 실수는 근사한 애니메이션이 모든 상호작용에 0.25초의 대기 시간을 더하는 경우입니다. 동작은 상태 변화를 명확히 보여줘야 하며, 장식이 되어서는 안 됩니다.

Juno사용자를 존중하는 동작 어떤 사람들은 동작을 산만하게 느끼거나 심지어 신체적으로 불편하게 느끼며, 브라우저에 동작을 줄여달라고 요청할 수 있습니다. prefers-reduced-motion으로 이를 읽어서 동작을 줄이지 않은 사람에게만 애니메이션을 실행하세요. 그리고 추가하는 동작 자체도 짧고 은은하게, 독자가 실제로 한 행동과 연결되도록 유지하세요.
Juno사용자를 존중하는 동작 차분한 버전을 기본값으로 작성하고 no-preference 쿼리 안에 동작을 추가하세요. 그러면 동작은 사람들이 애써 피해야 하는 것이 아니라 옵트인 방식이 됩니다. 좋은 동작은 메뉴가 앵커에서 슬라이드되어 나오는 것처럼 변화를 설명해주고, 장식용 동작은 금방 지겨워집니다. 빠르고 작은 동작은 완성도 있게 느껴지고, 느리고 큰 동작은 방해물처럼 느껴집니다.
Juno사용자를 존중하는 동작 줄인다는 것이 없앤다는 뜻인 경우는 드뭅니다. 슬라이드를 페이드로 바꿔서 동작을 줄여달라고 요청한 사람도 여전히 피드백을 받게 하고, 이 기능을 지원하지 않는 브라우저도 차분한 상태로 떨어지도록 기본값으로 설정하세요. 등장 동작에는 ease-out을 쓰고, 인터페이스 피드백은 150에서 300밀리초 정도로 유지하세요. 클릭마다 대기 시간을 더하는 근사한 애니메이션은 비용이지 기능이 아닙니다.

무엇을 애니메이션할지, 그리고 왜 그것이 중요한지

나중에 골치 아픈 일을 줄여줄 간단한 규칙 하나: 움직임을 애니메이션할 때는 width, height, top, margin 같은 속성이 아니라 transformopacity를 애니메이션하세요.

css
/* 부드러움: 브라우저가 저렴하게 처리 */
.card:hover {
  transform: translateY(-4px);
  opacity: 0.9;
}

이유는 transformopacity가 브라우저가 가장 저렴하게 움직일 수 있는 두 속성이기 때문입니다. 그래서 이 두 속성을 기반으로 만든 애니메이션은 느린 기기에서도 매끄럽게 유지됩니다. 크기나 위치 속성을 애니메이션하면 브라우저가 매 프레임마다 더 많은 작업을 해야 하고, 동작이 뚝뚝 끊길 수 있습니다. 더 깊이 파고들 때 전체 이유를 만나게 되겠지만, 이 습관은 지금부터 들여둘 만한 가치가 있습니다.

이 장의 예제들이 transformopacity에 기대는 데는 이유가 있습니다. 이 두 속성은 애니메이션하기에 가장 저렴하고, 대부분의 다른 속성은 그렇지 않습니다. width, height, top, margin을 애니메이션하면 브라우저가 매 프레임마다 레이아웃을 다시 계산해야 하는데, 이는 비용이 많이 들고 느린 기기에서는 뚝뚝 끊기는 현상으로 나타납니다.

실전 규칙은 가능하면 동작을 transformopacity로 표현하는 것입니다. 무언가를 움직이고 싶다면 top 대신 translate를 쓰세요. 크기를 조절하고 싶다면 width 대신 scale을 쓰세요. 무언가를 나타나게 하고 싶다면 height를 0에서 애니메이션하지 말고 opacity를 페이드시키세요.

css
/* 비용이 큼: 레이아웃 속성을 애니메이션 */
.panel {
  transition: height 0.3s;       /* 매 프레임마다 레이아웃 재계산 */
}

/* 저렴함: 같은 효과를 transform으로 표현 */
.panel {
  transition: transform 0.3s;
  transform: scaleY(0);          /* 레이아웃 작업 없음 */
}

이는 transform이 왜 주변 요소를 건드리지 않는지와도 연결됩니다. 레이아웃을 전혀 건드리지 않기 때문에 브라우저가 페이지를 다시 계산하지 않고도 애니메이션할 수 있는 것입니다. 색상이나 배경 트랜지션도 함께 다루고 싶다면, 색상과 배경에서 그중 어떤 것들이 보간되는지 다룹니다.

앞선 절들이 가리켰던 Deep Dive가 바로 이것입니다: transformopacity는 왜 애니메이션하기 저렴하고 거의 나머지 모든 속성은 왜 그렇지 않은가. 이는 브라우저의 렌더링 파이프라인, 즉 세 단계로 진행되는 과정으로 귀결됩니다. *레이아웃(Layout)*은 모든 박스의 크기와 위치를 계산합니다. *페인트(Paint)*는 색상, 텍스트, 테두리 같은 픽셀을 채웁니다. *컴포지트(Composite)*는 페인트된 레이어들을 최종 프레임으로 합성하며, 이 단계는 GPU가 처리할 수 있는 단계입니다.

transform이나 opacity를 애니메이션하는 것은 컴포지트 단계에서만 완전히 처리될 수 있습니다. 브라우저는 요소를 한 번만 페인트한 뒤 별도의 컴포지터 레이어(compositor layer)(GPU가 독립적으로 이동하고 블렌딩할 수 있는 별도로 페인트된 표면)로 승격시키고, 그 뒤로는 매 프레임마다 그 레이어를 새 위치나 투명도로 재합성만 합니다. 레이아웃도, 리페인트도 없습니다. 반면 widthtop을 애니메이션하면 레이아웃이 무효화되어, 브라우저가 매 프레임마다 기하 구조를 다시 계산하고 다시 그려야 하며, 이것이 부하가 걸릴 때 프레임이 끊기는 원인입니다.

css
/* 컴포지트만 사용: 저렴하고 GPU 친화적 */
.card {
  transition: transform 0.2s, opacity 0.2s;
}

/* 매 프레임마다 레이아웃과 페인트를 유발: 애니메이션을 피해야 함 */
.card-heavy {
  transition: width 0.2s, top 0.2s;
}

will-change를 쓰면 어떤 속성이 곧 애니메이션될 것임을 미리 알려서 브라우저가 그 레이어를 사전에 승격시킬 수 있게 해줍니다: will-change: transform. 각 승격된 레이어가 메모리를 소모하므로 아껴 쓰고 애니메이션이 끝나면 제거하세요. 모든 것을 승격시키면 목적이 무색해지고 오히려 페이지가 느려질 수 있습니다. 이는 측정된 뚝뚝 끊김 현상에 대한 표적 해결책으로 취급해야 하며, 기본값으로 걸어둘 것은 아닙니다.

이징(easing) 역시 정확히 짚어둘 만한 또 다른 레버입니다. 타이밍 함수는 경과 시간을 진행 정도로 매핑하는 곡선입니다. ease-out(빠르게 시작해서 느려짐)은 요소가 도착해서 안정되는 느낌을 주기 때문에 등장 동작에 적합합니다. ease-in은 퇴장 동작에 적합하고, linear는 스피너처럼 연속적인 동작에 적합합니다. 커스텀 곡선이 필요하다면 cubic-bezier(x1, y1, x2, y2)가 두 개의 제어점으로 곡선을 정의하며, 내장 키워드들도 그 아래에서는 이름 붙은 cubic-bezier일 뿐입니다. 성능 예산을 항상 염두에 두세요: 한 프레임의 시간(60fps에서 약 16ms) 안에 머무는 것을 목표로 하고, 컴포지트 단계에서만 처리되는 속성을 애니메이션하며, will-change를 쓰기 전에 먼저 프로파일러를 써보세요. 이런 값들 여러 개를 커스텀 속성으로 다룬다면 인터페이스 전체에서 지속 시간과 이징을 일관되게 유지할 수 있습니다.

Juno무엇을 애니메이션할지, 그리고 왜 그것이 중요한지transformopacity를 애니메이션하고, width, height, top, margin을 애니메이션하는 건 피하세요. 이 두 속성이 브라우저가 움직이기에 가장 저렴하므로, 느린 기기에서도 동작이 매끄럽게 유지됩니다. 전체 이유는 나중에 배우게 되겠지만, 이 습관은 지금부터 시작할 만한 가치가 있습니다.
Juno무엇을 애니메이션할지, 그리고 왜 그것이 중요한지 동작을 transformopacity로 표현하세요. top 대신 translate, width 대신 scale을 쓰고, height를 0에서 애니메이션하는 대신 opacity를 페이드시키세요. 레이아웃 속성을 애니메이션하면 브라우저가 매 프레임마다 레이아웃을 다시 처리해야 해서 느린 기기에서 뚝뚝 끊깁니다. transform이 주변 요소를 그대로 두는 것과 같은 이유입니다. 레이아웃을 전혀 건드리지 않기 때문이죠.
Juno무엇을 애니메이션할지, 그리고 왜 그것이 중요한지transformopacity는 자신만의 GPU 레이어에서 컴포지트 단계로만 애니메이션되어 레이아웃과 페인트를 건너뛰지만, widthtop은 매 프레임마다 레이아웃을 무효화해서 부하가 걸릴 때 프레임이 끊깁니다. will-change는 측정된 뚝뚝 끊김 현상에 대한 표적 해결책으로 쓰고 그 후에는 제거하세요. 승격된 레이어마다 메모리를 소모하니까요. 이징은 동작의 성격에 맞게 고르세요. 등장 동작에는 ease-out을, 그리고 최적화하기 전에 먼저 프로파일링하세요.