
위처럼 새로운 기능이 추가됨을 알리는 띠배너 애니메이션을 구현한 경험을 정리해보고자 합니다. 텍스트 길이에 따라서 자동으로 너비가 지정되게끔 하고 싶었는데 문제는 css의 keyframes로 width를 40px에서 auto로 변경하자 애니메이션이 제대로 되지 않고 뚝뚝 끊기는 듯한 현상이 발생했습니다.
keyframes는 정확히 구체적인 수치값을 지정해야 정상 동작합니다. 만약에 auto가 아니라 300px정도로 하드코딩하면 애니메이션이 잘 작동하지만 매번 텍스트가 변경될때마다 그렇게 할 수가없기 때문에 다른 방법을 찾아보았습니다. 마침 CSS-Tricks에서 좋은 방법이 소개되어 있었습니다.
첫번째 방법
css-tricks에서 소개하는 첫번째 방법은 다음과 같습니다.
- width를 auto로 지정할 수 없다.
- 따라서, width대신 max-width에 애니메이션을 주면 된다. 다만, max-width는 컨텐츠의 크기를 충분히 넘어야 한다.
이 방법의 원리는 200px 너비의 띠가 있을때 띠의 max-width를 40px에서 대략 1000px로 애니메이션 하면 200px의 너비에 도착했을때 width가 더이상 늘어나지 않는 방법을 이용한것입니다. max-width는 말그대로 최대 너비일뿐이라서 띠가 가져야할 원래 너비가 되면 더이상 늘어나지 않는 것이죠.
이 방법에는 두가지 단점이 있습니다.
1. max-width값을 하드코딩 해야 한다.
이번 구현의 경우엔 max-width값을 어느정도 예측할 수 있어서 그냥 100vw로 넣어도 잘 작동합니다. 하지만 그렇지 않은 경우도 있겠죠? 그럴땐 막상 정확히 몇을 넣어야 할지 애매해집니다.
2. 애니메이션의 정확한 컨트롤이 힘들다.
예를들어 애니메이션을 max-width를 0에서 920px로 늘렸다고 해봅시다. 컨텐츠의 크기는 220px이고, 애니메이션은 1.5s동안 실행되며 timing-functino값은 ease-out입니다.
이런상황에서, 정확히 몇초에 띠가 전부다 펼쳐질지 판단하실 수 있습니까? 굉장히 어렵습니다. 또한 timing-function은 2차원 곡선 형태로 되어있는데, 0부터 920px까지의 너비를 기준으로 적용됩니다. 220px쯤에서는 원래 의도했던 timing-function의 효과를 누리기 힘들겠죠.


위에 차이가 보이시나요? 브라우저 너비가 늘어나면 늘어날수록 띠 배너는 빠르게 펼쳐집니다. 왜냐면, width가 40px부터 100vw까지 4s동안 증가하게 만들었는데 브라우저의 뷰포트 width가 늘어나게 되면서 4s동안 100vw에 도달하기 위해 더 빠르게 펼쳐져야 하기 때문이죠. 이 페이지는 모바일 전용 페이지이기 때문에 그냥 적당히 500px정도로 하드 코딩해도 이슈는 생기지 않을것입니다. 어차피 텍스트는 모바일 기준으로 길이가 정해져서 디자인될것이기 때문이죠. 하지만 하드코딩은 웬만하면 피해주는게 좋습니다.
두번째 방법 : CSS transform: scaleX() 활용하기
CSS의 transform은 DOM을 마치 이미지처럼 다루게됩니다.(GPU를 사용합니다.) 그렇기 때문에 브라우저의 리플로우를 유도하지 않는다는 장점이 있습니다. 또한 리플로우가 일어나지 않기 때문에 주변 요소를 건드리지도 않고 딱 해당 요소에만 원하는 효과를 줄 수 있습니다.
css의 transform: scaleX를 0에서 1로 변경하게 되면 컨텐츠의 크기만큼만 늘어나는 띠배너를 만들 수 있습니다. 하지만, 제가 구현해야 하는 케이스는 초기 크기가 40px로 정해져있고 정적인 상태일때의 텍스트와 애니메이션이 동작중일때의 텍스트가 다릅니다.

위처럼 애니메이션이 시작할때와 종료되기 직전에서 약간의 시각적인 어색함이 생겨서 결국 이 방법도 사용하지 못하게 되었습니다. (자세히 보면 애니메이션이 진행 도중 border-radius가 풀리면서 사각형으로 변합니다.)
세번째 방법 : javascript 활용하기
자바스크립트를 사용한다고 하면 우선 컨텐츠의 너비를 먼저 계산하고나서 트랜지션의 끝의 width를 계산한 너비로 설정해주는 방법이 있을것입니다. 이 방법을 알아보기 위해서는 다음 개념을 알아야 합니다.
requestAnimationFrame
모니터는 1초에 60개의 프레임을 찍어냅니다. 프레임은 그냥 한장의 사진이라고 생각하시면 되는데 1초를 60으로 나눠보면 대략 16.6ms의 시간입니다. 즉, 16.6ms내에 1개의 프레임을 찍어내야 자연스러운 애니메이션 화면을 볼 수 있다는 말입니다.
requestAnimationFrame이라는 함수는 다음 프레임 생성이 시작되자마자 인자로 넘겨진 콜백함수를 실행시켜주는 함수입니다. 이것을 사용하게 되면 개발자가 애니메이션을 브라우저의 프레임에 맞춰 디테일하게 컨트롤 할 수 있죠.
collapseSection
css-tricks에서 소개하고 있는 방법을 파헤쳐봅시다. 여기서 소개한 예제는 수직으로 열고 닫을 수 있는 토글 메뉴를 다루고 있지만 원리는 같습니다. 위 코드는 토글 메뉴를 수직으로 닫을때 실행되는 collapseSection함수입니다.

우선 해당 요소의 scrollHeight를 구해서 실제 컨텐츠의 높이를 구합니다. 또한 그 요소에 적용된 transition값을 변수에 저장해두고 요소의 transition은 초기화 시켜줍니다.

여기서 아까 배웠던 requestAnimationFrame함수가 등장합니다. 아까 계산해둔 scrollHeight(sectionHeight)와 변수에 저장해뒀던 엘리먼트의 transition값(elementTransition)을 다음 프레임이 시작되자마자 설정될 수 있게끔 해줍니다. 그다음, requestAnimationFrame안에 또 requestAnimationFrame가 호출되는 모습이 보이는데, 그 안에서는 엘리먼트의 높이를 0으로 초기화 해주고 있습니다.
정리해보면, 엘리먼트의 scrollHeight(컨텐츠높이)와 transition값을 저장해두고 다음 프레임에 방금 구해둔 두 값을 엘리먼트에 설정하고 그다음다음프레임에 height를 0으로 설정함으로써 다음 프레임엔 구체적으로 지정된 height값, 다음 다음 프레임엔 0으로 설정된 높이가 자연스럽게 애니메이션 될 수 있게 구성되었습니다.
만약에 requestAnimationFrame을 사용하지 않고 scrollHeight를 구한다음 엘리먼트에 설정하고 transition을 설정한뒤 다시 0으로 세팅해서 같은 프레임내에서 이 모든일이 일어나게 되면 애니메이션이 작동하지 않을것입니다.
expandSection()
먼저 마찬가지로 엘리먼트의 scrollHeight를 구해서 설정해줍니다. 이전에 토글 메뉴는 닫혀있을것이기 때문에 해당 자바스크립트 실행 시점에서의 프레임에서 바로 height를 설정해줘도 이전 프레임과 높이가 다르기 때문에 애니메이션이 실행될것입니다.
그리고나서, transitionend 이벤트 핸들러를 달아주고 그 안에서 엘리먼트의 인라인height를 지워줍니다. 그럼 그 엘리먼트의 css로 설정된 height로 높이가 다시 잡히겠죠.(인라인height는 css height보다 우선순위가 높습니다.) 마찬가지로, 이벤트 핸들러는 딱 한번만 호출되어야 하므로 removeEventListener도 호출해줍니다.
이것도 정리해보자면, 펼칠 때는 height를 설정해주자마자 transition이 일어나고, transition이 끝나는 시점에(엘리먼트가 컨텐츠 만큼 펼쳐졌을때) 그 엘리먼트의 원래 높이로 재설정해주기 위해서 인라인으로 적용됬던 height를 제거해줍니다.
단점
보시다시피 고작 부드럽게 메뉴를 펼치는 기능을 적용하기 위해서 무려 25줄의 코드를 작성해야했습니다. 또한 엘리먼트에 패딩이나 border-box가 적용 되어 있다면 코드는 수정되어야 합니다. 정 다른 방법이 없고 완벽하게 동작해야 한다면 자바스크립트를 사용하는것이 좋겠지만, 이것도 정해는 아니라는 생각이 듭니다..
requestAnimationFrame의 브라우저 지원

보시다시피 96.3%의 케이스를 커버하고 있고 IE를 제외한 대부분의 브라우저에서 사용이 가능합니다.
+) 보너스
css의 flexbox를 사용하면 어느정도 비슷한 효과를 얻을 수 있지만 컨텐츠 기반으로는 불가능합니다.
css의 flex속성을 0에서 1로 설정하고 transition: flex 0.3s ease-out과 같이 설정해주면 자연스럽게 닫히게끔 할 수 있지만 컨테이너의 명시적 높이를 설정해야 한다는 점에서 위 내용과는 약간 다릅니다. 그냥 알아만 둡시다.
출처