CSS transition, resize할 때도 실행된다면

의도하지 않은 애니메이션, transition 제어로 잡기

2026년 3월 9일

🎨 리사이징 시에도 실행되는 CSS transition

우리 팀에서는 drawer를 통해 많은 사용자 행동을 처리한다. 최근에는 특정 폼에서 step1은 lg 사이즈 drawer로, step2는 화면에 꽉 차는 full 사이즈로 전환하는 요구사항이 생겼다.

이 상황에서 step이 바뀔 때 drawer 너비가 뚝 끊기며 바뀌는 게 신경 쓰이기 시작했다. 자연스럽게 늘어나는 애니메이션을 주고 싶었고, 단순하게 아래 코드를 추가했다.

step 변경 시 drawer 너비가 부드럽게 넓어지는 건 잘 됐다.

그런데 문제가 생겼다. 창 크기를 줄일 때도 transition이 계속 실행됐다.

사용자가 창 크기를 의도적으로 조절하는 경우는 많지 않지만, 직접 써보니 리사이징할 때마다 drawer가 출렁이는 게 꽤 어지러웠다. 사용자 경험에 좋지 않다고 판단해 해결하기로 했다.


왜 resize에서도 transition이 트리거되는가?

CSS transition은 *computed value가 변경되면 무조건 실행된다. step 변경이든 창 리사이징이든, computed width가 바뀌면 브라우저는 동일하게 transition을 트리거한다.

computed value란?

CSS는 값을 아래 순서로 처리한다.

핵심은 이것이다. CSS transition은 값이 어떤 이유로 바뀌었는지 구분하지 않는다. 값이 바뀌었다는 사실 자체에만 반응한다. resize든 step 변경이든 computed width가 달라지면 똑같이 애니메이션이 실행되는 이유다.


검토한 3가지 대안

1. View Transitions API + flushSync

CSS transition을 제거하고, step이 변경될 때만 명시적으로 애니메이션을 트리거하는 방식이다.

View Transitions API 동작 순서

  1. 현재 화면을 캡처 → ::view-transition-old에 저장
  2. 렌더링 일시 정지 (화면 freeze)
  3. 콜백 실행 → DOM 업데이트
  4. 새 화면 캡처 → ::view-transition-new에 저장
  5. pseudo-element 트리 생성 후 크로스페이드 애니메이션 실행
  6. 애니메이션 완료 후 pseudo-element 제거

flushSync가 필요한 이유

React 18부터는 setTimeout, Promise, 네이티브 이벤트 핸들러 등 모든 상황에서 자동 배칭이 적용된다. setState 호출 직후에 DOM이 업데이트돼 있다고 보장할 수 없다.

startViewTransition의 4단계(새 화면 캡처) 이전에 step이 완전히 반영돼야 하므로, flushSync로 동기 렌더링을 강제해야 한다.

일단 나의 경우는 이 방법을 선택하지 않았다. React 공식 문서도 flushSync를 "최후의 수단"으로 명시할 만큼, 렌더 사이클이 꼬이며 발생할 수 있는 문제가 있기때문이다. 브라우저 호환성 이슈까지 감수하면서 적용하기엔 오버엔지니어링이라는 판단이었다.


2. React <ViewTransition> 컴포넌트

React 19에는 flushSync 없이 View Transitions API를 사용할 수 있도록 <ViewTransition> 컴포넌트가 추가됐다.

flushSync 방식은 제어 방향이 바깥 → React다.

React 18의 Concurrent Mode는 렌더를 잘게 쪼개 비동기로 처리하도록 설계되어 있다. flushSync는 그 흐름을 강제로 끊기 때문에, 진행 중이던 다른 업데이트와 충돌하거나 Suspense boundary 안에서 에러가 발생할 수 있다.

반면, <ViewTransition>은 방향이 반대다. React → 브라우저 API다.

React가 커밋 타이밍을 직접 쥐고 있기 때문에 flushSync가 필요 없다. 렌더 사이클을 강제로 끊지 않으니 Concurrent Mode와도 충돌하지 않는다.

하지만 아직 실험적인 컴포넌트이고, 프로젝트의 React 버전도 수정해야하는 부담도 있어, 프로덕션에 바로 적용하기는 위험하다고 판단했다.


3. CSS @media 쿼리로 transition 비활성화

resize는 특정 width 구간에서만 발생하는 게 아니라 모든 구간에서 일어난다. 그래서 이 방법으로는 문제를 해결할 수 없다고 판단했다.


채택한 방법: data-resizing attribute + debounce

발상을 뒤집었다. resize가 발생하는 동안에만 transition을 끄는 방식이다. JS가 resize 중인지 판단하고, 그 순간만 CSS transition을 비활성화한다.

흐름

구현 코드

data-resizing 적용 후 결과
resize 중에는 data-resizing이 세팅되어 transition이 비활성화

정리

방법장점단점
View Transitions API + flushSync명시적 트리거 가능브라우저 호환성, flushSync 부작용
React <ViewTransition>렌더 사이클 안전React 19 실험적 기능
CSS @media 쿼리단순resize 구간 제한 불가
data-resizing + debounce브라우저 호환성 ✅, 구현 단순 ✅resize 감지에 약간의 딜레이

나는 data-resizing + debounce 방식을 선택했다. 추가 의존성 없이 동작하고, 기존 코드 변경도 최소화된다. 100ms debounce로 인해 resize가 끝난 직후 transition이 한 번 실행될 수 있지만, 실제로 사용해보면 거의 인지되지 않는 수준이다.

물론 더 정교한 UX가 필요한 상황이라면 View Transitions API를 고려해볼 수 있을 것 같다. React <ViewTransition>이 정식 릴리즈되면 다시 검토해볼 예정이다 !