애니메이션 타이밍을 어떻게 맞출 수 있을까요?

황준·2025년 2월 28일
5

좋아하는 글

목록 보기
4/8
post-thumbnail

✅ 개요

최근 프로젝트에서 슬라이드 애니메이션 구현 중 문제가 발생했습니다.
컴포넌트가 unmount될 때 애니메이션이 실행되지 않고 화면에서 즉시 사라지는 현상이 있었습니다.

이 글에서는 해당 문제의 원인과 setTimeout을 사용하지 않고 이벤트 핸들링을 통해 어떻게 해결했는지 공유해보려고 합니다.


🔍 문제 상황

Drawer 컴포넌트를 구현하는 과정에서 문제에 직면했습니다.
이 컴포넌트는 mount 시 왼쪽에서 오른쪽으로 슬라이드 인(slide in) 애니메이션을 통해 나타나는 모달 형태였습니다.

mount는 컴포넌트가 DOM에 삽입되어 화면에 표시되는 과정을 의미합니다.
unmount는 컴포넌트가 DOM에서 제거되는 과정을 의미합니다.

문제는 drawer를 unmount하게 만드는 상태 변경애니메이션이 실행되는 타이밍이 동일했다는 점입니다.

이로 인해 애니메이션이 실행되기도 전에 컴포넌트가 unmount 되면서 의도한 슬라이드 아웃(slide out) 효과를 구현할 수 없었습니다.

<div 
 onClick={() => {
   onChangeVisibility(false);
 }}
 // translate-x-0: 요소를 원래 위치에 배치 (X축 이동 없음)
 // translate-x-full: 요소를 X축으로 100% 너비만큼 이동시킴
 className={`${isVisible ? 'translate-x-0' : 'translate-x-full'}`}>
   {children}
</div>

위 코드에서 isVisible이 false로 변경되고 CSS transition 애니메이션이 실행되기 전에 컴포넌트가 언마운트되었습니다.

결과적으로 애니메이션이 완료되기 전에 컴포넌트가 "뿅" 하고 사라지는 문제가 발생했습니다.

CSS transition은 속성 값이 변경될 때 시작점에서 끝점까지 일정 시간에 걸쳐 부드럽게 변화하도록 해주는 CSS 기능입니다.


🛠️ 문제 해결 과정

이전에도 비슷한 문제를 경험한 적이 있었습니다.
당시에는 setTimeout을 사용하여 애니메이션 실행 시간에 맞추어 타이밍을 조절했습니다.

// setTimeout을 사용한 방식
const handleClose = () => {
  setIsVisible(false);
  setTimeout(() => {
    callback();
  }, 300); // 300ms는 CSS 트랜지션 시간
}

하지만 이 방법에는 몇 가지 문제점이 있었습니다:

  • 자바스크립트의 이벤트 루프와 매크로태스크 큐 처리 방식으로 인해 타이밍이 정확하지 않을 수 있습니다.
  • 디바이스 성능에 따라 타이밍이 어긋나는 경우가 발생했습니다.
  • 애니메이션 지속 시간을 코드에 하드코딩해야 해서 CSS와 자바스크립트 간의 동기화가 어려웠습니다.

이런 문제들로 인해 여러 방법을 시도했지만, 완벽한 해결책을 찾지 못했습니다.
문제는 지속적인 고민을 가져왔습니다.

💡 해결 방법을 찾게 된 계기

그러던 중 브라우저 성능 탭을 분석하면서 중요한 발견을 하게 되었습니다.
바로 animationstart라는 이벤트의 존재였습니다.

이를 통해 CSS 애니메이션에도 이벤트 리스너를 연결할 수 있으며,
애니메이션 타이밍 문제를 해결할 수 있지 않을까 하는 아이디어가 떠올랐습니다.

브라우저가 애니메이션의 시작 시점을 정확히 알려주므로, 애니메이션의 종료 시점도 알려주는 이벤트가 있을 것이라 생각했습니다.

이렇게 되면 더 이상 타이밍을 추측하거나 하드코딩할 필요가 없어질 것이라는 희망이 생겼습니다.


❗ CSS 애니메이션과 이벤트 기반 접근법

이 발견을 계기로 CSS 애니메이션과 트랜지션에 관련된 이벤트들을 더 깊이 조사하게 되었습니다.

알고 보니 브라우저는 애니메이션과 트랜지션의 생명주기 전반에 걸쳐 다양한 이벤트를 제공하고 있었습니다.

이러한 이벤트들은 정확한 시점을 포착하여 자바스크립트 코드를 실행할 수 있게 해줍니다.
이는 setTimeout이나 하드코딩된 타이밍에 의존하는 것보다 훨씬 더 신뢰할 수 있는 방법입니다.
왜냐하면 브라우저 자체가 애니메이션의 실제 진행 상황을 추적하고 있기 때문입니다.

CSS 애니메이션에서 제공하는 주요 이벤트들:

  1. animationstart: 애니메이션이 시작되는 순간에 발생합니다.

  2. animationend: 애니메이션이 완전히 끝났을 때 발생합니다.

  3. animationiteration: 애니메이션이 한 사이클을 완료하고 다음 반복을 시작할 때 발생합니다. 단, 애니메이션이 한 번만 실행되는 경우에는 이 이벤트가 발생하지 않습니다.

  4. animationcancel: 애니메이션이 중간에 취소될 때 발생합니다.


CSS 트랜지션에서 제공하는 주요 이벤트들:

  1. transitionstart: 트랜지션이 실제로 시작될 때 발생합니다.

  2. transitionrun: 트랜지션이 적용되어 실행 준비가 된 시점에 발생합니다.

  3. transitionend: 트랜지션이 완료되었을 때 발생합니다.

  4. transitioncancel: 트랜지션이 완료되기 전에 중단될 때 발생합니다.


이벤트 객체 속성들
이러한 이벤트들은 다양한 정보를 담고 있는 이벤트 객체와 함께 전달됩니다:

  • event.animationName: 진행 중인 애니메이션의 이름
  • event.elapsedTime: 애니메이션이나 트랜지션이 진행된 시간(초 단위)
  • event.propertyName: 트랜지션이 적용된 CSS 속성 이름
  • event.pseudoElement: 애니메이션이나 트랜지션이 적용된 의사 요소(있는 경우)

실제 사용 예시

// transitionend 이벤트 사용 예
const element = document.querySelector('.animated-element');

element.addEventListener('transitionend', (event) => {
  console.log('트랜지션 완료!');
  console.log('변화한 속성:', event.propertyName);
  console.log('트랜지션 지속 시간:', event.elapsedTime);
  
  // 이 시점에서 컴포넌트 언마운트 등의 작업 수행 가능
});

// 애니메이션 이벤트 예시
element.addEventListener('animationend', (event) => {
  console.log('애니메이션 완료!');
  console.log('애니메이션 이름:', event.animationName);
  console.log('애니메이션 지속 시간:', event.elapsedTime);
});

이러한 이벤트 기반 접근법은 애니메이션 타이밍과 관련된 많은 문제를 해결해 줍니다.

특히 애니메이션 속도가 디바이스 성능이나 브라우저 환경에 따라 달라질 수 있는 상황에서도 정확한 시점을 포착할 수 있어 매우 유용합니다.

🎯 최종 해결책

CSS 애니메이션 이벤트를 활용하여 문제를 해결하기로 했습니다.
제가 구현한 방법은 다음과 같습니다:

  1. 버튼 클릭 시 먼저 CSS 변경(애니메이션 트리거)을 수행합니다.
  2. 트랜지션 애니메이션이 완료된 후, 컴포넌트를 unmount합니다.

이 접근법을 사용하면 애니메이션이 완전히 끝난 후에 컴포넌트가 언마운트되므로, 사용자는 자연스러운 트랜지션 효과를 볼 수 있습니다.

useEffect(() => {
  const drawerElement = drawerRef.current;
  if (drawerElement) {
    const handleTransitionEnd = (e: TransitionEvent) => {
      // transform 속성의 트랜지션이 끝나고, 컴포넌트 unmount함
      if (e.propertyName === 'transform' && !isVisible) {
        onTransitionCompleteUnmount();
      }
    };
    
    drawerElement.addEventListener('transitionend', handleTransitionEnd);
    return () => {
      drawerElement.removeEventListener('transitionend', handleTransitionEnd);
    };
  }
}, [isVisible]);

// 클릭 이벤트에서는 URL 제거 함수를 호출하지 않고 가시성 상태만 변경
<div 
  onClick={() => {
    onChangeVisibility(false);
  }}
  className={`${isVisible ? 'translate-x-0' : 'translate-x-full'}`}>
  {children}
</div>

이 방법을 통해 애니메이션이 완료 시점에 컴포넌트를 DOM에서 제거할 수 있었고, 자연스러운 슬라이드 인/아웃 효과를 구현했습니다.

setTimeout과 달리, 애니메이션의 완료 이벤트를 활용함으로써 디바이스 성능이나 애니메이션 지속 시간과 상관없이 정확한 타이밍을 보장할 수 있었습니다.


🌟 마치며

이러한 CSS 애니메이션 이벤트는 단순하지만 강력한 API입니다.

이를 알았을 때와 몰랐을 때 애니메이션 구현 방식에 큰 차이가 생겼습니다.
특히 React에서 컴포넌트 생명주기와 애니메이션 주기를 조화롭게 다루는 데 매우 유용합니다.

우리가 알지 못했던 Web API들이 많이 존재하며, 이에 대한 관심과 학습이 개발자로서 얼마나 중요한지 다시 한번 깨닫게 되었습니다.

이 글이 비슷한 애니메이션 타이밍 문제로 고민하고 계신 다른 개발자분들께 작은 도움이 되었으면 합니다.

읽어주셔서 감사합니다!

profile
잘하고 싶은 사람

0개의 댓글