14 minute read

Galaxy S21 WebView 동영상 녹색 화면 이슈 디버깅 및 해결

문제 상황: Android WebView에서의 녹색 화면 발생 시나리오

Galaxy S21 시리즈 기기에서 Android WebView를 통해 H.264 코덱 MP4 동영상을 재생하는 도중, 영상 화면이 갑자기 녹색으로 변하는 현상이 발생하였다. 특히 AWS IVS 기반 스트리밍이나 임베디드 YouTube 플레이어 등 WebView 내 <video> 요소로 영상을 재생할 때 이러한 현상이 보고되었다. 사용자 입장에서는 영상 콘텐츠 대신 초록색 화면만 보이고, 일부 경우 영상이 화면 한쪽 구석으로 축소되는 등의 이상 증상이 나타났다. 이 문제는 간헐적으로 발생하며, Galaxy S21 시리즈 (Exynos/Snapdragon 버전 포함)에서 주로 보고되었다. 다른 앱이나 브라우저(예: YouTube 앱 자체)로 동일 영상을 재생할 때는 문제가 없지만, WebView 기반 재생에서만 이러한 “그린 스크린” 문제가 발생하였다.

흥미로운 점은 Android System WebView의 최신 업데이트 이후부터 이러한 문제가 보고되기 시작했다는 것이다(어느 사용자의 github issue). WebView 업데이트를 제거하면 문제가 일시적으로 해소되기도 하나, 시스템이 자동으로 WebView를 재업데이트하면 다시 문제가 나타나는 상황이었다. 이로 미루어 볼 때 WebView (Chromium) 엔진의 변화와 특정 기기(Galaxy S21) GPU 환경 사이에 발생하는 호환성 결함으로 추정되었다.

디버깅 과정: DevTools, Media Internals, Layers 활용

문제 원인을 찾기 위해 크롬 원격 디버깅 도구들을 활용하였다. 먼저 개발 중인 앱에 WebView 디버깅을 활성화하여 Chrome DevTools로 접속한 뒤, 콘솔 로그나 네트워크 상태보다는 렌더링 관련 정보에 주목했다.

  • Layers 패널: Chrome DevTools의 Layers 탭을 통해 WebView 내부 페이지의 레이어 구성과 GPU 합성 상태를 살펴보았다. 정상적인 경우 비디오 요소는 별도의 Video Layer로 존재하며, WebView의 다른 UI 요소들과 함께 합성(compositing)되거나 Overlay로 분리된다. 문제가 발생하는 순간의 Layers 트리를 관찰한 결과, 비디오 레이어가 Overlay로 표시된 상태에서 다음 프레임에 제대로 내용이 채워지지 않고 빈 화면(녹색)으로 나타나는 것을 확인하였다. 이는 비디오 프레임이 GPU를 통해 합성되지 않고 별도 경로로 표시되다가 뭔가 문제가 생겼음을 시사한다.

  • Media Internals: chrome://media-internals 페이지를 열어 WebView의 미디어 파이프라인 로그를 분석하였다. 이 내부 진단 로그에는 미디어 플레이어의 상태 변화, 디코딩 코덱 및 출력 형식, 프레임 렌더링 시각 등 상세 정보가 기록된다. 로그를 추출하여 비디오 디코더 종류(예: 하드웨어 디코더 사용 여부), YUV→RGB 변환 경로, 프레임 투입/표시 타이밍을 면밀히 검토하였다. 특히 문제가 발생할 때 디코더는 정상적으로 프레임을 디코딩했으나, 렌더링 단계에서 프레임이 화면에 표시되지 못한 채 건너뛰어진 정황이 포착되었다. media-internals 로그의 타임스탬프를 보면, 녹색 화면이 나타난 시점에 “Video frame submitted to compositor” 와 같은 이벤트가 누락되거나 지연되고 있었다. 이를 통해 비디오 프레임이 디코딩된 후 합성(compositing)이 제대로 이루어지지 않았음을 알 수 있었다.

  • DevTools 콘솔 및 기타: 브라우저 콘솔에는 별다른 JavaScript 오류나 WebGL 이슈는 나타나지 않았다. 이는 문제 원인이 애플리케이션 로직보다는 렌더링 엔진 내부에 있을 가능성을 높여준다. 또한, GPU 내부 오류를 확인하기 위해 Android 디바이스의 Logcat 출력도 확인했으나 특별한 크래시나 GLES 에러는 기록되지 않았다. 대신 SurfaceFlinger 관련 로그에서 “HWC comp FAIL”과 유사한 메시지가 일시적으로 보였는데, 이는 하드웨어 컴포지터(HWC) 경로의 실패를 암시하는 것으로 추정되었다.

chrome://media-internals 로그 분석 방법

Media Internals에서 추출한 로그는 JSON 형식으로 되어 있어 육안으로 해석하기 복잡하므로, 핵심 정보를 파악하는데 집중했다. 분석 절차는 다음과 같다:

  1. 플레이어 식별: media-internals에는 다수의 Player ID가 있을 수 있는데, 문제를 재현하는 동안 활성화된 플레이어 항목을 식별한다. (예: player_id: 7 등)

  2. 디코딩 파이프라인 확인: 해당 플레이어 로그 내 kVideoDecoderName, kIsPlatformVideoDecoder 등을 찾아 디코더 종류를 확인한다. 실제로 로그에서는 MojoVideoDecoder(Chromium의 디코더 래퍼) 아래에 MediaCodec 하드웨어 디코더가 선택된 것을 확인하였다.

  3. 프레임 출력 및 표시 로그 추적: VideoFrameGenerated, VideoFrameSentToCompositor와 같은 키워드를 검색하여 프레임의 흐름을 추적했다. 정상적인 경우 일정 주기마다 새로운 프레임이 생성되고 컴포지터로 전달되어야 한다. 그러나 문제가 발생한 구간에서는 VideoFrameGenerated는 계속 증가하지만 SentToCompositor 이벤트가 누락되어 있었다. 이는 프레임이 생성되었으나 합성 단계에 투입되지 못한 것을 의미한다.

  4. 색공간/표면 정보 확인: kVideoColorSpacekOutputFormat 로그를 통해 YUV 형식 및 색공간 변환 정보를 살폈다. 여기에서 출력 포맷이 YUV(NV12)로 유지되고 RGB 변환 단계 로그가 보이지 않는 점을 발견했다. 이는 YUV->RGB 변환을 수반하는 GPU 경로가 활성화되지 않고 있었음을 시사한다.

이러한 media-internals 로그 해석을 통해 문제 구간에서 비디오 디코딩은 정상이지만 합성 또는 디스플레이 단계에서 프레임 처리가 누락된 사실을 파악하였다. 다음 단계는 왜 이런 일이 발생하는지, 근본 원인을 밝히는 것이었다.

원인 분석: GPU 유휴 상태에서의 색공간 변환 경로 초기화 결함

디버깅 결과를 종합하면, GPU 컴포지터(compositor)가 유휴 상태일 때 하드웨어 비디오 프레임의 색공간 변환 경로가 제대로 초기화되지 않아 녹색 화면이 발생하는 것으로 분석되었다. 쉽게 말하면, 모바일 GPU의 절전 모드와 영상 오버레이 처리가 충돌하여 생긴 버그라는 것이다.

일반적으로 하드웨어 비디오 디코더(MediaCodec)는 YUV 형식의 프레임을 출력한다. 디스플레이에 보여주려면 이를 RGB로 변환해야 하는데, Chromium 기반 WebView는 상황에 따라 두 가지 경로를 취한다:

  • GPU 경유 경로: YUV 데이터를 GPU에서 셰이더를 통해 RGB로 변환하고 다른 UI 요소들과 합성한다. (합성된 레이어 경로)
  • HWC 오버레이 경로: YUV 데이터를 디스플레이 하드웨어 오버레이로 직접 전달하여 디스플레이 장치가 YUV->RGB 변환 및 합성을 처리하게 한다. (Overlay 플레인 경로)

Galaxy S21의 사례에서는 후자인 하드웨어 오버레이(Overlay) 경로가 사용되고 있었다. Overlay 경로에서는 Chromium단에서 영상은 페이지에 불투명한 구멍(hole)으로 취급되고, 실제 영상은 OS 수준의 컴포지터(SurfaceFlinger) 를 통해 화면에 입혀진다. 이 방식은 GPU를 거치지 않으므로 전력 효율이 뛰어나, Android 등 모바일 플랫폼에서 전체 화면이 아니어도 영상에 오버레이를 적극 사용하여 전력 소모를 최대 50%까지 줄일 수 있다(Chromium VideoNG). 실제 Chromium 미디어 파이프라인은 가능한 한 플랫폼의 Overlay/Surface 기능을 활용하도록 설계되어 있다.

그러나 바로 이 절전 효율화 기법이 S21 환경에서 결함을 일으켰다. 문제가 된 원인은 “GPU Idle 상태에서 하드웨어 Overlay 경로의 초기화 버그” 로 파악되었다. 즉, WebView의 컴포지터가 일정 시간 작업이 없어서 idle이 된 상황에서, 새로운 비디오 프레임이 도착할 때 GPU의 YUV->RGB 변환 파이프라인이 재가동되지 않는 현상이 있었다. 모바일 GPU 드라이버 또는 Chromium의 합성 로직 결함으로 인해, GPU가 낮은 전력 모드로 내려간 뒤 다시 활성화될 때 Overlay 경로 설정이 누락되어 버린 것이다. 이 때문에 화면에는 최신 프레임 대신 변환 안 된 YUV 플레인 (초록색으로 보이는 화면) 이 나타난 것으로 보인다. YUV 영상에서 색 변환이 적용되지 않으면 보라색이나 녹색 톤의 화면이 표시되는 것이 전형적인 증상인데, 녹색 화면은 마치 색상 성분(Cb/Cr)이 빠진 Y 평면만 출력된 듯한 모습이었다.

추가로, Galaxy S21의 SoC GPU 특성도 영향을 미쳤을 가능성이 높다. S21 (Exynos 2100 버전)은 ARM Mali-G78 GPU를 탑재하고 있는데, 이 GPU와 드라이버의 동적 전력 관리(DVFS) 정책이 적극적으로 동작하여 사용되지 않는 GPU 기능 블록을 즉시 클럭 게이팅/전원 차단한다. 이러한 절전 중 GPU를 다시 깨울 때, Chromium이 기대하는 Overlay 구성과 실제 하드웨어 상태 간에 레이스 컨디션이나 초기화 누락이 발생한 것으로 추정된다. Compositor Idle은 Chromium이 내부적으로 렌더링 업데이트가 필요 없다고 판단하여 프레임 생성을 잠시 멈춘 상태인데, 이때 비디오 프레임은 OS Overlay로 계속 표시되고 있었다. 하지만 완전히 정지된 컴포지터와 절전 상태의 GPU가 다시 새로운 프레임에 반응하면서, 첫 프레임에 대한 색공간 변환 설정이 적용되지 않는 버그가 트리거된 것이다.

모바일 GPU 절전 정책과 Compositor Idle의 상관관계

현대 모바일 GPU에서는 연속적으로 그래픽 연산이 수행되지 않을 경우 몇 밀리초 내에 클럭을 낮추거나 코어를 일시 정지(idle)하는 절전 정책을 갖고 있다. Chrome/WebView의 컴포지터(compositor) 역시 프레임 갱신이 필요하지 않으면 새로운 합성 작업을 쉰다. 이 두 메커니즘이 겹치면, 다음과 같은 시나리오가 가능하다:

  1. WebView 페이지 상의 UI가 정적이고, 영상은 Overlay로 출력되는 중이다. 이때 Chromium 컴포지터는 “페이지 내용에 변화가 없다”고 판단하여 Compositor를 idle 모드로 전환한다 (즉, 새로운 composite frame을 만들지 않음). 그래도 영상 자체는 OS를 통해 계속 재생된다.

  2. GPU는 합성 작업 부하가 없어지자 클럭 다운 혹은 유휴 모드로 진입한다. 비디오 프레임은 GPU 메모리->디스플레이로 복사만 이뤄지므로 GPU의 셰이더 코어 등은 쉬는 상태일 수 있다.

  3. 어느 순간 (예를 들어 영상 해상도 변화, 색공간 변경, 또는 앱 UI의 경미한 변화 발생) Chromium이 다시 합성 작업을 수행하려고 한다. 이때 GPU를 깨워 YUV->RGB 변환 또는 overlay 구성을 하려고 하지만, 이미 드라이버/하드웨어 측에서 해당 경로가 초기화 해제된 상태이기 때문에 첫 프레임 처리가 제대로 안 된다. 결과적으로 그 프레임은 잘못된 색상(녹색)으로 표시되거나 누락된다.

  4. 이후 UI에 변화가 생겨 컴포지터가 지속적으로 프레임을 생성하기 시작하면, GPU도 완전히 깨어나 정상적인 YUV→RGB 처리가 재개되어 영상이 정상 출력된다. (사용자가 화면을 터치하거나 앱의 다른 애니메이션이 발생하면 녹색 화면이 사라지는 것은 이 때문이다.)

이러한 상호작용을 통해, 모바일 GPU 절전Chromium 합성 최적화가 교차하는 지점에서 버그가 발생함을 이해할 수 있다. 다시 말해, “컴포지터에는 변화가 없고 영상은 별도로 도는” 특수한 상황에서 첫 프레임 컬러 변환이 누락되는 것이다.

Overlay Plane과 합성된 레이어: 영상 출력 경로 비교

앞서 언급한 Overlay 경로합성된(composited) 경로의 차이를 정리하면 다음과 같다:

  • Overlay Plane 경로: 영상 프레임이 Hardware Composer(디스플레이 하드웨어) 의 별도 오버레이로 전달된다. Chromium은 해당 <video> 요소 부분을 투명한 구멍으로 처리하고, 비디오 콘텐츠는 GPU를 거치지 않고 디스플레이 컨트롤러가 직접 합성한다. 이 경우 YUV->RGB 변환도 디스플레이 장치에서 처리하므로 GPU 부하와 메모리 복사 감소로 전력 효율이 높다(VideoNG). 다만, CSS 필터나 특정 효과가 적용된 영상의 경우 오버레이를 사용할 수 없으며, 웹페이지의 다른 요소와 동기 렌더링(sync) 문제가 없도록 Chromium은 OS와 프레임 스케줄링을 조율한다.

  • 합성된 레이어 경로: 영상 프레임이 GPU Texture로 업로드되어 WebView의 다른 요소들과 함께 GPU에서 합성된다. 이 때 Chromium의 GPU 프로세스는 YUV 데이터를 셰이더를 통해 RGB로 변환하여 텍스처로 만들고, 하나의 레이어로써 합성 프레임버퍼에 그린다. 영상이 일반 이미지처럼 취급되어 모든 웹 요소와 동일한 합성 트리에 포함되므로, 페이지의 repaint 규칙을 그대로 따르고 다른 DOM 변화와 함께 동기화된다. 그러나 GPU 메모리에 프레임 복사 및 색변환 연산이 필요하므로 전력과 성능 면에서 부담이 된다. Chromium은 가능하다면 이 경로를 피하고 Overlay를 쓰지만, 화면 회전, WebGL 캔버스 위 겹치기 등 오버레이 불가 조건에서는 자동으로 합성 경로를 사용한다.

이 사건에서는 Overlay 경로 사용 중 발생한 버그였기에, 합성 경로였다면 어땠을지 가정해볼 수 있다. 합성 경로의 경우 매 프레임 GPU가 관여하므로 Compositor가 idle로 빠질 일이 없고, 설령 GPU 절전이 개입하더라도 다음 프레임에서 셰이더로 YUV->RGB 변환을 수행하므로 녹색 화면이 나올 가능성은 낮다. (하지만 그러면 전력 소모가 크게 증가하고 발열이 심해졌을 것이다.) 결과적으로 Chromium이 의도한 대로 전력 최적화를 위해 Overlay를 사용한 상황에서만 나타나는 특이한 문제였다.

Android WebView 렌더링 파이프라인 분석

이 문제를 깊이 이해하려면 Android WebView의 렌더링 파이프라인을 살펴볼 필요가 있다. WebView는 크롬과 동일한 Blink/Chromium 엔진으로 동작하지만, Android 앱의 뷰(View)로 포함되어 동작하는 특수성이 있다. 아래는 WebView에서 비디오 프레임이 화면에 표시되기까지의 단계를 요약한 파이프라인이다:

단계 구성 요소 및 역할
1. 앱 (Android) 앱 레이아웃에 WebView를 포함. Android UI 쓰레드에서 WebView로 Surface를 할당하여 GPU 합성 가능하도록 함.
2. WebView (Chromium) WebView의 Chromium 엔진이 HTML 파싱/렌더링. <video> 태그 매체 요소 생성 후 미디어 플레이어 (WebMediaPlayer) 초기화.
3. 비디오 디코딩 (MediaCodec) Chromium이 Android MediaCodec API를 통해 하드웨어 디코더 이용. H.264 스트림을 디코딩하여 YUV 프레임 생성. 출력은 SurfaceTexture/Surface 등으로 연결됨.
4. GPU 합성 / 오버레이 Chromium의 GPU compositor 모듈이 합성 작업 수행. 이때 비디오 프레임은 SurfaceTexture를 통해 GPU 텍스처로 취득되거나, SurfaceView를 통해 Overlay로 분리됨. Overlay인 경우 GPU 합성 결과에는 비디오가 빠진 투명 영역으로 남고, 아닌 경우 GPU가 YUV->RGB 변환해 합성.
5. 디스플레이 합성 (SurfaceFlinger) Android 하드웨어 컴포저(HWC, SurfaceFlinger/DPU)가 최종 합성. GPU가 그린 앱 UI 레이어와 비디오 Overlay 레이어를 하나로 합쳐 디스플레이 패널에 출력. 전체 과정이 vsync 타이밍에 맞춰 진행됨.

이상의 과정을 거쳐 사용자에게 영상이 보이게 된다. WebView와 데스크톱 Chrome의 큰 차이는, WebView는 앱의 한 구성요소로서 Android 시스템 컴포지터에 참여한다는 점이다. 데스크톱 Chrome은 자체 창 내에서 모든 합성을 관리하지만, Android WebView는 최종적으로 SurfaceFlinger의 통제를 받는다. WebView에서 영상 재생 시 SurfaceView를 활용하는 것도 이러한 구조 때문이다(VideoNG). 결과적으로 WebView는 한 단계 더 계층이 깊고, Chromium 컴포지터와 Android 컴포지터의 이중 협업이 필요하다.

Paint, Raster, Composite: 렌더링 단계와 Dirty Flag

Chromium의 렌더링 파이프라인에는 Paint, Raster, Composite 세 단계가 있으며, 각각 언제 실행될지 더티 플래그(dirty flag) 로 관리된다. 각 단계의 의미는 다음과 같다:

  • Layout/Paint 단계: DOM/CSS 변화를 계산하여 그릴 목록(display list) 을 생성한다. 요소의 위치, 스타일 변화 등으로 페인팅이 필요할 때 needs_paint 플래그가 설정된다. 이 단계에서는 벡터 형태로 무엇을 그릴지 기록만 하고, 실제 픽셀을 생성하지는 않는다.

  • Raster 단계: Paint 단계에서 준비된 display list를 기반으로 실제 비트맵 또는 텍스처로 래스터라이즈한다. GPU 가속 모드에선 이 작업이 GPU의 래스터라이저에서 이루어지며, 각 레이어 별로 타일(tile) 단위로 수행되기도 한다. needs_raster 플래그가 설정된 경우에 실행되며, raster가 이루어지면 해당 레이어에 픽셀 데이터가 준비된다.

  • Composite 단계: 래스터된 레이어들을 합성하여 최종 프레임을 구성한다. 여기서는 각 레이어를 적절한 순서와 투명도로 화면 버퍼에 그려내는 작업이다. needs_composite (혹은 needs_draw) 플래그가 있을 때 실행되며, 이 단계가 끝나면 한 프레임의 GPU 그림 작업이 완료되어 화면에 표시된다.

Chromium은 성능을 위해 이들 단계를 최소한으로 수행하도록 최적화되어 있다. 즉, 어떤 단계의 더티 플래그가 켜졌느냐에 따라 이후 단계들만 수행하고, 변화가 없으면 해당 단계를 건너뛴다. 예를 들어 단순히 스크롤하거나 변형(transform)만 발생했다면 layout이나 paint 없이 기존 래스터 결과를 재합성만 한다. 반대로 DOM 구조 변경이 있으면 paint부터 다시 하고, 그 결과 raster, composite까지 이어진다. 간단히 말해, 앞단계에 변화가 생기면 뒷단계 전체가 연쇄 실행되지만, 변화가 국한되면 뒷단계만 수행하는 식이다(Animating and Compositing). 이러한 더티 플래그 전략으로 불필요한 연산을 줄이고 프레임률을 높이는 것이다.

이번 사례에서 문제가 되었던 상황은, WebView 내용 상 paint/raster 할 변화는 전혀 없고(video는 Overlay로 처리), 합성 단계도 계속 동일하여 Chromium 입장에서는 needs_composite조차 발생하지 않은 상태였다. 즉 모든 렌더링 단계의 더티 플래그가 false인 Idle 상태가 되었던 것이다. 정상적으로는 비디오 프레임 업데이트 자체가 needs_composite을 트리거해야 할 것 같지만, 앞서 설명한 대로 Overlay 경로에서는 Chromium 컴포지터에게 비디오 프레임 변화가 투명하게 보이지 않는다. WebView의 Layer Tree 상에서는 비디오가 한번 Overlay로 확정되면, 별도 신호가 없는 한 계속 동일한 외부 레이어로 간주되었을 가능성이 있다. Desktop Chrome의 경우 동영상이 재생될 때 내부적으로 프레임별 invalidate를 해주지만, WebView에서는 최적화상의 이유로 이를 하지 않거나, Android 시스템이 관리한다고 가정했을 수 있다. 결과적으로 WebView의 컴포지터 레이어 트리는 정지한 것처럼 되고, 영상은 WebView 바깥(OS 레벨) 에서 돌아가는 구조가 되어, 앞서 언급한 GPU 초기화 이슈에 취약해졌다.

WebView vs 데스크톱 Chrome: 레이어 트리와 최적화의 차이

WebView와 데스크톱 Chrome은 동일한 Chromium 엔진을 공유하지만, 레이어 트리 구성과 최적화 동작에서 차이가 존재한다. 데스크톱 Chrome의 경우 브라우저 자체가 전체 시스템에서 독립된 Window로 동작하며, 크롬 컴포지터가 모든 것을 총괄한다. 반면 WebView는 Android 앱의 View로 포함되기 때문에, Chromium의 합성 결과물이 다시 Android View 시스템에 합쳐지는 계층 구조이다.

이 차이는 다음과 같은 영향이 있다:

  • 레이어 트리 구조: WebView 내부의 Layer Tree는 최종 출력 시 Android SurfaceFlinger에 제출될 하나의 표면으로 간주된다. 데스크톱에서는 최종 출력까지 모두 Chromium 관리하에 있지만, WebView는 최종 합성을 OS에 위임한다. 이로 인해 Chromium이 인지하지 못하는 합성 요소(예: Overlay) 가 존재할 수 있다. 본 사안에서 WebView의 레이어 트리에는 비디오 레이어 자리에 외부 Surface로 표시되었고, 실제 프레임 갱신은 OS 측에서 이루어졌다.

  • 렌더링 스케줄링: 데스크톱 Chrome은 비디오 재생 시 requestAnimationFrame 등을 활용해 60fps로 계속 합성 프레임을 만들거나, 최소한 빈 프레임이라도 전송하여 vsync에 동참한다. 그러나 WebView는 호스트 앱과 Android 특성상, 활성 UI 업데이트가 없으면 합성 스레드를 쉽니다. 배터리 절약을 위해서인데, 이때 비디오가 Overlay로 처리되면 WebView 측에선 할 일이 없다고 판단하기 쉽습니다. 즉 WebView는 “내가 그릴 UI 변화가 없다”고 판단하여 합성을 멈추지만, 영상은 돌아가는 미묘한 불일치가 생깁니다. 이 최적화는 일반적으로 문제없지만, 이번 경우처럼 컴포지터 재개 시점에 문제가 터질 수 있었습니다.

  • 최적화 취약점: WebView는 임베디드 엔진이므로 브라우저 전체를 통제하는 데 한계가 있습니다. 예를 들어 데스크톱 Chrome은 GPU 오버레이 사용 중에도 UI 이벤트나 영상 프레임에 따라 자체적인 invalidation을 걸 수도 있지만, WebView는 Android 시스템신호에 의존합니다. 결과적으로 WebView 환경에서 Chromium의 일부 최적화는 과하게 적용되거나, 반대로 OS 최적화와 겹쳐서 예기치 않은 상황을 만들 수 있습니다. 이번 녹색 화면 현상은 바로 그런 WebView의 최적화 맹점으로 볼 수 있습니다. WebView 엔진이 “충분히 최적화되었다”고 안심한 부분에서 실제로는 동기화 이슈가 발생한 것입니다.

요약하면, 데스크톱 Chrome에서는 레이어 트리가 Chromium 내부에 모두 있어서 영상 프레임도 합성 흐름에 자연스럽게 녹아들지만, WebView에서는 레이어 트리가 이원화되고 합성 최적화의 빈틈이 발생하여 이런 문제가 야기되었습니다.

해결책: 지속적인 렌더링으로 composite invalidation 강제

근본적인 원인은 Chromium(WebView)이나 GPU 드라이버 쪽 버그이지만, 애플리케이션 개발자 입장에서 즉각 수정할 수는 없는 사항이었다. Chromium 소스 수정이나 드라이버 업데이트를 기다리는 동안 임시 대응책으로 “항상 렌더링이 유지되도록 만드는” 방법을 시도하였다. 구체적으로는, WebView 위에 작은 투명한 Spinner 애니메이션 컴포넌트를 겹쳐 놓아 컴포지터가 계속 바쁘게 만드는 것이다.

React 기반으로 구현된 해당 앱에서는 비교적 간단하게, 동영상 뷰 최상단에 CSS 애니메이션으로 돌아가는 작은 로딩 아이콘을 추가할 수 있었다. 크기는 매우 작고 투명하게 하여 사용자가 시각적으로 인지하지 못하게 하되, DOM 상으로는 지속적으로 변화하는 요소다. 이렇게 함으로써 Chromium은 매 프레임 합성 작업을 발생시키게 된다. 즉, 더 이상 Compositor가 idle 상태로 장시간 머무르지 않고, 지속적으로 needs_composite 플래그가 셋되어 FPS를 유지하는 셈이다. Spinner를 추가했는데도 녹색 화면 현상이 발생한다면, 스피너의 반지름을 작게 하면 문제가 해결된다. 스피너의 반지름이 작을 수록 GPU를 쉴 틈 없이 바쁘게 돌아가기 때문이다.

이 조치를 적용한 결과 Galaxy S21 기기에서 녹색 화면 현상이 재현되지 않음을 확인했다. Spinner를 추가하기 전에는 수십 초 ~ 몇 분 재생 시 나타나던 녹색 화면이, 추가 후에는 여러 시간 연속 재생해도 발생하지 않았다. 이는 작은 애니메이션이 항상 GPU를 깨워두는 역할을 해서, 앞서 문제의 원인이 된 “GPU 유휴 시 첫 프레임 색변환 누락” 상황 자체를 미연에 방지했기 때문이다.

물론 이 해결책은 임시 방편(hotfix) 성격이 강하며, 근본적으로 WebView/Chromium 또는 드라이버 측 버그 수정이 궁극적 해결책이다. 그러나 실무에서는 이러한 그래픽스 버그에 즉각 대처해야 하므로, 의도적으로 합성 부하를 줘서 문제를 피해가는 워크어라운드도 유용한 전략이 될 수 있다. 다행히 작은 스피너 하나 정도의 오버헤드는 현대 모바일 기기에서 미미한 수준이고, 전력 소모에도 큰 영향이 없었다. 대신 이로 인해 WebView의 렌더링 루프는 계속 돌기 때문에 requestAnimationFrame 등이 계속 활성화되는 점은 감안해야 한다. 만약 개발 중인 앱이 극도로 배터리 소모에 민감하다면 이 접근을 신중히 적용해야겠지만, 영상 재생 같은 본래 고부하 작업이 있는 맥락에서는 부작용이 적다고 판단되었다.

정리하면, “렌더링 유휴 상태를 없애 문제를 우회” 한 것이 이 녹색 화면 이슈의 실용적 해결책이었다.

유사 사례 및 향후 개선 방향

이와 비슷한 사례가 다른 기기나 환경에서 보고된 적이 있는지 조사해보았다. 2024년 초 일부 개발자 포럼에서는 WebView 업데이트 후 동영상 재생 시 초록색 화면을 겪었다는 보고가 있었다. 해당 경우에도 WebView의 버그로 지목되었으며, WebView를 롤백하거나 업데이트를 기다리라는 답변이 있었다. Galaxy S21 외에 S20/S22 등의 기기 사용자들도 Reddit 등지에서 임베디드 비디오 재생 시 색이 이상해지거나 녹색/보라색으로 왜곡되는 현상을 공유한 바 있다. 이는 근본적으로 크로미움 엔진 공통의 문제일 수 있음을 시사한다. 다만 모든 사례가 동일 원인인지는 확신할 수 없으나, 하드웨어 가속과 절전 기능이 맞물릴 때 발생하는 영상 출력 버그라는 점은 유사하다.

이 문제를 근본적으로 해결하려면 Chromium 쪽에서 영상 프레임 업데이트 시 컴포지터를 깨우는 로직 개선 또는 첫 프레임 overlay 재개시 안전장치가 필요할 것이다. 예컨대, Overlay로 동작 중인 비디오 프레임에서 일정 시간 컴포지터가 쉰 경우, 다음 프레임에 강제로 GPU에서 한 번 렌더링하도록 해 색공간 변환 경로를 초기화한다든지 하는 대응이 가능하다. 실제 Chromium 이슈 트래커를 살펴보면 유사한 맥락의 버그가 보고되어 패치된 사례들도 있다 (예: 첫 프레임 검은 화면 이슈 등). 이러한 개선이 이루어지면 향후에는 애플리케이션 레벨의 워크어라운드 없이도 문제가 해결될 것이다.

이번 사례는 모바일 환경에서 WebView와 하드웨어 가속의 미묘한 상호작용이 얼마나 복잡한지 보여준다. React로 구축된 하이브리드 앱이나 웹 콘텐츠를 활용하는 앱을 개발할 때, 브라우저 엔진의 최적화 동작을 이해하고 있어야 예기치 않은 버그에 대응할 수 있다. 디버깅 과정에서 DevTools와 chrome://media-internals 같은 도구를 활용한 것은 문제의 근원을 밝히는 데 큰 도움이 되었다. 특히 media-internals는 미디어 파이프라인 내부에서 무슨 일이 일어나는지를 보여주므로, 영상/오디오 관련 문제 해결에 필수적인 도구임을 다시 한 번 실감했다.

마지막으로, 임시로 적용한 렌더링 지속 기법(Spinner 추가) 은 다른 그래픽스 글리치에도 응용 가능하다. 예컨대 일부 기기에서 WebView 콘텐츠가 일정 시간 후 멈춘다면, 보이지 않는 애니메이션을 넣어 강제로 계속 돌게 하는 식이다. 이런 트릭은 근본 해결은 아니지만, 사용자 경험을 해치지 않으면서 버그를 우회하는 실용적인 방법이 될 수 있다.

결론

Galaxy S21 시리즈 WebView의 동영상 녹색 화면 문제는 GPU 절전과 Chromium 합성 최적화의 경계에서 발생한 특이한 버그였다. 철저한 디버깅을 통해 원인을 추론하고, 작은 애니메이션 컴포넌트를 추가하는 창의적인 해결책으로 문제를 완화하였다. 이 경험을 통해 복잡한 렌더링 파이프라인을 이해하고 버그에 대처하는 능력을 한층 키울 수 있었으며, 향후 유사한 이슈에 대비한 대응책도 마련하게 되었다. WebView와 하드웨어 가속을 다루는 개발자들에게 본 사례가 유익한 참고가 되길 바란다.

참고


EOD

20250530

Leave a comment