[Android] WebView를 일반적인 View처럼 생각하고 생성해도 괜찮을까?

Kame·2026년 8월 8일

Android

목록 보기
19/21

들어가며

이 글의 내용을 영상으로도 감상하실 수 있습니다.

Android WebView의 생성 비용을 실제 측정과 내부 구조를 통해 분석하고, 앱 성능 및 ANR과 어떤 관계가 있는지 살펴봅니다.

WebView는 android.view.View를 상속받는 컴포넌트입니다. 따라서 TextViewImageView처럼 필요할 때 인스턴스화해서 사용하면 그만이라고 생각하기 쉬우며, 기능 동작상으로도 아무런 문제가 없습니다.

하지만 WebView는 Android의 다른 View들과는 성격이 다릅니다. 겉보기에는 하나의 뷰 객체처럼 보이지만, 내부적으로는 Chromium 기반 브라우저 렌더링 엔진 전체를 실행하는 무거운 컨테이너에 가깝습니다. 즉, WebView() 생성자를 호출하는 순간 단순히 메모리에 뷰 하나를 올리는 것이 아니라, 브라우저 실행 환경(Chromium Runtime)을 초기화하는 작업이 함께 수행되는 것입니다.

이로 인해 WebView 생성에는 일반 View와 비교할 수 없을 정도로 큰 초기화 비용이 발생합니다. 이러한 특성을 고려하지 않고 작성된 코드는 화면 진입 시 프레임 드랍을 유발하고, 앱의 전체적인 반응성을 떨어뜨리며, 사양이 낮은 디바이스에서는 메인 스레드 점유로 인한 ANR로 이어질 수 있습니다.

본 글에서는 WebView 생성 과정에서 정확히 어떤 일이 일어나는지를 알아보며, 이것이 어떤 상황에서 문제를 일으킬 수 있는지 살펴보도록 하겠습니다.


WebView는 왜 일반 View와 다를까?

안드로이드 앱에서 웹뷰를 띄울 때 우리는 흔히 다음과 같은 코드를 작성합니다.

val webView = WebView(this)
webView.loadUrl("https://example.com")

앞서 언급했듯 이 코드가 실행되면 앱은 WebView Provider(Android System WebView)를 통해 Chromium Runtime을 사용할 준비를 시작합니다. 대략적인 구조는 다음과 같습니다.

얼핏 봐도 정말 많은 무거운 모듈들이 함께 얽혀 동작하는 구조입니다. WebView() 생성자를 호출하는 순간 단순히 메모리에 UI 뷰 하나를 올리는 것이 아니라, 앱 내부에 거대한 브라우저 런타임을 통째로 초기화하는 작업을 시작합니다.

이 과정에서 발생하는 주요 비용은 크게 세 가지로 나뉩니다. 이러한 Chromium Runtime의 초기화 비용은 모두 WebView 객체를 처음 인스턴스화하는 시점에 집중됩니다.

  • WebView Provider 동적 로딩 (Shared Library Load)
    시스템에 존재하는 Android System WebView APK의 Native 라이브러리(.so)를 앱 프로세스 메모리로 공유·로드하는 과정에서 I/O 및 메모리 매핑 비용이 발생합니다.

  • Chromium Engine & C++ 객체 초기화
    Blink 렌더링 엔진, V8 JavaScript 엔진, Network Stack, GPU Compositor 등 C++ 단의 핵심 컴포넌트들을 메인 스레드 및 백그라운드 스레드에서 초기화합니다.

  • Renderer Process 생성
    보안 및 안정성을 위해 웹 콘텐츠를 실행할 별도의 렌더러 프로세스(Renderer Process)를 포크(Fork)하고 샌드박스 환경을 구축합니다.

모든 작업이 메인 스레드에서 동작하는가?

여기서 한 가지 짚어야 할 점이 있는데, 앞에서 소개한 작업들은 전부 메인(UI) 스레드에서 동작하는 것은 아니라는 것입니다.

아래 그림과 같이, 실제로 UI 스레드가 담당하는 부분은 WebView 생성자 호출부터, WebView Provider를 로드하고 Chromium의 Browser Process를 트리거하는 과정까지입니다.

WebView는 View이기 때문에 메인 스레드에서 실행되어야 하고, 이 초기화 체인 전체가 끝나야 WebView 생성자 호출이 리턴됩니다. 추후 이어질 내용에서 실제 측정으로 확인하겠지만, 바로 이 구간이 메인 스레드를 점유하는 실질적인 비용입니다.

별도 프로세스에서 실행되는 부분(파란색): Chromium은 Chrome과 동일하게 멀티프로세스 구조를 취하고 있어서, 실제 페이지를 파싱·렌더링하는 Renderer Process는 앱의 메인 프로세스와 분리되어 있습니다. Blink(렌더링)와 V8(JS 실행)은 이 Renderer Process 안에서 동작하고, GPU Compositor도 별도의 GPU 프로세스를 사용합니다.

같은 프로세스의 백그라운드 스레드(회색): Network Stack과 Cookie Manager는 Browser Process(앱과 같은 프로세스) 안에 있지만, 보통 자체 백그라운드 스레드에서 처리되어 메인 스레드를 막지 않습니다.

따라서, 이 글에서 문제로 다루는 부분은 WebView가 페이지를 렌더링하는 동작 전체가 아닙니다. 웹뷰를 생성하는 과정에서 메인 스레드에서 동기적으로 수행되는 초기화 비용을 언급하고자 하는 것입니다. 이 차이를 염두에 두면서 그 짧은 생성 코드 한 줄이 앱의 성능에 어떤 영향을 미치는지 이어서 살펴보도록 하겠습니다.


웹뷰 생성 비용

첫 생성에서 큰 비용 발생

WebView는 첫 번째 생성 시점(Cold Start)에 앞서 살펴본 대부분의 초기화 비용이 집중됩니다. 인스턴스가 한 번 생성되고 나면 핵심 런타임 및 Provider 객체가 메모리에 유지되므로, 이후에 재차 생성할 때는 재사용을 통해 확연히 빠른 생성 속도를 경험할 수 있습니다.

AOSP 소스로 보는 초기화 과정

WebView 생성자가 호출되면, 내부적으로 WebViewFactory.getProvider()가 호출됩니다. 실제 AOSP의 WebViewFactory 소스를 살펴보면 흥미로운 사실을 하나 발견할 수 있습니다. 바로 개발자들도 이 초기화 과정이 매우 무겁다는 것을 인지하고 명시적으로 측정 체계를 구축해 두었다는 점입니다.

WebViewFactory 내부에는 아래와 같이 각 단계의 시작 및 종료 시각을 상세히 기록하는 StartupTimestamps라는 별도의 클래스가 정의되어 있습니다. 코드 내부에서 getProvider()로 시작하는 호출 체인을 따라가 보면, 구간 별로 Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, ...)Trace.traceEnd(...)가 촘촘하게 감싸져 있습니다.

public static class StartupTimestamps {
    long mWebViewLoadStart;
    long mCreateContextStart;
    long mCreateContextEnd;
    long mAddAssetsStart;
    long mAddAssetsEnd;
    long mGetClassLoaderStart;
    long mGetClassLoaderEnd;
    long mNativeLoadStart;
    long mNativeLoadEnd;
    long mProviderClassForNameStart;
    long mProviderClassForNameEnd;
}

프레임워크 차원에서 이미 해당 구간을 독립적인 트레이싱 대상(Systrace/Perfetto 등)으로 지정할 만큼 중요한 성능 병목 구간으로 관리하고 있는 것입니다.

세부적인 호출 체인은 복잡하지만, 메인 스레드 관점에서 기억해야 할 사실은 WebView 생성자가 리턴을 완료하고 다음 코드로 넘어가기 전까지, 아래의 두 가지 무거운 초기화 과정이 반드시 메인 스레드에서 동기적으로 완료되어야 한다는 점입니다.

  • WebView Provider 로딩 (WebViewFactory.getProvider(), 위 다이어그램의 2번 영역)
    시스템에서 사용할 WebView 패키지(Android System WebView 또는 Chrome)를 결정하고, Native 라이브러리(libwebviewchromium.so)를 로드하여 Java-Native 간 연결점인 Provider 인스턴스를 생성하는 구간입니다.

  • Chromium Core 초기화 (AwBrowserProcess, 위 다이어그램 3번 영역의 Chromium Core)
    위에서 만들어진 Provider가 Glue Layer를 통해 C++ 영역의 AwBrowserProcess를 기동하는 구간입니다. AwBrowserProcess는 Android WebView 전용 브라우저 프로세스 런처로, 아래에 연결된 Blink, V8, Network Stack, Renderer Process 등 무거운 컴포넌트들을 제어하고 브라우저 스레드 루프를 가동합니다.

Chromium 공식 문서에서도 이 구조를 다음과 같이 명시하고 있습니다.

"WebView's browser process code runs in the app's process (we call this the "browser process")... WebView follows Chrome's architecture by separating browser and renderer code."

이 두 단계가 메인 스레드에서 완전히 끝나기 전까지 다음 코드로 넘어가지 못하며, 이것이 바로 WebView 최초 생성 시 발생하는 메인 스레드 점유와 성능 병목의 원인이 되는 것입니다.


실제 측정으로 살펴보기

우선 준수한 성능을 가진 기기(Pixel 9a)를 통해 WebView의 생성 시간을 측정해 보았습니다.

  • 프로세서: Octa-core 3.1 GHz
  • 메모리: 8GB
private fun measureWebViewCreation(): Long {
    return measureTimeMillis { // 코드 생략
        val webView = WebView(this)
    }
}

애플리케이션을 실행하고, 위 코드를 3번 연속 호출하고 시간과 메모리(Debug.MemoryInfo의 PSS)를 함께 로그로 남겨봤습니다.

iter=1 time=164ms pssBefore=124535 pssAfter=160187
iter=2 time=11ms pssBefore=160187 pssAfter=181617
iter=3 time=3ms pssBefore=181619 pssAfter=181731

앞서 설명했듯, 첫 번째 생성만 압도적으로 느림을 확인할 수 있었습니다.

만약 웹뷰 생성자 호출 자체가 무거운 연산이라면 두 번째, 세 번째 생성도 비슷한 시간이 걸려야 했을 것입니다. 이는 첫 번째 생성 시점에 WebView 객체 생성 외의 초기화 작업이 함께 수행되고 있다는 것을 의미합니다.

Perfetto Trace로 더 깊이 들여다보기

이 164ms(정확히는 164ms 489µs) 안에서 실제로 어떤 코드가 시간을 쓰고 있는지, Perfetto Trace로 직접 확인해보았습니다.

첫 번째 시도에서의 시간 분포를 살펴보면 다음과 같습니다.

여기서 두 가지의 하위 구간에 주목해야 합니다.

파란색 부분인 WebViewFactory.getProvider()는 앞서 AOSP 소스에서 봤던 Provider 클래스를 찾고 불러오는 구간입니다. 빨간색 부분인 WebViewChromium.init은 그 안에서 실제로 AwBrowserProcess.finishBrowserProcessStartup이 호출되며 Chromium 브라우저 프로세스 자체를 트리거하는 구간입니다.

핵심은 이 164ms 동안 메인 스레드가 완전한 블로킹(Blocking) 상태라는 점입니다. 이 시간 동안 사용자가 화면을 터치하거나 스크롤하더라도 해당 이벤트는 처리되지 못하고 대기열에 쌓이게 됩니다.

이번에는 구간별 메인 스레드의 상태(Thread State) 비중을 살펴보겠습니다.

여기서 Running(CPU 순수 연산) 비중이 83%를 차지한다는 점에 주목해야 합니다.

이는 메인 스레드가 막혀 있는 이유가 시스템 서비스 응답을 기다리는 IPC 대기(Sleeping)나 디스크 I/O(Uninterruptible Sleep) 때문이 아니라, CPU가 수많은 명령어를 직접 처리하느라 빠져나오지 못하고 있기 때문입니다.

  • Running: CPU 코어를 배정받아 실제로 연산(계산)을 하고 있는 시간입니다. 이 비중이 크다는 건 코드가 실행하는 명령어 자체가 많다는 뜻이고, CPU 성능이 낮은 기기일수록 같은 연산에 시간이 더 걸려서 이 시간이 그대로 늘어납니다.
  • Sleeping: 다른 스레드의 응답이나 타이머 같은 걸 그냥 기다리는 시간입니다. CPU를 쓰지는 않지만, 그렇다고 메인 스레드가 자유로워지는 것도 아닙니다.
  • Uninterruptible Sleep: 디스크에서 파일을 읽어오는 동안의 대기입니다. 이 비중이 크다는 건 스토리지(디스크 I/O) 속도가 병목이라는 뜻입니다. CPU를 더 빠른 것으로 바꿔도 이 시간은 줄지 않습니다.
  • Runnable (Preempted): 연산할 준비는 됐지만, 다른 스레드가 CPU 코어를 먼저 쓰고 있어서 순서를 기다리는 시간입니다. 이 비중이 크다는 건 CPU 코어 자체가 부족해서 다른 작업과 경쟁하고 있다는 뜻입니다.

실험에서 164ms 중 대부분(Running 83%)이 순수 CPU 연산으로 채워졌습니다. IPC 대기(Sleeping)나 디스크 I/O(Uninterruptible Sleep)로 인한 블로킹은 거의 존재하지 않았던 것입니다.

이 결과만 보면 저사양 기기에서의 생성 시간 지연은 단순히 'CPU 성능 저하로 인한 연산 지연' 때문이라는 결론을 내리기 쉽습니다. 하지만 하드웨어 스펙이 낮은 환경에서도 정말 그 원인 하나뿐일지, 이어지는 실측 데이터를 통해 직접 확인해 보겠습니다.

반면, 같은 프로세스 안에서 두 번째, 세 번째로 생성한 WebView를 보면 이야기가 달라집니다.

시간만 줄어든 것이 아니라, 호출되는 함수 자체가 다르다는 것을 알 수 있었습니다.

  • 첫 시도: 브라우저 프로세스를 처음부터 켜는 무거운 경로
    WebViewChromium.inittriggerChromiumStartupAndReturnTrue → ... → AwBrowserProcess.finishBrowserProcessStartup

  • 두 번째부터: 이미 켜져 있는 브라우저 프로세스에 새 WebView 인스턴스만 얹는 가벼운 경로
    WebViewChromium.initinitForRealAwContents.constructor

정리하면, WebView 생성 비용은 프로세스당 한 번만 내는 고정비(Provider 로딩 + 브라우저 프로세스 시작)와 매번 조금씩 드는 변동비(WebView 인스턴스별 AwContents 생성)로 나뉩니다.

저사양 디바이스와의 비교

하드웨어 스펙이 상대적으로 낮은 Galaxy A13에서 동일한 실험을 진행해 보았습니다.

  • 프로세서: Octa-core 2.0 GHz
  • 메모리: 4GB

iter=1 time=1038ms pssBefore=52740 pssAfter=62872
iter=2 time=48ms pssBefore=62876 pssAfter=63095
iter=3 time=46ms pssBefore=63091 pssAfter=63063

Pixel 9a 실측 데이터에서 메인 스레드 점유 중 CPU 연산(Running) 비중이 83%에 달했던 점을 생각해 보면, CPU 성능이 떨어진 저사양 기기에서는 생성 시간도 단순 연산 속도에 비례해서 늘어날 것이라는 추측을 하기 쉽습니다. 하지만 하드웨어 스펙이 낮은 보급형 디바이스(Galaxy A13)에서 실제로 측정을 진행해 보면, 훨씬 복잡한 요인이 있음을 알 수 있습니다.

저사양 기기에서 벌어진 현상은 단순한 CPU 연산 지연의 문제가 아니었습니다.

Pixel 9a에서 165ms였던 Cold Start 시간이 Galaxy A13에서는 1,038ms(약 1초)로 무려 6배 이상 폭증했습니다. Perfetto 스레드 상태를 복기해 보면, Pixel 9a에서 2.3%에 불과했던 디스크 I/O 대기(Uninterruptible Sleep) 비중이 20.2%까지 치솟는 기현상이 발생했습니다. 느린 스토리지(slow storage) 스펙 때문에 Native Library(.so)를 메모리로 읽어오는 동안 스레드 자체가 완전히 멈춰 서는 I/O 병목이 새로 터져 나온 것입니다.

심지어 이미 브라우저 프로세스가 초기화된 후 AwContents만 가볍게 얹는 두 번째, 세 번째 생성(Warm Start)조차 46~48ms가 소요되었습니다. Pixel 9a의 3~11ms와 비교하면 가벼운 경로마저 많이 느려진 셈입니다.

결국 저사양 기기의 WebView 생성 비용은 단순히 'CPU가 느려서 비례해 늘어난 시간' 만이 아니었습니다. 느린 스토리지로 인한 디스크 I/O 병목 등까지 동시다발적으로 터진 복합 병목의 결과였습니다. 그리고 이 모든 과부하는 여전히 앱의 메인 스레드가 오롯이 감당하고 있던 것입니다.


왜 문제가 되는가?

앞에서 살펴본 실측 결과를 정리하면, 저사양 디바이스에서 WebView 생성이 얼마나 심각해지는지가 명확히 드러납니다.

Pixel 9a에서 165ms였던 콜드 스타트가 Galaxy A13에서는 1,038ms로 6배 넘게 늘어났고, 이 병목은 아래 요인들이 복합적으로 작용한 것입니다.

  • CPU가 느려서 이에 비례해 늘어난 시간
  • 느린 스토리지로 인한 디스크 I/O 대기(Uninterruptible Sleep 2.3% → 20.2%)

추가적으로 이 두 디바이스는 같은 코어 수를 가지고 있기에 직접적인 원인은 아니지만, 코어 수가 더 적은 다비이스라면 CPU 스케줄링 경쟁으로 시간이 더 늘어날 것으로 예상됩니다.

또한 이 병목이 콜드 스타트 한 번으로 끝나지 않는다는 점도 주목할 만 합니다. 즉 저사양 기기에서는 WebView와 관련된 메인 스레드 작업 전체가 구조적으로 무거워진다는 뜻입니다.

여기에 더해, 대부분의 앱에서는 WebView만 실행되는 것이 아니라는 점도 함께 고려해야 합니다.

실제 프로젝트에서는 웹뷰를 초기화하는 시점에 여러 다른 작업들도 메인 스레드에서 함께 수행됩니다. 고사양 기기라면 각각의 비용이 상대적으로 가벼워 크게 체감되지 않지만, 저사양 기기에서는 WebView 하나만으로 이미 상당한 메인 스레드 점유가 발생한 상태에서 다른 초기화 비용까지 그대로 더해지는 셈입니다.

ANR 위험

ANR(Application Not Responding)은 메인 스레드가 일정 시간 이상 사용자 입력이나 시스템 이벤트를 처리하지 못할 때 발생합니다. 앞서 살펴본 것처럼, 생성자가 진행되는 동안은 그 내부가 Running이든 Sleeping이든 상관없이 메인 스레드가 다른 이벤트를 처리할 수 없는 시간이므로, 이 구간의 길이(Duration)가 곧 ANR 위험과 직결됩니다.

물론 WebView 생성 자체가 ANR을 발생시키는 것은 아닙니다. 다만 WebView 생성은 메인 스레드에서 수행되는 고비용 작업이고, 저사양 디바이스에서는 그 비용이 훨씬 커집니다.

앞서 측정한 A13의 첫 시도(1,038ms)를 예로 들면, Android의 입력 이벤트 처리 ANR 기준(약 5초)의 20% 이상을 WebView 생성 하나만으로 이미 써버리는 셈입니다. 여기에 Room, Firebase, 광고 SDK 초기화처럼 메인 스레드를 사용하는 다른 작업이 더해지면 남은 여유는 더 줄어들고, ANR 기준을 넘길 가능성도 그만큼 커질 것입니다.

따라서, 저사양 디바이스에서는 특히 WebView는 메인 스레드를 장시간 점유하는 데 큰 비중을 차지하는 요소라고 이해하는 것이 가장 적절합니다.

그래서 WebView를 사용하는 프로젝트에서는 "언제 생성할 것인가"가 성능 측면에서 매우 중요한 설계 요소가 되기도 합니다. 특히 저사양 사용자 비중이 높은 서비스일수록 이 설계 판단의 무게가 커집니다. 이에 관련해서는 다음 게시글에서 살펴보도록 하겠습니다.


마치며

WebView는 View를 상속받고 있지만, 일반적인 View와는 비용 구조가 크게 다릅니다. 첫 번째 WebView 생성은 단순한 객체 생성이 아니라 Chromium Runtime을 초기화하는 과정까지 포함하기 때문입니다.

이 초기화는 메인 스레드에서 수행되며, 생성 시간과 메모리 사용량 모두 일반 View보다 큰 비용을 가집니다. 따라서 저사양 디바이스에서는 이러한 비용이 더욱 크게 나타나며, 다른 초기화 작업과 겹칠 경우 앱 시작 성능 저하뿐 아니라 ANR 발생 가능성도 높아질 수 있습니다. 저사양 디바이스의 비중이 높은 서비스를 개발할 때는 이러한 점을 고려하여 개발을 진행해야 할 것입니다.


참고 자료

https://developer.android.com/reference/android/webkit/WebView https://developer.android.com/topic/performance/rendering/profile-gpu?hl=ko https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/webkit/WebViewFactory.java https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/architecture.md https://chromium.googlesource.com/chromium/src/+/master/android_webview/java/src/org/chromium/android_webview/AwBrowserProcess.java

profile
Software Engineer

0개의 댓글