본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 16:29

2026년까지 JavaScript가 필요했던 5가지 CSS 애니메이션

요약

JavaScript 없이 CSS만으로 구현 가능한 5가지 애니메이션 기술을 소개합니다. interpolate-size, @starting-style 등 최신 CSS 속성을 활용해 높이 자동 조절 및 요소의 등장/퇴장 효과를 구현하는 방법을 다룹니다.

핵심 포인트

  • interpolate-size를 통한 height: auto 애니메이션 구현
  • @starting-style로 display: none 상태의 요소 애니메이션 가능
  • ::details-content를 활용한 네이티브 아코디언 구현
  • @property와 overlay 속성을 이용한 고급 그라데이션 및 다이얼로그 제어
  • JavaScript 라이브러리 의존성을 줄이는 최신 CSS 트렌드
  • interpolate-sizecalc-size 덕분에 이제 측정 스크립트 없이도 패널을 height: auto로 전환할 수 있게 되었습니다.

  • transition-behavior: allow-discrete@starting-style을 사용하면 display: none 상태인 요소가 나타나거나 사라질 때 애니메이션을 적용할 수 있습니다.

  • ::details-content는 네이티브 details 요소를 JavaScript 없이도 부드러운 공개/폐쇄(disclosure) 요소로 만들어 줍니다.

  • 타입이 지정된 @propertyoverlay 속성은 과거에 JavaScript가 필요했던 그라데이션(gradients), 카운터(counters), 다이얼로그(dialogs)를 처리합니다.

저는 이미 저의 JavaScript 라이브러리들을 조용히 대체한 CSS 애니메이션들에 대해 글을 쓴 적이 있습니다. 이번 글은 그 후속편으로, 단순히 CSS로 구현하기 쉬워진 것을 넘어 올해 전까지는 진정으로 불가능했던 몇 가지 기술들에 대해 다룹니다. 알 수 없는 높이(unknown height)로의 전환, 페이지를 떠나기 전 요소의 페이드 아웃(fade out), 부드러운 네이티브 아코디언(accordion), 사라지는 대신 닫히는 애니메이션이 적용되는 다이얼로그(dialog) 등이 그것입니다. 2025년까지만 해도 이 모든 것에는 스크립트가 필요했습니다. 2026년에는 브라우저가 이를 수행하며, 이것들이 제가 현재 가장 먼저 찾는 다섯 가지 방법입니다.

마침내, height: auto로의 전환

이것은 CSS에서 가장 오래된 장벽이었습니다. height0에서 auto로 전환할 수 없었기 때문에, 아코디언은 갑자기 툭 펼쳐지거나, JavaScript로 scrollHeight를 측정하여 콘텐츠가 변경되는 순간 깨져버리는 픽셀 값으로 애니메이션을 적용해야 했습니다. grid-template-rows: 0fr에서 1fr로 전환하는 트릭이 도움이 되긴 했지만, 그 과정에서 패딩(padding)과 테두리(border)가 왜곡되는 문제가 있었습니다.


:root { interpolate-size: allow-keywords; }

...

interpolate-size: allow-keywordsauto, min-content, fit-content와 같은 고유 크기 (intrinsic sizes)로 애니메이션을 수행할 수 있도록 페이지 설정을 변경합니다. 이것이 바로 핵심적인 해제 요소입니다. 콘텐츠가 커지거나 뷰포트 (viewport)가 변경될 때 측정할 필요도, 픽셀 목표치를 정할 필요도, 리사이즈 옵저버 (Resize Observer)를 사용할 필요도 없습니다. 더 세밀한 제어가 필요할 때는 calc-size(auto, size)를 사용하여 고유 값에 대해 계산할 수 있으므로, auto에 고정된 오프셋을 더하거나 값을 제한 (clamp) 할 수 있습니다. 이 단일 기능은 목록에 있는 그 어떤 것보다도 제 레이아웃 JavaScript를 더 많이 퇴출시켰는데, 왜냐하면 거의 모든 확장 패널, 드롭다운, FAQ 섹션이 사실은 높이를 측정하는 루틴을 숨기고 있었기 때문입니다.

두 가지 주의사항이 있습니다. interpolate-size는 상속되므로, :root에 한 번 선언하면 모든 곳에서 동작이 전환되며, 이는 의도한 바와 일치합니다. 그리고 애니메이션 도중에는 hidden 대신 overflow: clip을 사용하세요. clip은 높이 변화와 충돌하는 의도치 않은 스크롤 컨테이너를 생성하지 않기 때문입니다. 속성을 설정하고, auto로 애니메이션을 적용한 뒤, 픽셀 단위로 생각하는 것을 멈추세요.

display: none 상태로 애니메이션 인/아웃 구현하기

무언가를 나타나게 하는 애니메이션은 언제나 가능했습니다. 문제는 사라지게 하는 애니메이션이었습니다. display: none을 설정하는 즉시 트랜지션 (transition)이 끊겨버렸기 때문에, 일반적인 해결책은 페이드 (fade) 효과가 완료될 때까지 제거를 지연시키는 setTimeout을 사용하는 것이었습니다. 하지만 타이밍이 어긋나거나 컴포넌트가 조기에 언마운트 (unmount) 되면 문제가 발생했습니다.

.toast {
  transition: opacity 0.3s, transform 0.3s, display 0.3s allow-discrete;
...

allow-discrete를 사용하면 이산적 (discrete) 속성인 display가 트랜지션에 참여할 수 있습니다. @starting-style은 삽입 전의 시작 상태를 제공하여, 요소가 갑자기 나타나는 대신 애니메이션과 함께 나타나도록 합니다. 사라지는 과정에서는 트랜지션이 완료된 후에만 displaynone으로 전환되므로, 페이드 효과가 실제로 재생됩니다. 타이머도, 고립된 노드 (orphaned nodes)도 없으며, 요소는 정확히 적절한 프레임에서 레이아웃을 떠납니다. 토스트 (Toasts), 드롭다운, 툴팁 모두 이 하나의 패턴으로 수렴됩니다.

처음에는 순서 때문에 혼란을 겪을 수 있습니다. allow-discrete는 반드시 display transition에 위치해야 하며, @starting-style은 요소가 스타일링되는 첫 번째 시점, 즉 삽입(insertion) 시점에만 적용됩니다. 만약 진입 애니메이션 (entry animation)이 재생되지 않는다면, 거의 항상 @starting-style 블록이 누락된 경우입니다. 만약 퇴장 애니메이션 (exit animation)이 재생되지 않는다면, display transition에 allow-discrete가 누락된 것입니다.

네이티브로 구현된 애니메이션 디스클로저 (Animated Disclosure)

네이티브 and 쌍은 올바른 아코디언 (accordion) 구현 방식입니다. 키보드 토글, 스크린 리더 시맨틱스 (screen-reader semantics), 페이지 내 찾기 확장 기능이 모두 기본으로 제공됩니다. 문제는 애니메이션 없이 갑자기 툭 열린다는 점이었고, 이 때문에 팀들은 이를 JavaScript로 다시 구현하면서 그 과정에서 시맨틱스 (semantics)를 잃어버리곤 했습니다.

:root { interpolate-size: allow-keywords; }

...

::details-content는 드러나는 영역을 타겟팅하며, interpolate-size가 이미 작동하고 있으므로 자연스러운 높이로 애니메이션됩니다. content-visibilityallow-discrete와 결합하면, 내용이 필요할 때까지 렌더링 및 접근성 작업에서 접힌 콘텐츠를 제외할 수 있으며, 이는 성능과 스크린 리더 출력 모두에 좋습니다. 스크립트 없이도 올바른 시맨틱스를 갖춘 네이티브 애니메이션 디스클로저 (animated disclosure)를 얻을 수 있습니다. 네이티브 요소와 현대적인 CSS의 이러한 조합은 제가 이미 라이브러리를 대체한 CSS 애니메이션들에서 의존했던 방식과 동일합니다.

@property: 이전에는 보간할 수 없었던 것들을 애니메이션화하다

일반적인 커스텀 프로퍼티 (custom property)는 브라우저에게 단순한 문자열일 뿐이므로, 애니메이션되는 대신 값 사이를 즉시 전환(snap)합니다. 이를 @property로 등록하고 실제 타입을 부여하면, 브라우저가 이를 보간 (interpolate)하기 시작합니다. 이를 통해 이전에는 고정되어 있던 그라디언트 (gradients), 각도 (angles), 카운터 (counters) 등을 애니메이션화할 수 있게 됩니다.

@property --angle {
  syntax: "";
...

--angle이 타입이 지정되었기 때문에, conic-gradient 진행 상태 링(progress ring)이 끝으로 갑자기 튀지 않고 부드럽게 회전합니다. 동일한 방식으로 각도(angle)를 제어하여 회전하는 그라디언트 테두리(gradient border)를 애니메이션화하거나, --count를 등록하고 카운터(counter)를 통해 값을 보여주면서 전환(transition)시켜 숫자가 올라가는 효과를 낼 수 있습니다. 예전에는 진행 상태 링 하나만을 위해 작은 애니메이션 헬퍼 라이브러리를 가져와 사용하곤 했습니다. 이제는 단 네 줄의 CSS와 등록된 속성(registered property)만 있으면 됩니다. 스크롤 위치와 연동된 움직임의 경우, scroll-driven animations와 자연스럽게 결합됩니다.

기억해야 할 한 가지 제한 사항은 타입이 지정된 속성(typed properties)만 보간(interpolate)된다는 점입니다. 따라서 등록되지 않은 --foo는 여전히 즉시 변화합니다. 만약 애니메이션이 알 수 없는 이유로 끝 값으로 바로 점프한다면, 거의 항상 @property 등록이 누락되었기 때문입니다. 타입을 선언하고, inherits를 설정하면 전환(transition)이 다른 속성들처럼 작동하기 시작합니다.

닫힐 때 애니메이션이 작동하는 다이얼로그 (Dialog)

<dialog> 요소와 Popover API는 백드롭(backdrop) 및 포커스(focus) 처리를 포함하여 콘텐츠를 최상위 레이어(top layer)에 배치합니다. 부족했던 부분은 종료(exit) 단계였습니다. 최상위 레이어에서 제거되는 다이얼로그는 즉시 사라지곤 했는데, 이는 최상위 레이어 멤버십을 제어하는 overlay 속성이 이산적(discrete)이었기 때문입니다. 이제는 이를 전환(transition)할 수 있습니다.

dialog {  
opacity: 0;  
transform: scale(0.96);  
transition: opacity 0.3s, transform 0.3s,  
overlay 0.3s allow-discrete, display 0.3s allow-discrete;  
}  
dialog[open] {  
opacity: 1;  
transform: scale(1);  
}  
@starting-style {  
dialog[open] { opacity: 0; transform: scale(0.96); }  
}  
dialog::backdrop {  
background: rgb(0 0 0 / 0%);  
transition: background 0.3s, overlay 0.3s allow-discrete,  
display 0.3s allow-discrete;  
}  
dialog[open]::backdrop {  
background: rgb(0 0 0 / 50%);  
}  
@starting-style {  
dialog[open]::backdrop { background: rgb(0 0 0 / 0%); }  
}  

allow-discrete를 사용하여 overlay를 전환하면, 닫기 애니메이션이 완료될 때까지 다이얼로그 (dialog)가 최상위 레이어 (top layer)에 유지되므로, 갑자기 사라지는 대신 크기가 조절되며 서서히 사라집니다 (fade out). ::backdrop도 이에 맞춰 함께 서서히 나타납니다 (fade in). 팝오버 (Popover) 역시 동일한 처리를 받습니다. 이것은 제가 여전히 스크립트를 유지하고 있던 마지막 UI 요소였으며, 이제는 스타일만으로 구현됩니다.

솔직한 브라우저 지원 현황

이 다섯 가지가 모두 동일한 지원 단계에 있는 것은 아니므로, 상황에 맞춰 적절히 사용하세요. @property는 널리 지원되며 아무런 고민 없이 사용해도 안전합니다. @starting-styletransition-behavior: allow-discrete는 주요 엔진들에 출시되었으며 2026년 기준으로 신뢰할 수 있습니다. interpolate-size, calc-size(), 그리고 ::details-content는 더 최신 기능이며 Chrome이 주도하고 있고 다른 엔진들이 따라잡고 있는 단계이며, 다이얼로그를 위한 overlay 전환 역시 이와 같은 최신 범주에 속합니다.

이 기능들을 실제로 배포 가능하게 만드는 규칙은 간단합니다. 애니메이션을 기능의 핵심 (load-bearing)으로 만들지 마세요. 최신 기능들은 @supports 뒤에 배치하여, 브라우저가 이를 이해하지 못하더라도 패널은 여전히 열리고, 디스클로저 (disclosure)는 여전히 확장되며, 다이얼로그는 여전히 작동하도록 해야 합니다. 단지 부드럽게 미끄러지는 대신 툭 끊기듯 (snap) 동작할 뿐입니다. 이 기능들은 모두 이미 자체적으로 작동하는 상태 변화 (state change)를 향상시키는 것이므로, 폴백 (fallback)은 애니메이션이 없는 버전이 되며, 이는 그 자체로도 충분히 훌륭한 경험입니다. 여러분은 동작 (behaviour)이 아닌 세련미 (polish)를 추가하는 것이므로, 무엇도 망가뜨리지 않습니다.

결론

올해 제 프로젝트에서 다섯 개의 스크립트가 사라졌습니다. 높이를 측정하는 열기/닫기 도우미, 토스트 (toast)를 없애기 위한 setTimeout 패턴, 디스클로저 (disclosure) 플러그인, 프레임 단위의 프로그레스 링 (progress ring), 그리고 다이얼로그 닫기 애니메이션 심 (shim)이 그것입니다. 다섯 가지 모두 이제 CSS이며, 이 CSS는 그것이 대체한 코드보다 더 짧고 안정적입니다.

두 가지 주의 사항이 있습니다. 새로운 기능들을 @supports 뒤에 배치하여 제한하거나, 구형 브라우저가 애니메이션을 건너뛰고 콘텐츠를 그대로 보여주는 것을 수용하십시오. 이는 올바른 방식의 점진적 향상 (progressive enhancement)입니다. 그리고 항상 prefers-reduced-motion 블록을 사용하여 동작 선호도 (motion preferences)를 존중하십시오. 이것들이 일상적인 UI에서 여전히 JavaScript를 요구했던 마지막 공통 사례들이었습니다. 더 많은 사례들이 사라져가는 모습은 the Lab에서 확인해 보세요.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0