← Back to list

웹뷰 속 무한 CSS 애니메이션의 숨은 비용

안녕하세요. 크리에이트립의 서종학입니다. 오늘은 웹뷰에서 관찰된 CSS 애니메이션의 높은 CPU 현상을 기준으로, 해당 이슈의 원인과 해결책에 대해 이야기를 해보려고 해요.

creatrip-jonghak in creatrip · 2026-06-15 07:58 · 0 claps · 27.3 min read
#webview #css-animation
Open on Medium ↗
Wiki topics: 🌐 · Web Development 🎬 · Film & Television

웹뷰 속 무한 CSS 애니메이션의 숨은 비용

안녕하세요. 크리에이트립의 서종학입니다. 오늘은 웹뷰에서 관찰된 CSS 애니메이션의 높은 CPU 현상을 기준으로, 해당 이슈의 원인과 해결책에 대해 이야기를 해보려고 해요.

웹 프론트엔드 개발을 조금 해봤다면 이런 이야기를 한 번쯤 들어봤을 겁니다.

“애니메이션은 top, left보다 transform을 써야 한다.” “transformopacity는 브라우저가 잘 최적화해준다.”

대체로 맞는 말이에요. 실제로 웹 성능 가이드에서도 위치나 크기를 계속 바꾸는 애니메이션보다 transform, opacity를 사용하는 방식을 권장해요. 브라우저가 레이아웃을 다시 계산하거나 화면을 다시 칠하는 일을 줄이고, 가능하면 합성(compositing) 단계에서 처리할 수 있기 때문이에요.

그런데 가끔은 이 말만로 설명되지 않는 상황을 만나기도 해요.

예를 들어 React Native 앱 안의 웹뷰에서 이런 CSS가 돌고 있다고 해봅시다.

.banner {
  animation: slide 20s linear infinite;
}

@keyframes slide {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

웹 브라우저에서는 문제 없어 보이던 이 애니메이션이, Android 기기의 웹뷰안에서는 CPU 사용률을 크게 올리거나 화면을 버벅이게 만들 수 있어요.

처음에는 이상하게 느껴져요.

transform: translateX는 안전한 것 아니었나? HTML/CSS는 웹뷰 안에서 도는 건데 왜 앱 전체가 무거워질까? Chrome에서는 괜찮은데 웹뷰에서만 문제가 생길 수도 있을까?

실제로 무슨 문제가 있었나

문제는 Android React Native 앱의 웹뷰 화면에서 발견됐어요.

특정 상세 화면에서 오디오 플레이어가 보이는 위치로 스크롤하면 Android top 기준 앱 프로세스 CPU가 90~100% 근처까지 올라갔어요. 반대로 오디오 플레이어가 화면 밖으로 사라지거나 다른 탭으로 이동하면 CPU는 다시 한 자리수로 내려갔어요.

처음에는 오디오 플레이어 전체가 의심스러웠어요. 이미지 때문일 수도 있고, progress bar 때문일 수도 있고, 터치 영역이나 RN bridge 때문일 수도 있었어요. 하지만 측정값은 꽤 일관적이었어요.

오디오 플레이어 visible

  • 앱 CPU: 약 96~102%
  • 웹뷰 sandbox CPU: 약 21~23%
  • gfxinfo frame: 약 1367 frames (6s 측정)

오디오 플레이어 offscreen 또는 다른 탭

  • 앱 CPU: 약 3.7~6%
  • 웹뷰 sandbox CPU: 0% 또는 낮은 한 자리
  • gfxinfo frame: 0 frames (4s 측정)

*gfxinfo는 내가 만든 안드로이드 앱의 '화면이 얼마나 부드럽게 움직이는지' 검사하는 돋보기라고 생각하시면 됩니다. 안드로이드 개발자 공식 문서에서 제공하는 성능 측정 도구에요.*

여기서 CPU %는 Android top의 프로세스 기준 샘플이에요. 전체 기기 CPU 점유율과 같은 의미는 아니고, 멀티코어 기기에서는 특정 프로세스가 한 코어 이상을 강하게 쓰는 상태로 해석하는 편이 안전해요.

흥미로웠던 건 화면이 심하게 끊기는 전형적인 버벅거림 문제가 아니라는 점이었어요. gfxinfo상 frame 카운터는 계속 증가했고, 로그에서는 RNCWebView / WebViewChromium.onDraw 계열 호출이 약 8ms 간격으로 반복됐어요.

다만 gfxinfo frame 수와 logcat의 onDraw 간격은 집계 기준이 다르므로, 이를 단순 fps로 환산하기보다 “고빈도 draw scheduling이 지속됐다”는 증거로 보는 편이 정확해요.

제품 코드에서 가장 강한 후보는 오디오 제목 영역의 Marquee였어요. 제목이 길면 transform: translateX(...) 기반의 무한 CSS 애니메이션이 실행되는 구조였어요.

하지만 여기서 바로 “translateX가 문제다”라고 결론 내리면 위험해요. 그래서 제품 코드와 RN을 제거한 순수 Android 웹뷰 최소 재현 하네스를 만들고, 애니메이션 종류를 바꿔가며 비교했어요.

결과는 다음과 같았어요.

낮은 비용으로 확인된 mode

  • static: CPU 낮음, frames 0 — animation 없는 DOM은 idle
  • progress-transform: CPU 낮음, frames 0 — progress bar 구조는 원인 아님
  • marquee-paused: CPU 낮음, frames 0 — Marquee DOM 자체보다 애니메이션 실행 여부가 핵심

고CPU가 재현된 mode

  • box-transform: 앱 CPU 약 95%, frames 약 1368 — 단순 박스 transform만으로 재현
  • text-transform: 앱 CPU 약 96%, frames 약 1366 — 텍스트 특수성 없이도 재현
  • marquee-text: 앱 CPU 약 87%, frames 약 1368 — 실제 Marquee 유사 구조도 재현
  • opacity-animation: 앱 CPU 약 94%, frames 약 1369transform만의 문제가 아님

최소 재현 harness의 frame 수는 측정 스크립트의 gfxinfo reset/dump 구간에 누적된 카운터예요. /초 단위의 정확한 fps로 해석하기보다는 mode 간 상대 비교와 “지속 frame 생성 여부”를 보는 지표로 사용했어요.

이 실험으로 몇 가지 후보가 반증됐어요.

  • 오디오 플레이어의 이미지나 프로그레스 바는 원인이 아니었어요.
  • 텍스트, overflow: hidden, duplicate marquee 구조가 필수 조건도 아니었어요.
  • transform: translateX만의 특수 문제도 아니었어요. opacity 애니메이션도 비슷한 고CPU를 만들었어요.
  • will-change: transform, translateZ(0) 같은 일반적인 layer promotion 힌트도 이 현상을 없애지는 못했어요.

같은 HTML은 Android Chrome에서도 돌려봤어요. 물론 Chrome에서도 애니메이션 비용은 있었어요. 예를 들어 box-transform 케이스에서 Chrome 관련 프로세스 CPU 합산은 평균 약 48.8%, 최대 약 61.8%였어요.

다만 Chrome total CPU는 여러 Chrome 프로세스 합산이고, 웹뷰 표는 host app/sandbox 프로세스 기준이라 절대값을 1:1로 비교할 수는 없어요. 여기서는 같은 HTML에서의 정성적 양상과 Android ViewRoot 관점의 gfxinfo frame 폭증 여부를 중심으로 해석했어요.

그래서 더 정확한 결론은 이쪽에 가까워요.

제품의 직접 트리거는 오디오 플레이어의 Marquee였지만, 본질은 translateX 하나가 아니라 웹뷰에서 foreground-visible 지속형 CSS 애니메이션이 host Android View의 지속 draw loop를 유발하는 현상

먼저 결론을 보면

transform: translateX() 자체가 나쁜 속성은 아니에요. 여전히 웹 애니메이션에서 좋은 선택이에요. 이번 조사에서 실제로 확인된 건 다음 내용이에요.

웹뷰 안에서 화면에 보이는 무한 CSS 애니메이션이 실행되면, 웹뷰 compositor frame이 Android host View hierarchy의 draw/onDraw 루프를 지속적으로 유발한 것으로 관찰·추정되며, RN 앱에서는 그 웹뷰가 native view로 포함되어 앱 프로세스 CPU가 크게 상승할 수 있다.

조금 쉽게풀어 말하면 아래와 같아요.

Chrome에서 웹페이지를 띄우는 것과 Android 앱 안에 웹뷰를 넣어 웹페이지를 띄우는 것은 겉보기에는 비슷하지만 내부 렌더링 경로가 완전히 같지는 않아요.

그래서 Chrome에서는 괜찮았던 CSS 애니메이션웹뷰에서는 유독 비싸게 동작하는 상황이 생길 수 있어요.

그렇다면 무한 CSS 애니메이션이 웹뷰 안에서 어떤 이유 때문에 이렇게 높은 렌더링 비용으로 나타나게 되는 것일까요?

우리가 알고 있던 브라우저 렌더링 모델

브라우저가 화면을 그릴 때는 보통 다음과 같은 단계를 거쳐요.

각 단계는 아주 단순화하면 이래요.

1. Style

CSS 규칙을 계산해요.

.card {
  color: red;
  width: 100px;
}

이런 스타일이 실제 DOM 노드에 어떻게 적용되는지 결정하는 단계입니다.

2. Layout

요소의 크기와 위치를 계산해요.

width, height, margin, display, position 같은 값이 바뀌면 브라우저는 요소들이 어디에 놓일지 다시 계산해야 할 수 있어요.

3. Paint

계산된 요소를 실제 픽셀로 칠해요. 텍스트, 배경, border, shadow, image 등을 그리는 단계예요.

4. Composite

이미 그려진 여러 레이어를 합쳐 최종 화면을 만들어요.

여기서 transform이나 opacity 애니메이션은 유리할 수 있어요. 이미 그려진 레이어를 다시 칠하지 않고 위치나 투명도만 바꿔서 합성할 수 있기 때문이에요.

그래서 웹 성능 가이드에서는 흔히 이렇게 말해요.

애니메이션에는 가능하면 transformopacity를 사용하라.

web.dev의 애니메이션 가이드에서도 transformopacity를 성능에 유리한 속성으로 설명해요.

다만 여기서 한 가지는 짚고 넘어가야 해요.

“Composite만 한다”는 말은 “공짜”라는 뜻이 아니다

transform 애니메이션이 layout이나 paint를 피할 수 있다는 말은, 그 애니메이션이 비용이 전혀 없다는 뜻은 아니에요.

특히 infinite 애니메이션은 계속 화면을 갱신해야 해요.

animation: slide 20s linear infinite;

이 코드는 애니메이션이 끝나지 않는다는 뜻이에요. 따라서 브라우저 입장에서는 매 프레임마다 다음과 비슷한 일을 반복해요.

layout이나 paint를 피하더라도 compositor frame을 계속 만들고 제출하는 비용은 남아요.

60Hz 화면에서는 최대 초당 60번, 120Hz 화면에서는 최대 초당 120번에 가까운 빈도로 이 과정이 반복될 수 있어요. 물론 브라우저와 OS는 background, offscreen, throttling, dropped frame, refresh-rate 정책에 따라 이를 줄이거나 건너뛸 수 있어요. 보통은 이 정도 비용을 감당해요. 하지만 웹뷰 환경에서는 여기에 변수가 하나 더 붙어요.

일반 Chrome에서는 왜 compositor promotion이 효과가 있을까?

여기서 잠깐 일반적인 Chrome의 동작을 먼저 짚고 가면 웹뷰 문제가 더 잘 보여요.

브라우저에서 left를 바꿔 요소를 움직이면 보통 layout이 다시 필요해요.

.box {
  left: 100px;
}

반면 transform으로 움직이면 요소의 원래 layout 위치는 그대로 두고, 그려진 결과를 어떤 위치에 합성(composition)할지만 바꿀 수 있어요.

.box {
  transform: translateX(100px);
}

운이 좋게 이 요소가 별도 compositor layer로 올라가면 브라우저는 매 프레임 DOM layout이나 paint를 다시 하지 않고, 이미 픽셀화(rasterize)된 요소의 transform만 바꿔 화면을 만들 수 있어요.

그래서 일반 Chrome에서는 transform, opacity, will-change, translate3d 같은 기법이 실제로 큰 도움이 되는 경우가 많아요. 브라우저 main thread의 layout/paint 비용을 줄이고, compositor thread/GPU 중심으로 애니메이션을 처리할 수 있기 때문이에요.

그러나 compositor로 올린다는 것은 웹 엔진 내부의 layout/paint를 피한다는 뜻이지, 최종 화면 제출 비용이 사라진다는 뜻은 아니에요.

Chrome에서는 이 최종 화면 제출 경로가 브라우저가 직접 소유한 compositor/surface 모델 안에서 비교적 자연스럽게 처리돼요.

불행히도 Android 웹뷰에서는 이 frame이 다시 host Android 앱의 View 렌더링 시스템과 만나요.

웹뷰는 작은 Chrome이 아니다

많은 사람이 웹뷰를 이렇게 생각해요.

앱 안에 Chrome 하나를 띄우는 것 아닌가?

일부는 맞지만, 엄밀하게는 달라요.

물론 Android 웹뷰는 Chromium 기반(IOS는 Webkit 기반)이에요. Blink 렌더링 엔진, renderer process, compositor 같은 개념을 공유해요. 그래서 웹 표준을 해석하고 화면을 그리는 기본 원리는 Chrome과 많이 닮아 있어요.

하지만 Android 웹뷰는 Chrome 앱 그 자체가 아니에요.

Chromium 공식 문서에서도 Android 웹뷰를 //chrome 레이어가 아니라 //content 기반 embedder로 설명해요. Chrome 브라우저와 같은 엔진 계층을 많이 공유하지만, Chrome UI와 브라우저 앱 구조를 그대로 쓰는 것은 아니에요.

가장 큰 차이는 이거예요. Chrome은 자신이 브라우저 앱이에요. 반면 웹뷰는 다른 Android 앱 안에 들어가는 View 일 뿐이에요.

그래서 웹뷰는 Android 앱의 View hierarchy 안에서 그려져야 해요.

Android 앱 렌더링 파이프라인

웹 개발자는 브라우저의 렌더링 파이프라인에는 익숙해도 Android 앱의 렌더링 파이프라인은 낯설 수 있어요.

Android 앱도 화면을 그릴 때 View 트리를 따라 렌더링해요. 하드웨어 가속이 켜져 있으면 Android는 display list, RenderThread, GPU 등을 사용해 View를 화면에 그려요.

Android 공식 문서에서도 하드웨어 가속 렌더링에서 display list 기반 모델을 설명해요. 간단히 말해 Android는 각 View의 그리기 작업을 기록해두고, 필요한 시점에 이를 조합해 화면에 표시해요.

웹뷰는 바로 이 Android View 시스템 안에 들어가요.

그래서 웹뷰는 Chromium compositor 결과물을 Android Canvas/HWUI(Hardware UI) 렌더링 흐름에 맞춰 끼워 넣어야 해요.

Chrome 렌더링 경로와 웹뷰는 비슷하지만 같지 않다

아주 단순화하면 Chrome의 렌더링 경로와 Android 웹뷰는 대략 이런 경로를 타요.

둘 다 Chromium의 많은 구성요소를 공유하지만, 웹뷰는 Android 앱의 View로서 그려져야 하므로 앱의 View 렌더링 흐름과 더 강하게 얽혀요.

이 차이가 항상 큰 성능 차이로 이어지는 것은 아니에요. 대부분의 일반적인 페이지에서는 사용자가 차이를 느끼지 못할 수도 있어요.

하지만 매 프레임 계속 도는 애니메이션이 있고, 그 애니메이션이 큰 레이어나 비싼 시각 효과와 결합되어 있다면, 이 차이가 극명하게 드러날 수 있어요.

일반 Chrome에서 compositor promotion이 줄여주는 비용은 주로 이런 것들이에요.

  • DOM layout 재계산
  • CSS paint 재실행
  • 큰 영역 raster 재생성
  • 브라우저 main thread 작업

그런데 이번 Android 웹뷰 문제에서 실제로 크게 보인 건 다음이었어요.

will-change로 animated element를 layer로 올려도, 그 layer가 매 프레임 움직이는 한 웹뷰는 계속 새 compositor frame을 만들고 host Android View 쪽 draw 흐름을 깨우고 있었어요.

실제 실험 결과에서도 will-change: transformtranslateZ(0)를 적용했지만, app CPU는 약 87%, gfxinfo frame 카운터는 약 1366 수준으로 남았어요.

일반 웹 최적화 관점에서는 충분한 코드였지만, Android 웹뷰의 host view integration 비용을 없애지는 못했어요.

이 상황을 이해했다면 대응 방향도 달라져야 해요. 웹뷰 안에서 어떻게든 CSS layer를 더 잘 올리려 하기보다, 계속 도는 애니메이션 자체를 웹뷰 밖으로 빼거나 웹뷰 안에서는 멈추는 전략이 필요해요.

그래서 무한 애니메이션에서 무슨 일이?

다시 처음의 CSS로 돌아가 볼게요.

.banner {
  animation: slide 20s linear infinite;
}

@keyframes slide {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-100%);
  }
}

이 애니메이션은 끝나지 않아요.

브라우저는 계속 새 frame을 만들어야 해요.

Chrome에서는 이 작업이 비교적 효율적으로 처리될 수 있어요. Android 웹뷰에서는 그 frame이 Android 앱의 View/HWUI 렌더링 흐름과 맞물려요.

가능한 흐름은 다음과 같아요. 실제 실험에서 직접 관찰한 것은 앱/웹뷰 프로세스 CPU 상승, RNCWebView/WebViewChromium.onDraw 반복, gfxinfo frame 카운터 증가예요.

  • 무한 CSS 애니메이션이 foreground-visible 상태로 실행
  • display refresh cadence에 맞춰 compositor frame이 계속 필요
  • 웹뷰가 Android View 렌더링 경로에 연결
  • RenderThread / Viz / GPU 작업이 증가
  • 이후 App Window Surface와 SurfaceFlinger composition
  • CPU/GPU 사용률 상승

실험에서는 단순 박스에 transform 애니메이션만 걸어도 같은 수준의 고CPU가 재현됐고, opacity 애니메이션에서도 비슷한 현상이 나타났어요. 반대로 같은 DOM이라도 애니메이션을 멈추거나 display: none에 가까운 hidden 상태로 만들면 CPU와 frame 수가 안정됐어요.

저희 제품에서는 오디오 제목 Marquee가 이 구조를 실제로 발동시킨 트리거였어요.

조사는 어떻게 했나요?

먼저 측정 환경을 명시하면 다음과 같아요.

  • Device: SM-F721N
  • Android: 16 / SDK 36
  • Android System WebView: 148.0.7778.217
  • Accessibility: disabled
  • Display: 최대 120Hz 지원, dumpsys 일부 출력에서는 current 60Hz 단서도 확인

1. 제품 앱에서 가시성 A/B 확인

먼저 실제 앱에서 오디오 플레이어가 보일 때와 보이지 않을 때를 반복해서 비교했어요.

오디오 플레이어 visible

  • 앱 CPU: 96~102%
  • 웹뷰 sandbox CPU: 21~23%
  • gfxinfo frame: 약 1367 (6s 측정)

오디오 플레이어 offscreen 또는 다른 탭

  • 앱 CPU: 3.7~6%
  • 웹뷰 sandbox CPU: 0% 또는 낮은 한 자리
  • gfxinfo frame: 0 (4s 측정)

이 단계에서 하단 fixed CTA, tooltip, tab bar 같은 공통 요소가 원인일 가능성은 크게 낮아졌어요. 같은 화면 요소가 남아 있어도 오디오 플레이어가 보이지 않으면 지속 frame이 발생하지 않았기 때문이에요.

2. 순수 Android 웹뷰 최소 재현

그 다음 RN과 제품 코드를 제거한 순수 Android 웹뷰를 만들고 다음 케이스들을 비교했어요.

static, progress-transform, marquee-paused, box-transform, text-transform, marquee-text, opacity-animation, hidden-transform

애니메이션이 없는 케이스는 idle이었고, 현재 화면에 노출되는 지속형 애니메이션이 있는 케이스들은 대부분 앱 CPU 87~96%, gfxinfo frames 1365~1369 수준으로 재현됐어요. 이 frame 수는 측정 스크립트의 누적 카운터로, 정확한 fps 계산보다 케이스 간 비교 지표로 사용했어요.

특히 box-transformopacity-animation에서도 같은 현상이 나온 것이 중요했어요. 이로 인해 “텍스트 Marquee만의 문제”나 “translateX만의 문제”는 약해졌어요.

3. Android Chrome과 비교

마지막으로 같은 HTML을 Android Chrome에서도 실행했어요.

Chrome에서도 애니메이션 비용은 있었지만, Android 웹뷰처럼 앱 프로세스 CPU가 90~100%까지 올라가거나 Android ViewRoot 관점의 gfxinfo frame이 폭증하는 양상은 아니었어요.

다만 Chrome의 웹 compositor frame은 dumpsys gfxinfo의 ViewRoot frame으로 직접 잡히지 않을 수 있고, Chrome total CPU와 웹뷰 app/sandbox CPU는 프로세스 집계 범위도 달라요. 여기서의 0은 “Chrome이 animation frame을 만들지 않았다”가 아니라 “웹뷰 harness에서 보인 host ViewRoot frame 폭증이 관찰되지 않았다”는 의미로 제한해 해석했어요.

애니메이션을 꼭 보여줘야 한다면?

모든 애니메이션을 포기할 수는 없겠죠? 중요한 건 “어디에서 애니메이션을 돌릴 것인가”예요.

이번 문제는 웹뷰 안에서 애니메이션을 돌렸을 때 발생했어요. 따라서 애니메이션이 꼭 필요한 UI는 웹뷰 밖, 즉 React Native layer나 native Android layer에서 처리하는 전략을 생각할 수 있어요.

단, 아래 대안들은 모든 앱이 웹뷰 중심으로 구현된 제품에서는 현실적이지 않아요. 뷰를 분리해서 렌더링 할 수 있는 플로우나, 일부 영역을 유기적으로 RN으로 렌더링 할 수 있는 제품인 경우에만 유효한 해결책이에요.

1. React Native Animated native driver

React Native의 AnimateduseNativeDriver: true를 사용할 수 있어요.

React Native 공식 문서에 따르면 native driver를 사용하면 애니메이션 정보를 시작 전에 native 쪽으로 보내고, 애니메이션이 시작된 뒤에는 매 프레임 JS bridge를 거치지 않고 native code가 UI thread에서 애니메이션을 수행할 수 있어요.

예를 들어 제목 marquee를 웹뷰 HTML 안의 CSS 애니메이션 대신 RN 컴포넌트로 분리할 수 있어요.

Animated.loop(
  Animated.timing(translateX, {
    toValue: -contentWidth,
    duration: 25000,
    easing: Easing.linear,
    useNativeDriver: true,
  })
).start();

return (
  <View style={{ overflow: 'hidden' }}>
    <Animated.Text
      numberOfLines={1}
      style={{ transform: [{ translateX }] }}
    >
      {title}
    </Animated.Text>
  </View>
);

웹뷰 CSS 애니메이션과의 차이는 다음과 같아요.

웹뷰 안 CSS 애니메이션과 웹뷰 밖 RN/native 애니메이션 비교

웹뷰 안 CSS 애니메이션과 웹뷰 밖 RN/native 애니메이션 비교

웹 엔진과 웹뷰 compositor를 거치지 않게 처리하는 방식이에요. 물론 native view도 화면에 그려져야 하므로 비용이 0은 아니지만, 이번에 문제가 된 웹뷰 host draw loop 증폭은 피할 가능성이 높아요.

그 외에 주의할 점도 있어요.

useNativeDriver는 모든 스타일 속성을 지원하지 않아요. 보통 transform, opacity처럼 native driver가 지원하는 속성 위주로 사용해야 해요. 또한 매 프레임 JS에서 setState로 위치를 바꾸는 방식은 피해야 해요.

2. React Native Reanimated

Reanimated를 사용한다면 애니메이션 worklet을 UI thread에서 실행하는 방식도 선택할 수 있어요.

Reanimated 공식 문서도 부드러운 애니메이션과 인터랙션을 UI thread에서 실행할 수 있다는 점을 강조해요. 이 방식 역시 JS thread나 bridge를 매 프레임 거치지 않도록 설계할 수 있어요.

개념적으로는 다음과 같아요.

const offset = useSharedValue(0);

useEffect(() => {
  offset.value = withRepeat(
    withTiming(-contentWidth, {
      duration: 25000,
      easing: Easing.linear,
    }),
    -1,
    false
  );
}, [contentWidth]);

const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ translateX: offset.value }],
}));

return (
  <View style={{ overflow: 'hidden' }}>
    <Animated.Text style={animatedStyle}>{title}</Animated.Text>
  </View>
);

여기서도 애니메이션 계산과 view property 갱신을 웹뷰 내부 CSS가 아니라 RN native/UI runtime에서 처리한다는 점이 핵심이에요.

3. Native Android View animation

Android native 코드라면 TextView나 custom View를 웹뷰 밖에 두고 translationX 또는 alpha 같은 view property를 애니메이션할 수 있어요.

예를 들어 ViewPropertyAnimatorObjectAnimator로 native view의 translationX를 움직이는 방식이에요.

titleView.animate()
  .translationX(-contentWidth)
  .setDuration(25_000L)
  .setInterpolator(LinearInterpolator())
  .withLayer()
  .start()

Android의 하드웨어 가속 모델에서는 View가 내부적으로 RenderNode/display list를 사용하고, translationX, alpha 같은 속성은 이미 그려진 display list 또는 layer의 property를 바꾸는 식으로 처리될 수 있어요.

위 해결책이 어려운 상황이라면

모든 앱이 웹뷰 중심으로 구현된 제품에서는 다음과 같은 전략이 필요해요.

1. 웹뷰에서는 무한 애니메이션을 기본적으로 피하기

가장 확실한 대응은 웹뷰 환경에서 계속 도는 애니메이션을 만들지 않는 거예요.

제품의 오디오 플레이어 제목처럼 장식적이거나 보조적인 움직임이라면, 웹뷰에서는 marquee 대신 말줄임 처리를 사용하는 편이 안전해요.

.title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

이번 사례를 발견하고 가장 먼저 핫픽스로 조치했던 부분이기도 해요.

2. 웹뷰 환경을 감지해 정책을 다르게 가져가기

웹뷰 안에서는 애니메이션 정책을 브라우저와 다르게 가져가는 것이 좋아요.

예를 들어 body.webview 같은 class를 부여할 수 있다면 다음처럼 처리할 수 있어요.

body.webview .marquee-track {
  animation: none !important;
  transform: none !important;
}

body.webview .marquee-duplicate {
  display: none;
}

React 컴포넌트 레벨에서는 웹뷰 환경일 때 marquee 컴포넌트 대신 정적 텍스트를 렌더링할 수도 있어요.

if (isWebView) {
  return <span className="truncate">{title}</span>;
}

return <Marquee>{title}</Marquee>;

3. 화면 밖 애니메이션은 명시적으로 멈추기

일반적으로는 브라우저가 offscreen 애니메이션을 알아서 잘 최적화 해주지만, 그 최적화만 믿기는 어려워요.

이번 실험에서도 offscreen-transformgfxinfo ViewRoot frame 카운터는 증가하지 않았지만 앱 CPU 평균 약 24.1%, 웹뷰 sandbox CPU 최대 약 15%가 남았어요.

따라서 화면 밖으로 나갔을 때는 브라우저 최적화에만 기대기보다 애니메이션을 명시적으로 pause하거나 unmount하는 편이 더 안전해요.

const observer = new IntersectionObserver(([entry]) => {
  element.style.animationPlayState = entry.isIntersecting
    ? 'running'
    : 'paused';
});

다만 웹뷰에서 정말 안전하게 가려면 decorative 애니메이션은 처음부터 웹뷰 fallback을 두는 편이 나아요.

4. spinner, auto carousel, looping video도 같은 관점으로 보기

이번 문제는 Marquee에서 드러났지만, 웹뷰 안에서 계속 도는 UI라면 모두 같은 관점으로 점검할 필요가 있어요.

  • 무한 marquee
  • auto carousel
  • 계속 도는 loading spinner
  • pulsing marker
  • looping placeholder animation
  • autoplay loop video

특히 사용자가 보고 있지 않아도 계속 움직이는 UI는 별도 기준으로 관리하는 것이 좋아요.

5. 일반 css 최적화는 보조 수단으로만 보기

다음 최적화는 일반 웹 성능에서는 여전히 중요해요

  • translate3d
  • will-change
  • layer 크기 줄이기
  • shadow/filter/blur 제거

다만 이번 Android 웹뷰 현상에서는 이미 linear timing function을 사용한 상태에서도 문제가 재현됐고, will-changetranslateZ(0)만으로 host view draw loop가 사라지지 않았어요.

우선순위는 다음처럼 보는 편이 좋아요.

  1. 웹뷰에서 continuous animation을 끄거나 줄인다.
  2. 꼭 필요할 때만 짧은 시간 실행한다.
  3. 화면 밖/background/blur 상태에서는 명시적으로 pause한다.
  4. 그 다음 layer 크기와 시각 효과를 최적화한다.

6. 모션 감소 설정 존중하기

사용자가 모션 감소를 선호하는 경우 애니메이션을 줄이는 것도 좋아요.

@media (prefers-reduced-motion: reduce) {
  .banner {
    animation: none;
  }
}

비단 성능뿐 아니라 접근성 측면에서도 좋은 선택이에요.

정리

웹에서 배운 성능 규칙은 여전히 유효해요. transformopacitytop, left, width, height를 계속 바꾸는 것보다 훨씬 좋은 선택이에요.

하지만 이번 조사에서 확인한 내용은 웹뷰 환경에서의 웹앱 성능에 대해 조금 더 구체적인 조치가 필요하다는 것을 의미해요.

이 글이 웹뷰를 사용한 앱으로 서비스를 하는 제품 뿐만 아니라, 다른 앱의 인앱 웹뷰를 통해 서비스가 노출될 수 있는 모든 웹 개발자분들에게 현재 환경을 검토하고 돌아볼 수 있는 계기가 되면 좋겠어요.

참고자료


메타데이터
post_id
32a80c6ffa40
slug
웹뷰-속-무한-css-애니메이션의-숨은-비용-32a80c6ffa40
url
https://medium.com/creatrip/%EC%9B%B9%EB%B7%B0-%EC%86%8D-%EB%AC%B4%ED%95%9C-css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EC%9D%98-%EC%88%A8%EC%9D%80-%EB%B9%84%EC%9A%A9-32a80c6ffa40
canonical_url
https://medium.com/creatrip/%EC%9B%B9%EB%B7%B0-%EC%86%8D-%EB%AC%B4%ED%95%9C-css-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EC%9D%98-%EC%88%A8%EC%9D%80-%EB%B9%84%EC%9A%A9-32a80c6ffa40
author_url
https://medium.com/@creatrip-jonghak
status
ok
fetched_at
2026-06-20 20:29:01