WKWebView
iOS와 웹은 꽤 복잡한 관계를 맺고 있습니다. 이는 10년 전 플랫폼이 처음 생겼을 때부터 시작되었습니다.
지금 와서 아이폰 초기 디자인을 보면 정해진 순서대로 디자인했다고 생각할 수 있지만, 우리의 사랑스러운 터치스크린 기기는 한때 여러 선택지 중 하나에 불과했습니다. 물리 키보드가 달린 초기 프로토타입도 있었으며 심지어 아이팟의 클릭 휠조차도 그때는 엄청난 도전이었습니다.
그래도 저는 하드웨어가 아닌 소프트웨어를 더 발전시키기로 한 것이 초기에 했던 결정 중 가장 의미 있는 결정이었다고 생각합니다.
아이폰에서 어떻게 macOS처럼 앱을 실행할 수 있었을까요? 혹은 어떻게 사파리로 웹페이지를 띄울 수 있었을까요? iPhoneOS를 만들기로 한 것은 오늘날까지도 영향을 끼치는 광범위한 결정이었습니다.
WWDC 2007 키노트에서 스티브 잡스가 한 유명한 말들입니다.
사파리 엔진은 전부 아이폰 내부에 있습니다. 그러니 여러분은 Web 2.0과 Ajax 앱을 작성할 수 있습니다. 그것들은 아이폰의 앱과 같게 작동하고 보기에도 똑같습니다. 이 앱들은 아이폰 서비스에도 완벽하게 통합됩니다. 그들은 전화를 걸 수 있고 이메일도 보낼 수 있습니다. 게다가 구글 맵에서 나의 위치를 알아낼 수 있습니다.
요즘 아이폰이 모바일 웹에 큰 공을 세운 것과는 모순적으로 웹은 iOS에서 언제나 2급 시민이었습니다. UIWeb
는 거대하고 투박하며 체로 가루를 치는 것처럼 메모리가 누수되었습니다.
그런 이유로 모바일 사파리에 뒤처져서 더 빠른 자바스크립트와 렌더링 엔진을 가지고 있음에도 불구하고 아무런 이득을 얻지 못했습니다.
하지만 이런 아쉬운 점들도 WKWeb
와 Web
프레임워크가 나오면서 모두 바뀌었습니다.
WKWeb
는 iOS 8과 macOS Yosemite에서 소개된 현대 WebKit API의 핵심이며, UIKit의 UIWeb
와 AppKit의 Web
를 대체하고 두 플랫폼에 일관된 API를 제공합니다.
반응형 60fps 스크롤링, 내장 제스쳐, 앱과 웹페이간의 원활한 통신 그리고 사파리에서 쓰이는 것과 같은 자바스크립트 엔진을 자랑하는 WKWeb
는 WWDC 2014에서 발표한 가장 의미 있는 발표입니다.
UIWeb
와 UIWeb
로 이루어져 있던 클래스와 프로토콜은 WebKit 프레임워크에서 14개의 클래스와 3개의 프로토콜로 나누어졌습니다. 너무 복잡하다고 놀라지마세요! 이 새로운 아키텍쳐는 더 깔끔해졌으며 새로운 기능들로 가득합니다.
UIWebView / WebView에서 WKWebView로 마이그레이션하기
WKWeb
는 iOS 8 이후 선호되는 API입니다.
여러분의 앱이 여전히 기존의 UIWebView / WebView를 사용중이라면 UIWeb
와 Web
가 iOS 12와 macOS Mojave에서 공식적으로 폐지 예정(deprecated)이라는 사실을 꼭 기억하세요.
그러니 WKWeb
로 최대한 빨리 업데이트해야 합니다.
더 나은 이해를 위해 UIWeb
와 WKWeb
API를 비교해봤습니다.
UIWebView | WKWebView |
---|---|
var scroll |
var scroll |
var configuration: WKWeb |
|
var delegate: UIWeb |
var UIDelegate: WKUIDelegate? |
var navigation |
|
var back |
로딩
UIWebView | WKWebView |
---|---|
func load |
func load(_ request: URLRequest) -> WKNavigation? |
func load |
func load |
func load |
|
var estimated |
|
var has |
|
func reload() |
func reload() -> WKNavigation? |
func reload |
|
func stop |
func stop |
var request: URLRequest? { get } |
|
var URL: URL? { get } |
|
var title: String? { get } |
히스토리
UIWebView | WKWebView |
---|---|
func go |
|
func go |
func go |
func go |
func go |
var can |
var can |
var can |
var can |
var loading: Bool { get } |
var loading: Bool { get } |
자바스크립트 작업
UIWebView | WKWebView |
---|---|
func string |
|
func evaluate |
기타사항
UIWebView | WKWebView |
---|---|
var keyboard |
|
var scales |
|
var allows |
페이지네이션
WKWeb
는 페이지네이션에 해당하는 API를 지금은 가지고 있지 않습니다.
var pagination
Mode: UIWeb Pagination Mode var pagination
Breaking Mode: UIWeb Pagination Breaking Mode var page
Length: CGFloat var gap
Between Pages: CGFloat var page
Count: Int { get }
WKWeb View Configuration
을 사용해서 리팩토링하기
다음의 UIWeb
의 속성들은 독립적인 설정 객체로 나누어졌습니다. 이 설정 객체는 WKWeb
를 초기화 할 때 사용합니다.
var allows
Inline Media Playback: Bool var allows
Air Play For Media Playback: Bool var media
Types Requiring User Action For Playback: WKAudiovisual Media Types var suppresses
Incremental Rendering: Bool
자바스크립트와 스위프트 사이의 통신
UIWeb
이후의 가장 크게 바뀐 점 중 하나가 앱과 웹 컨텐츠 사이에 앞뒤로 데이터를 주고받는 방식입니다.
사용자 스크립트로 동작 끼워 넣기
WKUser
는 문서를 불러오는 시작과 끝에 자바스크립트를 끼워 넣을 수 있게 해줍니다.
이 강력한 기능은 페이지 요청에 대해 안전하고 일관된 방식으로 웹 컨텐츠를 조작할 수 있습니다.
다음은 웹 페이지의 배경 색을 바꿀 수 있는 사용자 스크립트를 끼워 넣는 방법에 대한 간단한 예제입니다.
let source = """
document.body.style.background = "#777";
"""
let user Script = WKUser Script(source: source,
injection Time: .at Document End,
for Main Frame Only: true)
let user Content Controller = WKUser Content Controller()
user Content Controller.add User Script(user Script)
let configuration = WKWeb View Configuration()
configuration.user Content Controller = user Content Controller
self.web View = WKWeb View(frame: self.view.bounds,
configuration: configuration)
WKUser
객체를 만들 때 우리는 실행할 자바스크립트를 제공했고, 문서를 로딩하는 부분의 어떤 지점에 끼워 넣을지 지정했으며, 모든 프레임에서 사용할지 메인 프레임에서만 사용할지도 지정했습니다.
그 후에 WKWeb
를 초기화할 때 WKWeb
객체를 이용해서 사용자 스크립트를 WKUser
에 추가했습니다.
이 예제는 더 의미 있는 방식으로 확장될 수 있습니다. 예를 들면 모든 “the cloud”라는 단어를 “my butt”으로 변경하는 것이 있습니다.
메세지 핸들러
메세지 핸들러 도입으로 인해 웹에서 앱으로의 통신이 상당히 개선되었습니다.
console.log
가 정보를 Safari Web Inspector로 출력하는 것처럼, 웹 페이지의 정보는 다음과 같은 방법으로 앱에 전달할 수 있습니다.
window.webkit.message Handlers.<#name#>.post Message()
이 API가 정말로 훌륭한 이유는 자바스크립트 객체들은 Objective-C나 스위프트 객체들에게 자동으로 시리얼라이즈 된다는 점입니다.
핸들러의 이름은 add(_:name)
에서 설정되고 이 메소드는 WKScript
프로토콜을 따르는 핸들러를 등록할 수 있습니다.
class Notification Script Message Handler: NSObject, WKScript Message Handler {
func user Content Controller(_ user Content Controller: WKUser Content Controller,
did Receive message: WKScript Message)
{
print(message.body)
}
}
let user Content Controller = WKUser Content Controller()
let handler = Notification Script Message Handler()
user Content Controller.add(handler, name: "notification")
이제 앱에 객체가 생성됐을 경우와 같이 알림이 왔을 때, 다음과 같은 식으로 정보를 받을 수 있습니다.
window.webkit.message Handlers.notification.post Message({ body: "..." });
웹 페이지 이벤트를 위한 훅을 만드는 사용자 스크립트를 추가합니다. 여기선 메세지 핸들러로 앱과 통신을 합니다.
같은 접근법으로 페이지에서 정보를 모아서 앱에서 표시하거나 분석할 수 있습니다.
예를 들면 만약 여러분이 NSHipster.co.kr 전용 브라우저를 만들고 싶다면 관련된 기사를 나열한 버튼을 만들 수도 있을 것입니다.
// document.location.href == "https://nshipster.co.kr/wkwebview"
const show Related Articles = () => {
let related = [];
const elements = document.query Selector All("#related a");
for (const a of elements) {
related.push({ href: a.href, title: a.title });
}
window.webkit.message Handlers.related.post Message({ articles: related });
};
let js = "show Related Articles();"
self.web View?.evaluate Java Script(js) { (_, error) in
print(error)
}
// 미리 등록된 메세지 핸들러에서 결과를 받습니다
컨텐츠 차단 규칙
어떻게 사용하는지에 따라 자바스크립트와의 왕복 통신의 번거로움을 피할 수 있습니다.
iOS 11과 macOS High Sierra부터 Safari Content Blocker app extension처럼 WKWeb
를 위한 컨텐츠 차단 규칙을 구체적으로 선언할 수 있게 되었습니다.
예를 들어 웹 뷰에 Medium Readable Again을 만들고 싶으면 다음과 같이 JSON으로 정의할 수 있습니다.
let json = """
[
{
"trigger": {
"if-domain": "*.medium.com"
},
"action": {
"type": "css-display-none",
"selector": ".overlay"
}
}
]
"""
이 규칙을 compile
에 전달하고 컴플리션 핸들러의 결과로 오는 컨텐츠 규칙 목록으로 웹 뷰를 설정해주세요.
WKContent Rule List Store.default()
.compile Content Rule List(for Identifier: "Content Blocking Rules",
encoded Content Rule List: json)
{ (content Rule List, error) in
guard let content Rule List = content Rule List,
error == nil else {
return
}
let configuration = WKWeb View Configuration()
configuration.user Content Controller.add(content Rule List)
self.web View = WKWeb View(frame: self.view.bounds,
configuration: configuration)
}
규칙을 선언적으로 선언함으로써 WebKit은 같은 작업을 자바스크립트를 삽입한 경우보다 더 효율적으로 실행할 수 있는 바이트코드로 작업을 컴파일 할 수 있습니다.
페이지 요소를 숨기는 것 외에도 컨텐츠 차단 규칙을 사용하여 페이지 자원이 로드되는 것을 방지하고 (이미지 또는 스크립트와 같이) 쿠키가 서버에 대한 요청에서 분리되고 페이지가 HTTPS를 통해 안전하게 불러오도록 할 수 있습니다.
스냅샷
iOS 11과 macOS High Sierra에서 시작해서 WebKit 프레임워크는 웹 페이지의 스크린샷을 찍는 내장 API를 제공합니다.
로딩이 끝난 이후에 화면에 보이는 뷰포트를 찍으려면 web
델리게이트 메소드를 구현하여 take
메소드를 다음과 같이 호출하면 됩니다.
func web View(_ web View: WKWeb View,
did Finish navigation: WKNavigation!)
{
var snapshot Configuration = WKSnapshot Configuration()
snapshot Configuration.snapshot Width = 1440
web View.take Snapshot(with: snapshot Configuration) { (image, error) in
guard let image = image,
error == nil else {
return
}
// ...
}
}
예전엔 웹 페이지의 스크린샷을 찍는 것은 뷰 레이어와 그래픽 컨텍스트를 망친다는 것을 의미했습니다. 반대로 바뀐 API는 깨끗하고 하나의 메소드 옵션을 가지고 있기 때문에 환영받고 있습니다.
WKWeb
는 웹을 진정한 일등 시민으로 만들어주었습니다.
그 어떤 네이티브 순정 주의자라도 WebKit이 제공하는 강력함과 유연성을 보면 놀랄 수밖에 없을겁니다.
실제로 우리가 사용하는 많은 수의 앱들은 까다로운 컨텐츠를 렌더링하기 위해 WebKit에 의존하고 있습니다. 앱 개발에도 모범 사례가 존재하는 것처럼 웹 뷰의 모범 사례는 사용자가 알아채지 못하고 사용할 수 있는지가 지표가 될 것입니다.