[Android] WebView 활용하기 - 고급편
WebView는 말 그대로 웹 페이지를 안드로이드에서 보여주기 위한 View입니다. 주로 하이브리드 앱이나 웹상의 특정 페이지를 보여주기 위해 사용하는데요. 잘만 활용한다면 웹 페이지를 앱에 최적화시켜 보여줄 수 있는 아주 유용한 도구입니다만 사용하기가 좀 까다롭습니다. 왜냐하면 실제 서비스 운영 시에는 똑같은 페이지라도 웹에서는 보여도 되지만 앱에서는 보이면 안 되는 요소들이 있거든요. 또, 링크 클릭 시 다른 웹 페이지가 아닌 앱의 특정 화면(Activity)을 보여줘야 할 때도 있습니다. 이런 이유로 사용하기가 까다롭지만 잘만 쓴다면 웹에 구현된 페이지를 그대로 가져다가 앱에 최적화된 콘텐츠를 보여줄 수 있습니다. 이번에는 아래 작업들을 해볼 겁니다. 이 정도만 마스터해도 웹뷰로 꽤 많은 페이지들을 잘 구현할 수 있으리라 생각합니다. 꼭 끝까지 해보세요.
- WebView로 불러온 페이지의 특정 부분을 추가하거나 삭제하기
- WebView의 링크 클릭 이벤트를 가로채서 Activity 실행하기
- WebView로 불러온 페이지의 Javascript가 정상적으로 동작하게 구현하기
Jsoup 설치하기
먼저 Jsoup을 설치합니다. WebView는 주어진 링크를 통해 웹사이트의 HTML을 비롯한 여러 가지 요소들을 불러와 View에 보여줍니다.
이 화면을 마음대로 조작하려면 WebView가 보여주기 전에 커스텀 된 HTML을 넘겨주어야 하는데요. 이때 사용하는 라이브러리가 바로 JSoup입니다. HTML을 해석해서 새로운 요소를 추가하거나 기존 요소를 수정 / 삭제할 수 있습니다.
App 레벨 build.gradle에 아래와 같이 추가합니다.
dependencies{
// HTML Parser
implementation("org.jsoup:jsoup:1.14.1")
}
조작된 HTML을 WebView에 보여주기
이제 HTML을 JSoup으로 로딩한 뒤 직접 수정을 해보겠습니다.
private fun initWebView(url: String){
// WebView 초기화
val webView: WebView = binding.eventWebview
webView.apply {
// URL 후킹을 위한 Custom WebView Client 설정
webViewClient = EventWebViewClient()
// Javascript Alert Dialog가 정상동작하도록 Custom Web Chrome Client 설정
webChromeClient = EventWebChromeClient()
settings.javaScriptEnabled = true
// 여러개의 윈도우를 사용하도록 허용하면 shouldOverrideUrlLoading이 호출되지 않는다
// settings.setSupportMultipleWindows(true)
// settings.javaScriptCanOpenWindowsAutomatically = true
settings.loadWithOverviewMode = true
settings.useWideViewPort = true
settings.setSupportZoom(false)
settings.cacheMode = WebSettings.LOAD_NO_CACHE
fitsSystemWindows = true
}
// JSoup은 UI Thread에서 실행하면 에러가나므로 반드시 Background Thread에서
// 실행해야한다
Thread{
val jsoup = Jsoup.connect(url)
// jsoup의 get 메서는 Network 처리이기때문에 Main Thread에서 실행되면 안된다
val doc = jsoup.get()
// Simple Banner가 안보이도록 css를 추가한다
val removeSimpleBannerCss = "<style type=\"text/css\" media=\"screen\">" +
".simple-banner{display:none;}</style>"
val header = doc.head()
header.append(removeSimpleBannerCss)
// 필요없는 요소들을 Class 이름으로 필터링해서 제거한다
doc.getElementsByClass("elementor-location-header").remove()
doc.getElementsByClass("elementor-location-footer").remove()
// 현재 로그인이 되어있으면 user_id 값을 input 태그에 설정해준다 그렇지 않을 경우 0
run loop@{
// 특정 input 태그를 불러와서 onClick 속성값을 변경해준다
doc.select("input[type=button]").forEach {
// 이벤트 신청하는 onClick 이벤트 값에 들어가는 user id 값을 바꿔치기
// 이때 로그인 안된 user_id는 무조건 0이어야한다
val name = it.attr("onClick")
val id = "".plus(24)
val replace = name.replace("0", id)
it.attr("onClick", replace)
}
}
// 조작 HTML을 문자열로 변환한다
val data = doc.toString()
// 조작된 HTML을 Webview에 전달하기 위해서 WebView에서 제공하는 post 메서드를 활용한다
// 이때 loadData를 사용하면 정상적으로 로딩이 되지 않는다.
// 반드시 loadDataWithBaseUrl을 사용해야하며 첫번째 파라미터에는 사이트 URL이 들어간다.
// 이 Base Url 값을 반드시 넣어줘야하는데 그렇지 않으면 링크클릭이 무효처리된다
webView.post {
webView.loadDataWithBaseURL(
BuildConfig.SERVER_BASE_URL,
data,
"text/html",
"UTF-8",
null
)
}
}.start()
}
JSoup을 사용할 때 가장 먼저 주의할 점은 Thread와 관련된 문제입니다. get 메서드를 호출하면 주어진 URL으로 HTML을 가져와 분석하기 시작합니다. 이것을 Main Thread에서 했다 가는 앱이 동작을 멈추게 되니 다른 Thread(backgroud thread)에서 실행해야 하는 것이죠. 문제는 JSoup을 통해 HTML을 조작한 뒤 WebView에 넘겨줘야 수정한 HTML 화면이 보일 텐데 이것을 어떻게 할 수 있을까요?
WebView에 HTML data를 로드하는 메서드는 반드시 UI thread(main thread)에서 실행돼야 하기 때문이죠.
이를 위해 WebView에서 제공하는 post 메서드를 활용합니다. 이 메서드를 활용하면 background thread에서 Jsoup으로 수정이 끝난 HTML 데이터를 로딩에 사용할 수 있도록 전달하는 것이 가능합니다.
JSoup으로 Element를 수정할 때에는 흔히 Id나 Class 이름으로 찾습니다. 따라서 웹페이지 구조를 어느 정도 알고 있어야 하는데요.
이 부분은 Chrome에서 F12를 눌러 개발자 콘솔을 통해 분석하는 것이 좋습니다. 이것을 활용하면 수정하거나 삭제하고 싶은 Element의 Id나 class 이름을 알 수 있습니다. 이 경우, JSoup의 getElementsByClass 메서드나 getElementById 메서드를 통해 쉽게 찾아서 수정하거나 삭제할 수 있습니다.
조금 어려운 부분은 Javascript를 통해 없었던 요소가 새로 추가되는 경우인데요. 이런 경우에는 get을 통해 가져왔을 때에는 요소가 존재하지 않다가 WebView로 로딩하니 그제야 요소가 나타나게 됩니다. 이런 경우 수정이 힘들죠. 이럴 때에는 해당 Element를 찾아서 제거하는 방식이 아닌, 해당 요소의 css 값을 수정하는 방식으로 안 보이게 처리하면 됩니다. 해당 요소의 css id를 찾은 뒤 보이지 않도록 style 태그를 만드는 것이죠. 그리고 문서의 헤더에 추가해주면 후에 WebView에서 로딩된다고 하더라고 해당 요소가 보이지 않게끔 처리할 수 있습니다.
Link 이동을 Activity 실행으로 대체하기
링크 클릭 이벤트를 가로채려면 Custom WebView Client를 구현해줘야 합니다.
// Custom WebView Client
inner class EventWebViewClient: WebViewClient(){
// a href 태그나 버튼 클릭 등으로 새로운 url을 로딩할 때 로직처리
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
// 요청 URL을 분해해서 요청 타입을 알아낸다
val splices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
request?.url.toString().split("/")
} else {
// 에러처리 필요
TODO("VERSION.SDK_INT < LOLLIPOP")
}
// 제품 링크인 경우 제품 상세페이지로 이동한다
return if("product" in splices){
Toast.makeText(this@EventDetailActivity, "상품 상세페이지 이동", Toast.LENGTH_SHORT).show()
true
}
// 그 외에는 URL 이동한다
else{
// true로 설정할 경우 override된 함수의 로직을 처리하고 끝냄
// false로 설정할 경우 로직을 처리 후 해당 URL로 이동
false
}
}
// 페이지 로딩이 완료되면 실행되는 메서드
override fun onPageFinished(view: WebView?, url: String?) {
// 로딩 아이콘 없애기
setLoadingIcon(false)
super.onPageFinished(view, url)
}
}
위에서 보는 것과 같이 shouldOverrideUrlLoading 메서드를 구현하게 되면 a 태그를 클릭했을 때 이 메서드가 실행됩니다.
즉, 링크 클릭 이벤트를 가로챌 수 있는 것이죠. return 값에 따라 메서드 실행 후 동작이 달라집니다.
값이 false인 경우 로직 실행 뒤 요청한 URL로 이동합니다. 반면, true를 리턴하면 로직만 수행하고 URL로 이동은 하지 않습니다.
예를 들어보자면 이런 경우에 활용할 수 있습니다. 웹 페이지상에 상품 목록이 나타나 있다고 해보죠. 이럴 때 웹에서는 해상 상품 클릭 시 상품 페이지가 나오는 게 정상입니다. 하지만 앱에서는 어떨까요? 상품을 보여주는 화면(Activity)이 나오는 게 더 이상적이겠죠. 이렇게 같은 웹 페이지라도 앱과 웹에서 동작이 달라집니다. 이 메서드는 이럴 때 사용하면 매우 유용합니다.
Javascript Alert Dialog 처리하기
웹 페이지에서 버튼 클릭과 같은 이벤트가 발생하게 되면 대다수의 경우 Alert Dialog를 통해 사용자에게 알려주게 됩니다.
하지만 WebView에서는 이 Alert Dialog를 보여주기 위해서는 WebChromeClient를 따로 구현해줘야 합니다. 절대 WebView Client를 구현하고 WebView 설정에 javascript 사용을 하겠다고 해 줘도 Alert창이 뜨지 않습니다. Chrome Client의 콜백 함수를 통해 Alert 창이 뜨게 됩니다. Alert창이 뜰 때 아래 메서드가 호출됩니다. 여기에서 보일 메시지를 커스텀하는 등 다양한 로직을 추가할 수 있습니다.
// Custom WebChrome Client
inner class EventWebChromeClient: WebChromeClient(){
override fun onJsAlert(
view: WebView?,
url: String?,
message: String?,
result: JsResult?
): Boolean {
return super.onJsAlert(view, url, message, result)
}
}