20131119

Android 4.4 KitKat WebView 이슈 정리

KitKat 뜨고 테스트 단말 Nexus 5 샀는데 앱 WebView 가 다 깨짐. 원인은 알다시피 Chromium 기반으로 바뀐거. 4.3 까지는 Android WebKit (webkit.org 포크) 이었는데 4.4 부터 Chromium WebView 로 통째로 교체됐다. 렌더링 엔진이 바뀌었으니 깨지는 건 당연하긴 한데, 문제는 "어디가 깨지는지" 를 파악하는 게 일이다.

우리 앱은 하이브리드라 네이티브 + WebView 반반인데, WebView 쪽 화면이 전부 레이아웃 틀어지고 JS bridge 도 일부 안 됨. 일주일 걸려서 잡은 이슈들 메모.

1. User-Agent 바뀜
// 4.3
Mozilla/5.0 (Linux; U; Android 4.3; ...) AppleWebKit/534.30 ...
// 4.4
Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/KRT16M) AppleWebKit/537.36 ... Chrome/30.0.0.0

서버에서 UA 로 OS 분기하던 레거시 코드 몇 개가 오작동. "AppleWebKit/534" 를 정규식으로 잡던 부분 다시 뜯어야 했다. UA 파싱은 원래 지뢰밭인데 WebView 버전까지 바뀌니까 더 복잡해짐. 교훈: UA 문자열에 의존하는 분기는 최소화하고, 가능하면 feature detection (Modernizr 같은) 으로 가야 한다.

4.4 WebView UA 에는 "Chrome/30" 이 붙는데 이게 실제 Chrome 앱이 아니라 Chromium WebView 를 의미한다. 서버에서 "Chrome 이면 데스크톱 브라우저" 로 판단하는 로직이 있었는데 그게 모바일 앱에서도 트리거돼서 PC 버전 페이지가 내려갔다. "wv" 토큰이 UA 에 있으면 WebView 라는 규칙인데, 이건 Chrome 40+ 부터 추가된 거라 4.4 초기에는 구분할 방법이 마땅치 않다. 결국 앱 측에서 커스텀 UA suffix 를 붙여서 해결.

2. loadUrl("javascript:...") 가 void
그전엔 native 에서 js 인보크하고 return value 못 받는걸 당연히 알고 있었는데, 4.4 부터는 evaluateJavascript() 라는 새 API 가 생겼음. callback 으로 결과 받을 수 있음. 근데 4.4 미만에서는 안되니까 SDK 버전 분기 필요.

if (Build.VERSION.SDK_INT >= 19) {
    webView.evaluateJavascript("getToken()", new ValueCallback<String>() {
        public void onReceiveValue(String value) {
            // value 는 JSON 인코딩된 문자열. "null", "\"hello\"" 등
            // JSON.parse() 해야 실제 값을 얻음
            Log.d(TAG, "token: " + value);
        }
    });
} else {
    webView.loadUrl("javascript:getToken()");
}

여기서 주의할 게 evaluateJavascript 의 결과값이 JSON 형태로 온다는 것. 문자열이면 따옴표가 붙어서 온다. "hello" 가 아니라 "\"hello\"" 로 옴. null 이면 문자열 "null" 로 옴. 이거 몰라서 equals 비교 실패하는 버그를 한참 찾았다. 그리고 콜백이 UI thread 에서 불리니까 여기서 무거운 작업 하면 안 된다.

3. file:// 에서 XHR 막힘
로컬 html 에서 json 불러오던게 CORS 로 막힘. Chromium WebView 가 보안 정책을 크롬 브라우저와 동일하게 적용하기 시작한 거다. 4.3 이하에서는 file:// 끼리는 자유롭게 XHR 이 됐는데, 4.4 부터는 same-origin policy 가 엄격해짐.

// 4.4+ 에서 file:// XHR 허용하려면
if (Build.VERSION.SDK_INT >= 16) {
    webSettings.setAllowFileAccessFromFileURLs(true);
    webSettings.setAllowUniversalAccessFromFileURLs(true);
}

이렇게 하면 되긴 하는데 보안상 찝찝함. file:// 스킴으로 디바이스의 다른 파일에 접근할 수 있는 여지가 생긴다. 악의적인 HTML 이 로드되면 (예: 외부에서 다운받은 HTML) 로컬 파일을 읽어서 외부로 전송할 수 있는 취약점. 결국 assets 대신 서버에서 받아오는 쪽으로 바꿨다. 오프라인 지원이 필요한 부분은 loadDataWithBaseURL() 로 HTML 을 직접 주입하는 방식으로 전환.

4. CSS 렌더링 차이

viewport 계산이 살짝 달라져서 레이아웃 몇 개 깨짐. 구체적으로:

  • CSS position: fixed 가 4.3 에서는 WebView 전체 기준이었는데 4.4 에서는 visible viewport 기준. 키보드 올라오면 fixed 요소 위치가 달라짐
  • box-sizing: border-box 가 prefix 없이 동작하기 시작. 기존에 -webkit-box-sizing 만 적어둔 건 문제없지만, 두 가지를 섞어 쓰면서 계산이 이중으로 적용되는 케이스를 봤다
  • font rendering 이 달라져서 같은 폰트 사이즈인데 줄바꿈 위치가 바뀜. 텍스트 영역 높이가 달라지면서 레이아웃 깨지는 화면이 3~4개
  • CSS animation 에서 -webkit-transformtransform 이 동시에 있으면 4.4 에서는 transform 이 우선. 4.3 에서는 -webkit- 이 우선이었던 것과 반대

5. 디버깅 방법 — Chrome DevTools 연동

이건 4.4 의 가장 큰 장점. WebView 를 chrome://inspect 에서 직접 디버깅할 수 있게 됐다. 코드에서 WebView.setWebContentsDebuggingEnabled(true) 한 줄만 추가하면 됨. 이전에는 console.log 찍고 logcat 에서 찾던 시절인데 이건 진짜 혁명적 편의성. DOM 검사, 네트워크, JS 브레이크포인트 다 된다.

if (Build.VERSION.SDK_INT >= 19) {
    WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);
}

BuildConfig.DEBUG 로 감싸서 릴리스 빌드에서는 자동으로 꺼지게 해둠. 프로덕션에서 이게 켜져있으면 누구나 USB 연결해서 웹 콘텐츠를 들여다볼 수 있으니 보안 이슈.

전반적으로 4.4 WebView 전환은 "단기적으로 고통, 장기적으로 이득" 이다. Chromium 엔진이니 표준 호환성은 훨씬 나아지고, 성능도 JS 실행 속도가 체감될 정도로 좋아짐. 다만 기존 WebKit quirks 에 의존하던 코드는 전부 다시 봐야 한다.