[Android] 웹뷰 캐싱은 어떻게 동작하는가 (2) : Service Worker Cache와 V8 Cache

Kame·2026년 7월 12일

Android

목록 보기
17/18
post-thumbnail

들어가며

지난 글에서는 WebView가 서버 리소스를 재사용하는 HTTP 캐시와, 웹 앱이 데이터를 저장해두는 DOM Storage를 다뤘습니다.

이번 글에서는 웹뷰 로딩 최적화를 위해 적용된 기법들을 더 자세히 알아봅니다.

  • WebView 내부에서 실제로 어떤 부품들이 페이지를 그려내는지
  • 그 과정에서 끼어드는 캐시들
    • 오프라인 상황 대응 등에 사용되는 Service Worker Cache
    • 리소스 파일이 아니라 컴파일된 코드 자체를 캐시하는 V8 코드 캐시

WebView도 결국 크롬 브라우저다

WebView로 페이지를 하나 띄운다는 것은 사실상 앱 안에 작은 Chrome 브라우저 하나를 심어두고, 그걸 통해 웹 페이지를 보여주는 것과 같습니다.

Android의 WebView는 크롬에서 사용하는 브라우저 엔진인 'Chromium'을 그대로 가져다가 구현한 것이기 때문입니다.

크롬 브라우저는 Chromium 엔진 자체가 앱의 몸통입니다. 사용자가 주소창에 URL을 치면 그 엔진이 곧바로 페이지를 그립니다. WebView는 이 Chromium 엔진을 통째로 떼어와서, Android 앱의 부품 중 하나로 추가한 것으로, 결국 내부 동작은 크롬과 동일합니다.

결국 브라우저 엔진이 페이지를 화면에 띄우기까지 어떤 단계를 거치는지, 그리고 그 단계마다 어떤 캐시가 끼어드는지를 이해해야, 안드로이드 WebView의 캐싱이 어떻게 이뤄지는지도 이해할 수 있습니다.


Chromium의 핵심 구성 요소

Chromium(브라우저 엔진)이 웹 페이지 하나를 불러올 때, 크게는 세 가지 컴포넌트들이 관여하게 됩니다. 전체적인 흐름은 아래 다이어그램으로 이해해볼 수 있습니다.

이해를 위해 공식 문서 원문을 통해 Blink가 어떤 일을 하는지 살펴보겠습니다.

Blink begins the rendering process by gathering all necessary resources such as HTML, CSS, JavaScript, videos, and images. To retrieve these resources, Blink manages interactions with the network stack, in Chromium and the underlying operating system.

웹 페이지 로딩 전반을 이끄는 존재로, HTML과 CSS를 읽어 문서 구조를 브라우저가 다룰 수 있는 트리 자료구조로 바꿉니다. 짧게 말해, HTTP와 CSS를 '파싱'해 각각 DOM 트리와 CSSOM으로 변환하는 역할입니다.


네트워크 스택

Blink가 요청한 리소스(이미지, CSS, JS 등)를 실제로 서버에서 받아오는 부분입니다. HTML이나 JS의 내용 자체를 이해하거나 실행하지는 않고, 그저 파일을 가져다 Blink에게 돌려줄 뿐입니다.

참고) 리소스 요청을 가로챌 수도 있다?

사실 네트워크 스택(CDN, 오리진 서버 등)에 직접 접근하는 대신, 자바스크립트 코드 혹은 모바일 앱 코드 쪽에서 요청을 가로채 필요한 응답을 직접 만들어 제공할 수도 있습니다.

앞서 살펴본 내용을 복습해보면, HTML에서 특정 리소스(src)가 필요할 때 Blink는 다음과 같은 순서로 그 리소스를 받아옵니다.

Blink가 "이 파일 필요해"라고 요청
→ 네트워크 스택이 실제로 서버에 요청
→ 응답받은 내용을 Blink에게 전달

그런데 이 흐름 자체를 개발자가 직접 가로채서 대신 처리할 수 있는 지점이 두 군데 있습니다. 하나는 Android 코드 측, 다른 하나는 웹 페이지의 자바스크립트 측입니다.

Blink가 "이 파일 필요해"라고 요청
네트워크 스택이 실제로 서버에 요청하지 않고 WebView 혹은 JS 쪽에서 가로채 응답을 직접 생성
→ Blink에게 전달

WebViewClient.shouldInterceptRequest()

/**
 * Notify the host application of a resource request 
 * and allow the application to return the data.
 * If the return value is null, the WebView will continue to load 
 * the resource as usual. 
 * Otherwise, the return response and data will be used.
 */
@Nullable
public WebResourceResponse shouldInterceptRequest(
    WebView view, 
    WebResourceRequest request
) {
    return shouldInterceptRequest(view, request.getUrl().toString());
}

Chromium 네트워크 스택을 완전히 비켜가는 방식으로, Android 앱 코드에서 구현하는 콜백입니다.

WebView가 리소스를 요청할 때마다 이 함수가 호출되고, 여기서 직접 만든 응답WebResourceResponse를 돌려주면 네트워크 스택이 아예 동작하지 않고 그 응답이 그대로 쓰입니다. 오프라인 지원, 로컬에 미리 저장해둔 에셋 제공, 커스텀 캐싱 로직 구현 등에 자주 쓰입니다.

Service WorkerCache Storage API

shouldInterceptRequest와 같은 요지의 콜백을 웹 페이지 안의 자바스크립트가 등록하는 것입니다. 웹 표준 기술인 Service Worker가 fetch 이벤트를 가로채서, 미리 저장해둔 응답을 대신 돌려줄 수 있습니다. 자세한 내용은 이어질 내용에서 살펴봅니다.

⚠️ shouldInterceptRequest는 동작 원리를 제대로 이해하지 않고 사용하면 오히려 페이지 로딩 시간을 악화시킬 수 있습니다. 어떤 상황에 이 콜백을 쓰는 것이 적절한지 잘 판단해서 사용해야 하며, 이와 관련된 자세한 내용은 다음 글에서 다루겠습니다.


V8

JavaScript와 WebAssembly 코드를 파싱하고 실행하기 위해, Blink은 V8을 사용합니다. V8은 Chromium 프로젝트에서 개발한 오픈소스 엔진입니다.

즉 V8은 자바스크립트만 전담하는 실행 엔진입니다. Blink가 넘겨준 .js 텍스트를 컴파일해서 바이트코드로 만들고 실행한 뒤, 그 결과(DOM 조작 등)를 다시 Blink에게 돌려줍니다.

As soon as CSS and HTML is loaded, Blink can transform that code, in the form of text, into a representation it can work with. This is called parsing. JavaScript also needs to be parsed and then executed. (e.g, text -> js)

Once all that's done, Blink begins rendering. Rendering is the work of laying out and displaying web pages that you view and interact with.

Blink는 HTML, CSS을 직접 파싱하지만, JavaScript의 파싱은 V8에 위임합니다. Blink가 DOM 트리를 만드는 동안, V8은 그와는 별개로 JS 텍스트를 받아 자신만의 파싱·컴파일 과정을 거치는 것입니다.


캐싱은 어디서, 어떻게 이뤄지는가?

캐싱은 바로 이 화살표들, 즉 컴포넌트들 사이의 경계에서 이뤄집니다. 앞선 다이어그램을 다시 보면, Blink와 네트워크 스택 사이(①, ②), 그리고 Blink와 V8 사이(③④)에 각각 캐시가 자리 잡고 있는 것을 확인할 수 있습니다. 이 글에서 다룰 캐시들을 먼저 간략히 살펴보겠습니다.

HTTP 캐시

Blink가 요청한 리소스를 네트워크 스택에서 재사용하는 캐시입니다. Blink ↔ 네트워크 스택 경계(①, ②)에 자리하며, 서버가 내려준 응답 헤더(Cache-Control 등)를 기준으로 WebView가 자동으로 관리합니다.

Service Worker Cache

Service Worker가 네트워크 요청을 가로채 Cache Storage에 저장된 응답을 제공하는 방식입니다. HTTP 캐시와 마찬가지로 Blink ↔ 네트워크 스택(①, ②)에 걸쳐 있지만, WebView가 아니라 웹 페이지의 자바스크립트가 직접 관리한다는 점이 다릅니다.

V8 코드 캐시

V8이 자바스크립트를 파싱 및 컴파일한 결과를 저장해, 이후 동일한 스크립트를 다시 실행할 때 컴파일 비용을 줄여주는 캐시입니다. Blink ↔ V8 경계(③, ④)에 자리하며, 앞서 살펴본 것처럼 독자적인 저장소가 아니라 HTTP 캐시 항목에 얹혀서 저장됩니다.

참고로 지난 글에서 다룬 다른 캐싱 방식인 DOM Storage는 어떤 경계에 걸쳐있는 것이 아닌, JS 코드가 직접 만들어내는 별도의 저장 공간이라는 점을 유의해야 합니다.(안드로이드의 SharedPreferences/DataStore와 유사)

HTTP 캐시는 지난 글에서 이미 살펴보았으니, 이번 글에서는 Service Worker Cache와 V8 코드 캐시를 자세히 들여다보며 웹뷰 캐싱을 전반적으로 이해해보겠습니다.


Service Worker Cache

Service Worker란?

HTTP 캐시는 기본적으로 서버의 응답 헤더(Cache-Control)에 따라 어떻게 사용될 지가 결정되는 것입니다. 결국 이는 캐시를 사용하려면 기본적으로 서버와의 연결이 전제가 되어있어야 하며, 캐시를 쓸지 말지, 언제까지 쓸지 등을 모두 서버 측에서 결정하고 클라이언트 측에서는 그 규칙을 따를 뿐인 것입니다.

그런데 프론트엔드 개발자 입장에서는 "네트워크가 아예 끊겨도 페이지를 띄우고 싶다", "캐시 전략을 내 코드로 직접 짜고 싶다"는 요구가 있다면, HTTP 캐시만으로는 이를 구현해낼 수 없습니다. 즉, 요청 자체를 가로채서 응답을 직접 통제하고 싶다면 다른 방식이 필요한데 이것이 Service Worker를 활용하는 것입니다.

예전에는 AppCache라는 기술로 이를 풀려 했지만 폐기되었고, 그 자리를 Service Worker + Cache API가 대신하고 있습니다.

Service Worker는 특별한 파일 형식이 아니라, 웹 프론트엔드 개발자가 작성해서 서버에 올려두는 평범한 .js 파일입니다. 다만 페이지 JS가 이걸 등록(navigator.serviceWorker.register(...))하는 순간, 브라우저는 이 파일을 페이지와는 별도의 백그라운드 스레드에서, 페이지가 닫혀도 독립적인 생명주기로 실행시킵니다.

Android로 치면 액티비티가 죽어도 살아있는 백그라운드 서비스, 혹은 실제 요청 전에 끼어드는 OkHttp Interceptor에 가까운 위치입니다. 등록되고 나면 Service Worker는 브라우저가 보내는 이벤트에만 반응합니다. 캐싱과 직결되는 건 두 가지입니다.

  • install: 필요한 파일들을 미리 캐시(Cache Storage)에 저장해둠
  • fetch: 페이지가 리소스를 요청할 때마다 발생. 캐시에 있으면 그걸 반환하고, 없으면 실제 네트워크로 요청
// sw.js — 프론트엔드 개발자가 작성
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => cached || fetch(event.request))
  )
})

즉 sw.js 작성과 등록, 캐싱 전략 결정은 전부 웹 프론트엔드 코드베이스의 몫입니다. Android 개발자는 이 파일을 직접 다룰 일이 없고, WebView가 이 기능을 쓸 수 있는 환경(API 24+, ServiceWorkerController 설정)만 갖춰주면 됩니다.


V8

컴파일은 공짜가 아니다

앞서 살펴봤듯, V8은 .js 파일을 받으면 바로 실행하는 게 아니라 파싱과 컴파일을 거쳐 바이트코드를 만듭니다. 만약 웹 페이지를 열 때마다, 그리고 필요한 각 js 파일마다 이 과정을 반복한다면, 웹 로딩 시간은 늘어날 수밖에 없고, 파일이 크고 복잡할수록 그 정도가 더 심할 것입니다.

같은 페이지를 하루에도 여러 번 여는 사용자를 생각해보면, 매번 똑같은 app.js를 처음부터 다시 파싱하고 컴파일하는 것은 명백한 낭비입니다. 파일 내용이 하나도 바뀌지 않았다면, 어제 만들어둔 바이트코드를 오늘도 그대로 쓰면 되기 때문입니다.

이 때문에 V8 코드 캐시(V8 Code Cache)라는 것이 존재합니다. 한 번 컴파일한 바이트코드를 저장해뒀다가, 다음에 같은 파일을 다시 만나면 컴파일을 건너뛰고 저장해둔 바이트코드를 곧바로 재사용하는 기능입니다.

V8 캐시는 어디에 저장되는가?

DOM Storage나 IndexedDB처럼 별도의 저장 공간이 있는 것이 아니다

V8 코드 캐시는 독자적인 저장소를 갖지 않습니다. 대신 HTTP 캐시가 이미 저장해둔 디스크 캐시 항목에 얹혀서 저장됩니다.

  1. 네트워크 스택이 서버에서 app.js를 받아온다
    → 그 응답 본문이 HTTP 디스크 캐시에 저장된다 (지난 글 참고)

  2. V8이 app.js를 파싱 + 컴파일해서 바이트코드를 만든다
    → 이 바이트코드가 "1번 캐시 항목에 딸린 부가 데이터"로 함께 저장된다

  3. 다음에 같은 URL로 다시 요청이 오면
    → 네트워크 스택: 디스크 캐시에서 응답 본문을 꺼내줌
    → V8: 같은 항목에 붙어있던 바이트코드를 함께 꺼내서 재사용

이렇게 설계되어 있기 때문에, 웹뷰를 다룰 때는 아래와 같은 사항들을 고려해야 할 것입니다.

  • HTTP 디스크 캐시 항목이 애초에 만들어지지 않으면, V8 코드 캐시도 함께 존재할 수 없음
  • webView.clearCache(true)로 디스크 캐시를 지우면, 거기에 얹혀 있던 V8 코드 캐시도 함께 사라짐
  • 서버 응답의 Cache-Controlno-store라서 애초에 디스크 캐시 자체를 저장하지 않는다면, V8 코드 캐시도 만들어질 자리가 없음

정리

위치다루는 주체
HTTP 캐시Blink ↔ 네트워크 스택 경계WebView가 자동으로, Cache-Control 헤더 기준
shouldInterceptRequestBlink ↔ 네트워크 스택 경계를 통째로 대체Android 코드 (WebViewClient)
Service Worker CacheBlink ↔ 네트워크 스택 경계 (캐시 미스 시에만 네트워크 스택 통과)웹 페이지 자바스크립트
DOM Storage경계와 무관한 별도 저장 공간웹 페이지 자바스크립트
V8 코드 캐시Blink ↔ V8 경계 (HTTP 디스크 캐시 항목에 얹힘)WebView가 자동으로, 단 shouldInterceptRequest 사용 시 영향을 받음

한 가지 짚어둘 점은, HTTP 캐시와 V8 코드 캐시는 항상 세트로 움직이는 반면, Service Worker Cache와 DOM Storage는 완전히 독립적으로 웹 앱이 직접 관리한다는 것입니다.

shouldInterceptRequest는 이 지도에서 유일하게 네트워크 스택 자체를 통째로 건너뛰게 만드는 지점입니다. 이 차이를 기억해두면 다음 글에서 다룰 "네트워크 스택 자체를 건드리면 무슨 일이 벌어지는가"를 이해하기가 훨씬 쉬워집니다.


마치며

WebView는 Chromium을 통째로 앱에 심어놓은 것이며, 페이지 로딩은 Blink(지휘자)이 네트워크 스택과 V8을 필요할 때마다 불러 쓰는 구조로 이어집니다. 캐시는 이 부품들 사이의 "경계"마다 자리 잡고 있습니다.

Service Worker Cache는 웹 페이지 JS가 등록한 Service Worker가 fetch 이벤트를 가로채, Cache Storage에 저장해둔 응답을 직접 돌려주는 방식입니다. 캐시 미스일 때는 네트워크 스택을 정상적으로 거칩니다.

V8 코드 캐시는 독자적인 저장소가 아니라 HTTP 디스크 캐시 항목에 얹혀서 저장되는 컴파일 결과 캐시입니다. HTTP 캐시가 사라지면 V8 코드 캐시도 함께 사라지고, Android 개발자가 직접 켜고 끌 수 있는 기능이 아닙니다.

다음 글에서는 shouldInterceptRequest를 실제 Android 코드로 어떻게 구현하는지, 그리고 이 콜백이 왜 V8 코드 캐시를 통째로 무력화시키는지, 또 왜 잘못 쓰면 오히려 로딩 시간이 늘어나는지를 자세히 살펴보겠습니다.


참고 자료

Chrome for Developers — What is Blink?
MDN — Service Worker API
MDN — Cache
Android Developers — ServiceWorkerController
V8 Blog — Code Caching for Developers
Android Developers — WebViewClient.shouldInterceptRequest

profile
Software Engineer

0개의 댓글