[Android] 웹뷰 캐싱은 어떻게 동작하는가

Kame·7일 전

Android

목록 보기
16/16

들어가며

WebView의 캐시를 이해하기 위해서는 웹에서 캐시가 어떻게 동작하는지 이해해야 합니다.

이 글에서는 WebView가 내부적으로 어떤 종류의 캐시를 갖고 있고, 각각이 어떻게 동작하는지를 정리합니다.


선행 지식

HTTP 요청과 응답

안드로이드 앱에서 Retrofit으로 서버를 호출할 때를 떠올려보겠습니다. 앱이 서버에 데이터를 요청하면, 서버는 JSON과 함께 여러 가지 부가 정보를 응답 헤더에 담아서 보냅니다.

웹 브라우저에서도 동일하게(사실 여기가 원조), 브라우저가 https://kame.com/app.js를 요청하면, 서버는 JS 파일 내용과 함께 응답 헤더를 보냅니다.

이 헤더 안에는 "이 리소스를 어떻게 캐시하고 재사용해야 하는지"에 대한 정책이 담겨 있습니다.

  • 유효 기간이 얼마인지
  • 브라우저만 캐시할 수 있는지
  • 중간 서버도 캐시할 수 있는지
  • 매 요청마다 서버에 확인이 필요한지 등

각 항목의 자세한 내용은 이어질 내용에서 살펴보도록 하겠습니다.

CDN

CDN(Content Delivery Network)은 브라우저와 원본 서버 사이에 위치하는 중간 서버들의 네트워크입니다. 전 세계 여러 지역에 서버를 분산 배치해두고, 사용자가 리소스를 요청하면 원본 서버 대신 가장 가까운 CDN 서버가 응답합니다.

예를 들어 서울에 있는 사용자가 미국에 위치한 서버의 app.js를 요청할 때, CDN이 없다면 매번 미국 서버까지 왕복해야 합니다. CDN이 있다면 한국에 위치한 CDN 서버가 이미 저장해둔 파일을 대신 전달합니다.

CDN 없음: 브라우저 → (태평양 횡단) → 미국 서버
CDN 있음: 브라우저 → 한국 CDN 서버 (이미 파일 보유)

Origin

웹에서 자주 등장하는 개념입니다. Origin은 프로토콜 + 도메인 + 포트의 조합입니다.

https://example.com:443   → 하나의 origin
https://api.example.com   → 다른 origin (서브도메인이 달라도 다름)
http://example.com        → 다른 origin (프로토콜이 달라도 다름)

웹 스토리지(localStorage 등)가 특정 origin의 데이터를 격리하여 저장한다는 말은, 도메인이 다르면 서로의 데이터에 접근할 수 없다는 의미입니다. 네이티브 앱에서 패키지명으로 앱을 격리하는 것과 비슷한 개념입니다.


WebView 캐시의 종류

WebView의 캐시는 크게 두 가지로 나뉘며, 두 종류의 캐시는 전혀 다른 목적, 제어 방법, 생명주기를 가지고 있습니다.

  • HTTP 캐시: 브라우저 측에서 관리하는 캐시
  • DOM Storage (Web Storage): 웹 앱이 직접 관리하는 캐시

HTTP 캐시

HTTP 캐시는 서버로부터 받은 리소스 파일(HTML, JS, CSS, 이미지 등)을 다음 요청 시 재사용하는 메커니즘입니다. 웹뷰를 사용한다면, 기본적으로 필요한 리소스들을 기기에 저장해두고 재사용할 수 있게 됩니다.

굳이 비유하자면, Retrofit으로 서버에서 이미지 URL을 받아와 Glide로 표시할 때, Glide가 내부적으로 이미지를 캐시해두는 것과 유사합니다.

브라우저: "app.js 파일 주세요"
서버:     app.js 내용 + "이 파일은 1시간 동안 캐시해도 됩니다" (헤더)
브라우저: 파일 저장 → 1시간 이내 재요청 시 서버에 묻지 않고 로컬 파일 사용

Cache-Control 헤더

캐시 정책을 담는 대표적인 응답 헤더입니다. 서버가 이 헤더를 응답에 포함시키면, 브라우저는 그 지시에 따라 캐시를 다룹니다.

HTTP/1.1 200 OK
Content-Type: application/javascript
Cache-Control: max-age=3600

max-age=3600은 "3600초(1시간) 동안 이 파일을 캐시해도 된다"는 의미입니다. 이 시간이 지나기 전까지 브라우저는 서버에 요청을 보내지 않고 저장된 파일을 그대로 사용합니다.

자주 보이는 값들을 정리하면 이렇습니다.

의미
max-age=NN초 동안 캐시 유효
no-cache캐시는 저장하되, 매 요청마다 서버에 유효성 확인
no-store캐시 자체를 저장하지 않음
public브라우저 외 CDN 같은 중간 서버도 캐시 가능
private브라우저만 캐시 가능, 중간 서버는 저장 불가

no-cache 는 이름 때문에 오해하기 쉽지만, 캐시하지 않는다는 의미가 아닙니다. 아예 저장 자체를 하지 않으려면 no-store를 써야 합니다. 저장은 하되, 사용하기 전에 항상 서버에 캐시가 아직 유효한지를 확인하는 것입니다. 서버가 아직 유효하다고 하면 저장된 파일을 쓰고, 바뀌었다고 하면 새로 받습니다.

public / private 은 CDN 같은 중간 서버에게 캐시를 허용할지 말지를 지정합니다. Cache-Control의 public / private 값이 이 CDN과 직접 연관됩니다. 서버가 public을 내려주면 CDN도 해당 파일을 저장하고 재사용할 수 있고, private을 내려주면 CDN은 저장하지 않고 브라우저만 저장합니다.

public은 JS 번들, 이미지처럼 모든 사용자에게 동일한 파일에 적합합니다. 반면 로그인한 사용자의 개인정보가 담긴 응답에 public을 쓰면, CDN이 이를 저장했다가 다른 사용자에게 전달하는 심각한 문제가 생길 수 있습니다. 이런 경우에는 private을 써서 브라우저만 저장하도록 제한해야 합니다.

ETag와 Last-Modified

캐시 유효성 확인을 위한 수단

max-age가 만료되면 브라우저는 서버에 다시 요청을 보냅니다. 이때 파일이 변경되지 않았다면 같은 파일을 다시 전체 다운로드하는 것은 낭비입니다. 이를 최적화하기 위한 장치가 ETagLast-Modified입니다.

ETag

서버는 응답에 ETag를 포함시킬 수 있는데, 이는 일종의 해시값처럼 생각할 수 있습니다.

HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600

캐시가 만료되면, 브라우저는 이전에 받아둔 ETag를 If-None-Match 헤더에 담아 서버에 보냅니다.

GET /app.js
If-None-Match: "abc123"

서버는 현재 파일의 ETag와 비교합니다.

  • 같으면: 304 Not Modified 반환 (본문 없음, 헤더만 전송)
  • 다르면: 200 OK와 함께 새 파일 전송

파일이 바뀌지 않았다면 본문을 전송하지 않으니 네트워크 비용이 크게 줄어듭니다.

Last-Modified

ETag와 원리는 같지만, 해시값 대신 파일의 마지막 수정 시각을 기준으로 합니다.

HTTP/1.1 200 OK
Last-Modified: Mon, 01 Jan 2025 00:00:00 GMT
Cache-Control: max-age=3600

재요청 시 브라우저는 If-Modified-Since 헤더에 이 값을 담아 보냅니다. 서버는 타임스탬프를 비교해 변경 여부를 판단합니다.

ETag와 Last-Modified가 둘 다 있을 경우, ETag가 우선합니다. 일반적으로 서버는 두 헤더를 함께 내려주는 것이 권장됩니다.

Android에서 어떻게 제어하는가

HTTP 캐시는 WebView가 서버와 통신하는 과정에서 자동으로 생성됩니다. 개발자가 "이 파일을 캐시해라"고 명시적으로 지시하지 않아도, 서버 응답 헤더가 올바르다면 WebView가 알아서 캐시합니다.

Android 코드에서 제어할 수 있는 것은 "캐시를 어떤 방식으로 사용할 것인가" 입니다.

webView.settings.cacheMode = WebSettings.LOAD_DEFAULT

캐시 모드는 네 가지입니다.

LOAD_DEFAULT 는 서버의 Cache-Control 헤더를 그대로 따릅니다. 서버가 max-age=3600을 내려줬다면 1시간 동안은 서버에 묻지 않고 로컬 캐시를 사용하고, no-cache를 내려줬다면 매번 서버에 유효성을 확인합니다. 별다른 이유가 없다면 이 모드를 유지하는 것이 적합합니다.

LOAD_CACHE_ELSE_NETWORK 는 서버의 캐시 정책을 무시하고, 로컬에 저장된 파일이 있으면 무조건 사용합니다. 서버가 no-cache를 내려줘도 로컬 파일을 그냥 씁니다. 네트워크 요청을 최소화할 수 있지만, 서버에서 파일이 바뀌어도 사용자가 이전 버전을 계속 보게 될 수 있습니다.

LOAD_NO_CACHE 는 로컬에 캐시 파일이 있어도 무시하고 항상 서버에서 새로 받아옵니다. 항상 최신 파일을 보장하지만, 매번 전체 리소스를 다운로드하므로 로딩이 느려집니다.

LOAD_CACHE_ONLY 는 네트워크 요청을 아예 하지 않고 로컬 캐시만 사용합니다. 캐시에 없는 리소스는 로드에 실패합니다. 완전한 오프라인 모드가 필요할 때 사용합니다.

한 가지 오해하기 쉬운 점이 있습니다. LOAD_NO_CACHE로 설정해도 캐시 파일 자체는 생성됩니다. 단지 "기존 캐시를 사용하지 않겠다"는 것이지, 파일을 디스크에 저장하는 행위 자체를 막는 게 아닙니다.

캐시를 완전히 지우고 싶다면 별도로 호출해야 합니다.

webView.clearCache(true) // true: 디스크 캐시까지 삭제

DOM Storage (Web Storage)

DOM Storage는 HTTP 캐시와 전혀 다른 개념입니다. 리소스 파일을 캐시하는 것이 아니라, 웹 앱이 필요한 데이터를 브라우저에 key-value 형태로 저장하는 수단입니다.

안드로이드의 SharedPreferences와 매우 유사합니다. 앱이 설정값, 사용자 정보 등을 기기에 저장해두는 것처럼, 웹 앱도 브라우저에 데이터를 저장해둘 수 있습니다.

// 웹 앱 JS 코드에서 사용하는 방식
localStorage.setItem("theme", "dark")
const theme = localStorage.getItem("theme") // "dark"

sessionStorage vs localStorage

DOM Storage에는 두 종류가 있습니다.

sessionStorage는 탭 단위로 격리됩니다. WebView를 닫으면 데이터가 사라집니다. Activity가 종료되면 사라지는 onSaveInstanceState의 데이터와 비슷한 생명주기입니다.

localStorage는 origin 단위로 격리됩니다. 앱을 완전히 종료하고 다시 켜도 데이터가 유지됩니다. SharedPreferences처럼 명시적으로 삭제하기 전까지 남아있습니다.

sessionStoragelocalStorage
격리 단위origin + 탭origin
생명주기탭(WebView) 닫히면 삭제명시적 삭제 전까지 유지
유사한 Android 개념onSaveInstanceStateSharedPreferences

Android 개발자 입장에서 DOM Storage란

DOM Storage는 Android 개발자가 직접 사용하는 것이 아닙니다. 웹 앱(JS 코드)이 사용하는 것이고, Android 개발자는 그것이 동작할 수 있도록 활성화만 해주면 됩니다.

웹(데이터 저장) :    localStorage.setItem("token", "abc")
Android(동작 허용): webView.settings.domStorageEnabled = true

웹 앱이 localStorage를 쓰고 있는데 Android 개발자가 이 설정을 빠뜨리면, 웹 앱 입장에서는 localStorage가 동작하지 않는 환경에서 실행되는 것이기 때문에 오류가 발생하거나 기능이 조용히 망가집니다. 예를 들어 웹 앱이 로그인 토큰을 localStorage에 저장하는 구조라면, 이 설정이 꺼진 상태에서는 로그인이 유지되지 않는 증상이 나타납니다.

정리하면, DOM Storage는 "내가 쓰는 것"이 아니라 "웹 앱이 쓸 수 있도록 열어줘야 하는 것"입니다.

Android에서 활성화 및 삭제

webView.settings.domStorageEnabled = true

데이터를 삭제해야 할 때는 다음과 같이 할 수 있습니다.

webView.evaluateJavascript("window.localStorage.clear();", null)
webView.evaluateJavascript("window.sessionStorage.clear();", null)

clearCache(true)는 HTTP 캐시만 지웁니다. DOM Storage 데이터는 위 방법으로 별도로 삭제해야 합니다.


AppCache(Deprecated)

AppCache는 웹 앱이 오프라인에서도 동작할 수 있도록, 개발자가 캐시할 리소스 목록을 직접 지정하는 기능이 존재하곤 했습니다.

Android API level 21(Android 5.0 Lollipop)에서 deprecated되었고, 현재 WebView에서는 사실상 무시됩니다.


비교

HTTP 캐시DOM Storage
목적리소스 파일(JS, CSS 등) 재사용앱 데이터(설정, 상태 등) 저장
저장 단위파일 단위key-value
제어 주체서버 (응답 헤더)웹 앱 (JS 코드)
Android 설정setCacheMode()domStorageEnabled = true
생명주기서버 헤더 정책에 따름종류에 따라 탭 or 영구
유사한 Android 개념Glide 이미지 캐시SharedPreferences

마치며

  • WebView의 캐시는 크게 HTTP 캐시(리소스 파일 재사용)와 DOM Storage(데이터 저장)로 나뉩니다.
  • HTTP 캐시는 서버의 응답 헤더(Cache-Control, ETag, Last-Modified)가 정책을 결정하며, Android 코드에서는 setCacheMode()로 사용 방식만 제어할 수 있습니다.
  • DOM Storage는 웹 앱이 JS에서 직접 제어하는 key-value 저장소로, WebView에서 기본 비활성화되어 있기 때문에 필요하다면 domStorageEnabled = true를 명시해야 합니다.

이 두 가지 중 로딩 성능에 직접적인 영향을 주는 것은 HTTP 캐시입니다. 다음 글에서는 이 HTTP 캐시가 JS 실행 성능과 어떻게 연결되는지, V8 Code Cache를 통해 살펴보겠습니다.


참고 자료

profile
Software Engineer

0개의 댓글