새로운 프로젝트를 진행하면서 웹뷰를 사용하게 되었다. 기존에 내가 경험해 봤던 웹뷰는 간단하게 화면을 띄워주는 것과 웹뷰를 이용한 패스 인증만 해본 경험이 있어서 생소한 개념으로 다가왔다.
프로젝트가 대부분 웹뷰로 되어있어서 꼭 네이티브가 필요한 작업들(카메라, 알림 등..)을 제외하곤 모두 웹뷰로 되어있었기 때문에 해야 할 작업이 적었음에는 확실했지만 간단하지만 처음 경험해 보는 웹뷰 연동에 많은 시행착오가 있었다.
먼저 제목으로 언급한 WKWebView 말고 UIWebView, SFSafariView와 같은 종류의 웹뷰가 있다.
1. UIWebView
iOS 2.0에 출시된 굉장히 오래된 웹뷰이다. 현재 deprecate 되었으므로 UIWebView는 다른 방식으로 대체하여 사용한다.
2. WKWebView
iOS 8.0에 출시되었다.
WebKit을 import 하여 사용하고, 좀 더 customize 하게 웹뷰를 사용할 수 있다.
UIWebView와 WKWebView를 비교하면 여러가지 차이점이 있지만 간단히 비교하자면 WKWebView가 성능이 훨씬 좋다.
자세한 UIWebView와 WKWebView의 차이는 아래 링크를 참고하자!
https://support.kioskgroup.com/article/840-wkwebview-supported-features-known-issues
+) WKWebView에서 WK가 뭘까 생각해 봤는데 아마 WebKit이 아닐까..?
3. SFSafariView
iOS 9.0에 출시되었다. URL이 노출되기 때문에 이를 고려하여 사용해야 하고, 이 URL은 수동으로 수정은 불가능하다. 웹 페이지 내의 버튼 클릭으로 인한 페이지 전환은 가능하다. 기본적으로 제공하는 버튼들이 있기 때문에 굳이 뒤로 가기, 닫기 버튼 등을 만들지 않아도 된다는 장점이 있다. (인스타그램에서 많이 본 웹페이지인 것 같다.)
+) Safari 앱 열기
Safari 앱을 직접 열어서 웹 페이지를 로딩한다. 매우 간단하지만 이벤트 처리나 앱과의 상호작용이 불가능하기 때문에 단순히 사파리 앱을 통해 웹페이지를 로딩하고 싶을 때 사용한다. 그리고 굳이 앱을 찾아서 다시 돌아가야 하기 때문에 좀 번거롭지 않을까..?
UIWebView만 가지고 있는 장점이 iOS 8이하에서 지원하는 게 유일하다면.. 더 이상 사용할 이유가 없어진다.
여기서는 내가 사용한 WKWebView를 공부해 보고자 한다.!!
애플 공식 문서에도 나와있는 개념이다. in-app browser와 같은 interactive 웹 콘텐츠를 표시하는 객체이다.
그니까 인앱 브라우저와 같이 웹 콘텐츠를 보여주는 객체라는 의미이다.
먼저 WkWebView를 사용하기 위해 다음과 같이 코드를 작성한다.
// iOS
private var webView: WKWebView!
웹뷰를 선언한다.
// iOS
private func setWebView() {
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true // 자바스크립트가 새 창을 열 수 있는지 확인
let contentController = WKUserContentController() // 자바스크립트 코드와 연결 설정
contentController.add(self, name: "testId") // 자바스크립트 코드에서 호출할 수 있는 메시지 핸들러 추가 (여기서는 아이디가 testId)
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.userContentController = contentController
// 버전에 따른 자바스크립트 허용 여부
if #available(iOS 14.0, *) {
configuration.defaultWebpagePreferences.allowsContentJavaScript = true
} else {
configuration.preferences.javaScriptEnabled = true
}
webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = self
loadURL()
}
이후 설정한 웹뷰를 뷰 컨트롤러에 추가시키는 작업을 해준다.
// iOS
private func loadURL() {
let urlString = "www.google.com"
guard let url = URL(string: urlString) else { return }
let request = URLRequest(url: url)
webView.load(request)
}
내가 로드할 페이지를 로드하는 코드를 작성한다.
여기까지가 기본으로 셋팅해 주는 부분이다.
내가 이번에 진행한 프로젝트는 웹뷰와 네이티브를 연결해서 사용하는 하이브리드 앱이었기 때문에 웹 -> 네이티브, 네이티브 -> 웹 간의 통신이 필요했다.
1. 웹 -> 네이티브
위 코드대로 수행하면 웹이 띄워져있는 상태이다. 여기서 웹에서 어떤 버튼을 클릭하여 이벤트가 발생했을 때 앱으로 돌아가 앱 내의 화면을 띄워주어야 한다면, 여러 가지 방법이 있겠지만 나는 인터페이스 통신으로 진행됐다.
웹에서 어떤 버튼을 클릭해 이벤트가 발생했을 때, JS 함수가 작동하면서 앱 내로 messageHandler의 이름(이 포스팅에서는 "testId")을 보내주면, 앱에서는 웹과 조율하여 결정한 이름에 해당하는 동작을 실행하면 된다.
// javaScript(Web)
function login() {
try {
var message = {
"action": "login"
};
webkit.messageHandlers.testId.callBack(message);
} catch(error) {
alert(error)
}
}
위는 js 코드로, 웹에서 어떤 버튼을 누르면 위와 같은 js 함수가 실행되도록 짜여 있을 것이다.
message라는 변수에 "action": "login"이라는 데이터를 보내준다.
이 데이터는 webkit.messageHandlers.testId.callBack(message); 함수를 통해 앱 내로 전달된다.
그럼 앱에서는 "action": "login"이라는 json에서 "login"을 파싱 하여 원하는 작업을 수행하면 된다.
messageHandlers는 js에서 iOS 네이티브로 인터페이스 요청을 보낼 때 사용하는 함수이며, testId는 조율하여 결정한 인터페이스 이름이다.
// javaScript(Web)
webkit.messageHandlers.testId(조율하여 결정한 인터페이스 명).callBack.message(웹에서 앱으로 보낼 데이터)
즉, 웹에서는 위와 같이 코드를 작성하여 실행한다.
이제 iOS 네이티브에서 인터페이스 요청을 받기 위해 다음과 같이 설정한다.
// iOS
extension WebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard message.name == "testId",
let dictionary = message.body as? [String: Any],
let action = dictionary["action"] as? String else { return }
switch action {
case "login":
// 로그인 로직 수행
break
default:
break
}
}
}
해당 함수는 웹으로부터 인터페이스 요청이 오면 동작하는 함수이다.
message.name에는 js에서 작성한 webkit.messageHandlers.testId.callBack(message); 에서 testId에 해당하는 값이 온다.
message.name이 일치하면, message.body를 확인한다.
여기서는 webkit.messageHandlers.testId.callBack(message);에서 message에 해당하는 값이 온다.
message는 json 데이터이기 때문에 변환하여 딕셔너리에 저장하여 switch-case문을 통해 로직을 수행하면 된다.
message.name -> testId(웹과 조율한 인터페이스 이름)
message.body -> message(웹에서 보내준 json String 데이터)
2. 네이티브 -> 웹
네이티브에서 작업을 수행한 후 다시 웹뷰로 돌아갈 때, 처음 앱을 시작하고 웹뷰의 url을 로드해 준 것과 동일하게 돌아가면 된다.
프로젝트를 진행하면서 네이티브에서 수행한 작업에 대해 웹뷰에 데이터를 보내주어야 할 상황이 생겼다. 이전에 나는 항상 URL Session이나 Alamofire를 통해 서버에 데이터를 전송하는 것만 경험해 봤는데, 웹뷰에 데이터를 보내준다는 개념이 나에게 좀 생소했다.
우선 웹 개발자가 자바스크립트를 호출하여 데이터를 전송하는 방법을 원했기 때문에 아래와 같이 코드를 짜주었다.
자바스크립트를 호출한다는 건 웹 개발자가 미리 웹에서 작성해 둔 함수를 앱에서 동작시키는 것이다. 이때 파라미터에 앱에서 웹으로 보낼 데이터를 담아 실행시킨다.
// iOS
// 앱에서 웹으로 보낼 데이터
let data = ["image": myImage,
"userId": userId] as [String : Any]
do {
let jsonData = try JSONSerialization.data(withJSONObject: data, options: [])
if let jsonString = String(data: jsonData, encoding: .utf8) {
webView.evaluateJavaScript("javascript:callBackData('\(jsonString)');") { (result, error) in
if let error {
print("에러")
}
}
}
} catch {
print("에러")
}
보내주어야 하는 데이터를 딕셔너리 형태로 변수에 저장한 후 json으로 변환한 후 webView.evaluateJavaScript() 함수를 사용하여 웹의 함수를 앱에서 동작하게 한다.
인자 값으로 전달되는 string을 살펴보면
- [javascript:]
자바스크립트를 동작하겠다는 키워드인 것 같다. 찾아보니 다들 동일하게 붙여주는 걸로 봐서 정해진 규약인듯하다. 간혹 안 붙이고 보내는 경우가 있다고 하는데 되도록이면 붙이는 것을 지향하는 것 같다. - [callBackData]
웹 개발자가 작성해둔 함수의 이름이다. 웹 개발자가 어떤 이름으로 함수 이름을 설정했는지 알려주어야 js를 동작시킬 수 있기 때문에 만약 모른다면 웹개발자에게 어떤 이름의 함수인지 물어보아야 한다.
나는 프로젝트 특성상 웹 개발자의 직접적인 커뮤니케이션이 어려웠던 상황이라 개발자 도구를 활발히 이용했다. 개발자 도구를 통해 코드를 분석하여 동작시킬 함수를 확인했다. (웹 개발자에게 질문하려면 거쳐야 하는 중간 인원이 좀 많았다.ㅠㅠ) - 잘 작성했다고 생각되는데 안된다면 jsonString 앞뒤에 ' 작은따옴표를 붙였는지 확인해 보자.
나는 이것 때문에 시간을 많이 날렸다 ㅠㅠ
만약 웹 개발자가 작성해둔 함수의 이름을 모르는데 알기 어려운 상황이라면 개발자 도구를 통해 알 수도 있다. 내가 진행한 프로젝트에서는 그렇게 엄청난.. 프로젝트까지는 아니였어서 확인이 가능했는데 보안이 높은 프로젝트에서도 확인할 수 있을지는 모르겠다.. :)
기존에는 항상 크롬 브라우저 개발자 도구를 통해 디버깅했었는데 이번에 사파리를 통해 디버깅해보니 사파리가 좀 더 편한 것 같았다ㅎㅎ
다행히 웹 개발자가 작성한 코드를 개발자 도구로 확인할 수 있었기 때문에 사진과 방식으로 검색하면서 웹에서 코드를 어떻게 짰는지 확인하며 진행했다.
위 사진처럼 이거일 것 같다! 하는 코드를 검색해서 내가 해야 할 동작과 비슷해 보이는 부분의 함수를 어찌저찌 찾았다. (예를들면 카메라 촬영 후 웹뷰로 돌아간다면 camera, photo등 검색해봄!)
사진처럼 웹 개발자가 작성한 코드를 검색할 수 있었기 때문에 궁금한 키워드를 검색하고 break 포인트를 찍어보면서 어떻게 작동하는지 확인했다.
이렇게까지 한 이유는 중간 다른 사람을 거쳐서 전달되다 보니 커뮤니케이션에 문제가 있었고, 중간에 의견 전달이 잘 안돼서 개발이 안 되어있는데 되는지 확인해달라는 둥 답답한 상황이 많이 발생했었다. 흑흑
어쩔 수 없는 상황이긴 했지만 웹 공부를 깊게 해본 경험이 없어 웹에 대한 이해가 부족함을 느꼈다. 기회를 봐서 웹 공부도 원활한 커뮤니케이션을 위해 조금은 공부 해 보는 것도 좋겠다는 생각이 들었다. 그래도,, 뭐가 부족한지 모르는 것보다 부족한 부분을 알게 되어 긍정적으로 생각한다.
어쨌든 저런 식으로 검색해 웹에서 작성한 함수를 확인하고
콘솔에 함수를 직접 작성해 보면서 어떤 식으로 값을 넘겨줘야 제대로 실행되는지 확인하는 과정을 거쳤다.
나는 제대로 작성했다고 생각했는데 잘 안돼서 콘솔에 직접 내가 원하는 작업을 입력해서 작동하는 코드를 확인했다.
'작은따옴표를 안 붙여서 자꾸 안됐었는데 이 글은 보시는 분들은.. 꼭 붙이세요 ㅎㅎ
js를 한지가 너무 오래돼서 "과 '의 차이 이런 것도 다 생각이 잘 안 나서 좀 헤맸던 것 같다 ㅠㅠ.. 그래도 나중에는 안 까먹겠지...~
기존에 테스트가 잘 안되어서 개발자 도구로 확인했었던 건데, 내가 잘못한 부분은
1) 웹 개발자가 원하는 위치(화면)에서 js함수를 실행시켜주지 않았다.
2) 형식이 조...금 잘못되었었다. (중괄호가 좀 이상하게 들어갔다, 작은따옴표를 안붙이거나 쌍따옴표를 붙였다.)
+) 인자에 들어가는 string을 print하면 내가 원하는 형식으로 출력되지만 디버깅으로 값을 확인해보면 '(작은 따옴표) 하나만 넣었는데 이스케이프 시퀀스( \ )가 자동으로 들어간다. 혹시 이것때문에 잘 안되나 싶어 이걸 빼고 string을 구성하는 방법을 찾아봤었는데 그런 방법은 없다. 그리고 ( \ )가 들어간다고 해도 전혀 상관없으니 그냥 진행해도 무방하다 ^^..
이런식으로 작성하면 웹개발자가 다음화면으로 이동하는 코드를 작성했다면 정상적으로 화면이 넘어갈것이다.
실제 프로젝트가 아닌 실습을 진행하고 싶다면 ..
https://github.com/ClintJang/sample-swift-wkwebview-javascript-bridge-and-scheme
요 링크에서 많은 정보를 얻을 수 있다!
깃헙 코드 직접 실행해 보면서 하면... 빠른 이해 가능👍🏻
'iOS' 카테고리의 다른 글
[iOS] iOS 푸시알림(APNS, FCM) (0) | 2023.06.29 |
---|---|
[iOS] UUID와 UDID (0) | 2023.06.15 |
[iOS] WKWebView 쿠키 데이터 가져오기 (자동로그인) (0) | 2023.06.08 |
[iOS] PageViewController 사용하기 (0) | 2023.05.18 |
[iOS] ReactiveX란? / RxSwift 기초 익히기 (0) | 2023.05.08 |