React에서 unmount 애니메이션 적용하기

우혁·2025년 8월 27일
171

FE

목록 보기
11/11
post-thumbnail

React에서 unmount 애니메이션

React에서 컴포넌트가 unmount될 때 애니메이션이 제대로 동작하지 않는 이유는 상태나 조건이 변경되면 컴포넌트를 즉시 DOM에서 제거하기 때문입니다.

즉, 컴포넌트가 사라지기 전에 CSS 애니메이션(ex. fade-out)을 실행할 시간이 없어서 애니메이션이 보이지 않습니다.

따라서 unmount 시점의 애니메이션을 적용하려면 애니메이션이 끝나기까지 기다린 후 컴포넌트를 unmount 하도록 지연시켜야 합니다.

애니메이션이 끝난 후에 컴포넌트를 unmount하는 방법은 크게 두 가지로 나눌 수 있습니다.

  1. 시간 지연 방식
  2. 이벤트 감지 방식

아래에서는 이 두 가지 방법을 통해 unmount 애니메이션을 구현하는 방법을 소개하고자 합니다.


시간 지연 방식

1. setTimeout 활용

setTimeout 으로 애니메이션이 동작하는 시간만큼 unmount를 지연시키는 방법으로 가장 구현이 간단합니다.

  • 구현 코드
function DelayUnmount({ visible, delay, children }) {
  const [mounted, setMounted] = useState(visible);
  
  useEffect(() => {
    if (visible) {
      setMounted(true);
      return;
    }

    const timerId = setTimeout(() => {
      setMounted(false);
    }, delay);

    return () => {
      clearTimeout(timerId);
    };
  }, [visible, delay]);

  if (!mounted) {
    return null;
  }

  return children;
}

2. requestAnimationFrame

requestAnimationFrame은 브라우저의 프레임 주기에 맞춰 실행되어 setTimeout 보다 타이머의 정확도가 뛰어납니다.

💬 requestAnimationFrame 의 특징과 setTimeout 과의 차이점은 이 글에서 자세히 다루고 있어 궁금하시다면 참고 부탁드립니다!

  • 구현 코드
function DelayUnmount({ visible, delay, children }) {
  const [mounted, setMounted] = useState(visible);
  
  useEffect(() => {
    if (visible) {
      setMounted(true);
      return;
    }

    let animationFrameId;

    const frameCallback = (currentTime, startTime) => {
      if (visible) {
        setMounted(true);
        return;
      }

      const elapsedTime = currentTime - startTime;
      if (elapsedTime >= delay) {
        setMounted(false);
        return;
      }

      animationFrameId = requestAnimationFrame((time) => frameCallback(time, startTime));
    };

    animationFrameId = requestAnimationFrame((time) => frameCallback(time, time));

    return () => {
      if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };
  }, [visible, delay]);
  
  if (!mounted) {
    return null;
  }

  return children;
}

하지만 시간 지연 방식들의 문제점은 애니메이션이 동작하는 시간을 알아야 한다는 문제점이 있습니다.

애니메이션 동작 시간(duration)이 delay prop 과 일치하지 않으면 unmount 타이밍이 어긋나고, 애니메이션이 작성되어 있는 코드(css)와 delay prop을 넘겨주는 곳의 위치가 멀어 응집도 측면에서도 좋지 않다고 생각합니다.


이벤트 감지 방식

이벤트 감지 방식은 애니메이션이 동작하는 시간이 아닌 애니메이션이 끝났음을 감지하는 API들을 활용하는 방식입니다.

1. animationend 이벤트 활용

onAnimationEnd 이벤트 리스너를 부착하여 CSS 애니메이션이 완전히 끝났을 때 이벤트가 발생하도록 처리합니다.

💡 animationend 이벤트란?
CSS 애니메이션이 종료되었을 때 발생하는 이벤트입니다. 이 이벤트를 사용하면 애니메이션이 끝난 후 다음 동작(ex. 다른 애니메이션 시작, 스타일 변경 등)을 자동으로 실행할 수 있습니다.

  • 구현 코드
function UnmountAfterAnimation({ visible, children }) {
  const [mounted, setMounted] = useState(visible);

  useEffect(() => {
    if (visible) {
      setMounted(true);
      return;
    }
  }, [visible]);

  const unmount = () => {
    if (visible) {
      return;
    }

    setMounted(false);
  };

  if (!mounted) {
    return null;
  }

  return (
    <div onAnimationEnd={unmount}>
      {children}
    </div>
  );
}

하지만 자식 요소(children)에 애니메이션이 없는 경우 animationend 이벤트가 동작하지 않아 unmount 되지 않는 문제가 발생할 수 있습니다.

또한 자식 요소에 여러 개의 애니메이션이 있다면, 가장 먼저 종료된 애니메이션에 이벤트가 트리거 되어 unmount 할 수 있어 나머지 애니메이션이 끝나지 않는 상태에서도 unmount가 진행되는 문제가 있습니다.

2. getAnimations, finished Promise 활용

이 방식은 getAnimations 메서드를 활용해 자식 요소에 있는 모든 애니메이션들을 가져오고, finished 속성(Promise)을 통해 애니메이션이 끝났을 때 후속 처리로 unmount를 진행하는 방식입니다.

💡 getAnimations란?
DOM 요소에서 현재 실행 중인 모든 애니메이션 객체들을 배열로 반환하는 메서드입니다.
이를 통해 특정 요소의 애니메이션 상태를 확인하고 프로그래밍 방식으로 제어할 수 있습니다.

💡 finished란?
Web Animation API에서 제공하는 Promise 객체로, 애니메이션이 완료되면 reslove되고 취소되면 reject됩니다. 이를 통해 await이나 .then()을 사용하여 애니메이션 완료를 비동기적으로 처리할 수 있습니다.

  • 구현 코드
function UnmountAfterAnimation({ visible, children }) {
  const [mounted, setMounted] = useState(visible);

  useEffect(() => {
    if (visible) {
      setMounted(true);
    }
  }, [visible]);

  const refCallback = (element: HTMLElement | null) => {
    if (!element || visible) {
      return;
    }

    const animations = element.getAnimations({ subtree: true });
    Promise.all(animations.map((animation) => animation.finished))
      .then(() => {
        setMounted(false);
      })
      .catch(() => {
        // 애니메이션 중단 처리(abort error)
        setMounted(false);
      });
  };

  if (!mounted) {
    return null;
  }

  return (
    <div ref={refCallback}>
      {children}
    </div>
  );
}

콜백 ref를 활용해서 최상위 컨테이너 요소에 접근하고 getAnimations({ subtree: true }) 메서드를 통해 자식 요소의 모든 애니메이션을 가져옵니다.

🚨 getAnimations 메서드를 사용할 때 { subtree: true } 옵션을 사용하지 않으면 자식 요소의 애니메이션을 가져오지 못합니다.

animation 객체의 finished 속성을 통해 애니메이션이 끝나는 시점을 알 수 있는 Promise를 반환하고 Promise.all를 활용하여 모든 애니메이션이 끝난 후에 unmount 시키는 방식입니다.


정리하기

시간 지연 방식은 애니메이션의 동작 시간을 외부에서 직접 지정하여(unmount 지연 시간 = 애니메이션 duration) 계산적으로 unmount 시점을 제어하는 방식입니다. 구현이 단순하고 직관적이지만, CSS에 선언된 애니메이션 시간과 코드 상의 delay 값이 불일치하면 어긋날 수 있다는 특징이 있습니다.

이벤트 감지 방식은 animationend, getAnimations, finished 같은 Web API를 활용하여 실제 애니메이션이 끝난 시점을 감지해서 정확하게 unmount 시킨다는 특징이 있습니다.

unmount 시점의 애니메이션 적용 방식들을 학습하면서 애니메이션 관련 Web API가 많다는 것을 알게 되었습니다. 추후에는 motion 라이브러리 처럼 애니메이션을 쉽게 적용할 수 있는 훅, 컴포넌트를 만들어 보고 싶다는 생각이 들었던 것 같습니다🙃

혹시 글에 작성되어 있는 방식 이외의 방식으로 unmount 애니메이션을 쉽게 적용할 수 있다면 공유해 주시면 감사하겠습니다!🙇‍♂️


🫨 참고 자료

getAnimations - MDN
Animation finished - MDN
KeyframeEffect - MDN
Animation - MDN
AnimatePresence - Motion
React Animate Presence 개념부터 구현까지
setTimeout없이 애니메이션이 끝날 때 컴포넌트 언마운트 시키기 (feat. FSM, animation event)
Framer Motion의 AnimatePresence 동작 원리 분석하기!

profile
🏁

15개의 댓글

comment-user-thumbnail
2025년 8월 28일

저도 이번에 애니메이션을 다루면서 시간 지연 방식으로 코드를 작성했었는데요!
이때 불편한 점이 한두가지가 아니었습니다,, 근데 우혁님이 말씀하신 이벤트 감지 방식을 사용하면 많이 편리하겠군요!
좋은 글 잘 읽고 갑니다!!~~

1개의 답글
comment-user-thumbnail
2025년 8월 30일

react component 에서, 마운트 시 애니메이션 구현은 쉽게 했는데 언마운트 시에는 딜레이를 어떻게 줘야하고 끝나는 타이밍을 어떻게 잡지? 하면서 고민을 옛날에 많이 했었죠.

Vue 에는 Transition 컴포넌트 같은 기능을 줘서 편리하게 애니메이션 구현(진입, 진출)이 간단한데..

그리고 if 조건으로 생성할 때 애니메이션 효과를 어떻게 주지 하는것도 고민을 많이했었죠.. 키프레임을 많이 사용했는데 지금은 starting-style과 allow-discreate를 활용할 수 있어서 좋네요..!

글 잘 보고 갑니다..!

1개의 답글
comment-user-thumbnail
2025년 9월 2일

이 글을 읽고 하나 궁금한게 있습니다!

저도 리액트에서 {condition ? <Component /> : null} 이런식으로 조건 렌더링을 쓰면
컴포넌트가 DOM에 추가됬다가 사라져서 애니메이션 적용이 안되가지고 고민을 했었는데요!

<div class="box">box</div>
.box {
  opacity: 0;
  visibility: hidden;
  transition: 0.3s;
}
.box.active {
  opacity: 1;
  visibility: visible;
}

애니메이션을 주기위해 위처럼 조건부 렌더링을 사용하지 않고, css로 감췄다가
보여줄 때 active 클래스를 넣어서 opacity로 보여주는 식으로 사용했었는데, 이 방식은 잘못된 방식일까요?

1개의 답글
comment-user-thumbnail
2025년 9월 2일

이벤트 감지 쫀득쫀득 좋은데요~

1개의 답글

저도 요렇게 구현해봤습니다 ㅎㅎ
https://ssgoi.dev/ko/blog/how-to-make-transition

1개의 답글
comment-user-thumbnail
2025년 9월 3일

Just tried reading about unmount 애니메이션 in React and wow, it’s kinda tricky to get it smooth without messing up the rest of the component. I’m gonna experiment with it soon. Also had this story(https://99nightsintheforest.online/99-nights-in-the-forest-story) open on the side while scrolling, so my brain’s all over the place lol.

답글 달기
comment-user-thumbnail
2025년 9월 5일

Great post on React unmount animations! It reminds me of how smooth animations can enhance user experience, similar to using scripts for roblox for better game interactions. Thanks for sharing!

1개의 답글