Upgrade to Pro — share decks privately, control downloads, hide ads and more …

카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기

kakao
December 08, 2022

카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기

#iOS #WebView #리팩토링

카카오페이는 네이티브와 웹뷰를 적절히 사용하여 높은 수준의 사용자 경험을 주고자 노력하고 있습니다. 네이티브와 웹뷰의 데이터 전달부터 풀스크린 웹뷰, 바텀시트형 웹뷰, 네비게이션 영역 핸들링이 가능한 웹뷰 등 다양한 방식의 웹뷰를 사용하고 있습니다.

본 세션에서는 다양한 시도를 통해 현재 사용중인 웹뷰로 발전하게 된 과정을 소개합니다. 여러 타입의 웹뷰를 핸들링하는 방법과 리팩토링 스토리를 포함하여 페이 서비스의 특성상 카카오페이앱 및 카카오톡앱 2개의 앱을 개발해야하는 환경에서 어떻게 코드를 관리하는지에 대해 알아봅니다.

발표자 : joey.con
카카오페이 사용자에게 더 나은 경험을 주고싶은 iOS 개발자 joey입니다. 카카오톡 내의 카카오페이 서비스와 카카오페이 앱을 개발하고 있습니다.

kakao

December 08, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. 카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기 윤한울 Joey.con 카카오페이

    Copyright 2022. Kakao Corp. All rights reserved. Redistribution or public display is not permitted without written permission from Kakao. if(kakao)2022
  2. 1. 페이의 웹뷰 2. 페이 웹뷰 1.0 그리고 한계점 3.

    페이 웹뷰 2.0의 JSAPI 활용 4. 리팩토링 (with 페이의 framework 관리) 5. Shared Framework 코드 관리의 장단점
  3. 1. 페이의 웹뷰 2. 페이 웹뷰 1.0 그리고 한계점 3.

    페이 웹뷰 2.0의 JSAPI 활용 4. 리팩토링 (with 페이의 framework 관리) 5. Shared Framework 코드 관리의 장단점
  4. 페이 구버전 iOS 웹뷰의 소통 방식 // scheme - app://kakaopay/pdf/view?title=title

    if url.path == “pdf/view” { if let urlString = url.queryParameters?[“url”], let pdfUrl = URL(string: urlString), let title = url.queryParameters?[“title”] { loadPdf(url: pdfUrl, title: title) evaluateJavaScript("window.app.pdfCallback()") } } App Scheme - 호출
  5. // scheme - app://kakaopay/pdf/view?title=title if url.path == “pdf/view” { if

    let urlString = url.queryParameters?[“url”], let pdfUrl = URL(string: urlString), let title = url.queryParameters?[“title”] { loadPdf(url: pdfUrl, title: title) evaluateJavaScript("window.app.pdfCallback()") } } 페이 구버전 iOS 웹뷰의 소통 방식 Scheme URL Path - 호출 구분
  6. // scheme - app://kakaopay/pdf/view?title=title if url.path == “pdf/view” { if

    let urlString = url.queryParameters?[“url”], let pdfUrl = URL(string: urlString), let title = url.queryParameters?[“title”] { loadPdf(url: pdfUrl, title: title) evaluateJavaScript("window.app.pdfCallback()") } } 페이 구버전 iOS 웹뷰의 소통 방식 URL Parameter - 필요값 수신
  7. // scheme - app://kakaopay/pdf/view?title=title if url.path == “pdf/view” { if

    let urlString = url.queryParameters?[“url”], let pdfUrl = URL(string: urlString), let title = url.queryParameters?[“title”] { loadPdf(url: pdfUrl, title: title) evaluateJavaScript("window.app.pdfCallback()") } } 페이 구버전 iOS 웹뷰의 소통 방식 JavaScript CallBack - FE 정보 전달
  8. 정보의 한계 - URL Parameter 값을 인코딩해야 하는 경우 또는

    url이 길어지는 경우 한계 존재 - JavaScript로 전송할 수 있는 정보의 한계 존재 페이 구버전 iOS 웹뷰의 한계점 디버깅 이슈 - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능 - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생 더 많은 니즈 - 웹뷰의 활용 범위가 커지면서 더 많은 정보를 주고받을 니즈 발생 - 다양한 UI 핸들링의 한계점 발생
  9. 더 많은 니즈 - 웹뷰의 활용 범위가 커지면서 더 많은

    정보를 주고받을 니즈 발생 - 다양한 UI 핸들링의 한계점 발생 페이 구버전 iOS 웹뷰의 한계점 정보의 한계 - URL Parameter 값을 인코딩해야 하는 경우 또는 url이 길어지는 경우 한계 존재 - JavaScript로 전송할 수 있는 정보의 한계 존재 디버깅 이슈 - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능 - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생
  10. 더 많은 니즈 - 웹뷰의 활용 범위가 커지면서 더 많은

    정보를 주고받을 니즈 발생 - 다양한 UI 핸들링의 한계점 발생 페이 구버전 iOS 웹뷰의 한계점 정보의 한계 - URL Parameter 값을 인코딩해야 하는 경우 또는 url이 길어지는 경우 한계 존재 - JavaScript로 전송할 수 있는 정보의 한계 존재 디버깅 이슈 - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능 - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생
  11. 1. 페이의 웹뷰 2. 페이 웹뷰 1.0 그리고 한계점 3.

    페이 웹뷰 2.0의 JSAPI 활용 4. 리팩토링 (with 페이의 framework 관리) 5. Shared Framework 코드 관리의 장단점
  12. Script - Native에서 Web에게 일방적으로 정보를 보낼 때 사용 -

    Event 또는 Button Action과 같이 간단한 정보를 전송 webView.evaluateJavaScript(“window.pay.pause();”, completionHandler: nil) webView.evaluateJavaScript(“window.pay.resume();”, completionHandler: nil) webView.evaluateJavaScript(“window.pay.close();”, completionHandler: nil)
  13. /// command, parameterܳ ా೧ чਸ ߉Ҋ /// messageܳ dictionary۽ ਢ࠭ী

    ੹׳ struct WebViewPluginRequest { var command: String var parameter: [String: Any]? var extra: [String: Any]? var message: WKScriptMessage? } JSAPI (JavaScript API) Web에서 Native 로직이 필요한 모든 경우에 사용 Web에서 정보를 받은 후, Native의 결과 값 전달
  14. /// command, parameterܳ ా೧ чਸ ߉Ҋ /// messageܳ dictionary۽ ਢ࠭ী

    ੹׳ struct WebViewPluginRequest { var command: String var parameter: [String: Any]? var extra: [String: Any]? var message: WKScriptMessage? } JSAPI (JavaScript API) command & parameter로 웹뷰 값 수신
  15. /// command, parameterܳ ా೧ чਸ ߉Ҋ /// messageܳ dictionary۽ ਢ࠭ী

    ੹׳ struct WebViewPluginRequest { var command: String var parameter: [String: Any]? var extra: [String: Any]? var message: WKScriptMessage? } JSAPI (JavaScript API) extra id 값으로 동일한 command의 비동기 처리 구분 id 값을 통해 간편한 디버깅 가능
  16. // command: “open” func requestOpen(_ request: WebViewPluginRequest) { guard let

    urlString: String = request.parameter?[“url”] as? String, let url: URL = URL(string: urlString) else { return request.fail() } UIApplication.shared.open(url, options: [:]) { success in if success { request.success([“opened”: true]) } else { request.fail() } } } extension WebViewPluginRequest { func success(_ result: [String: Any]? = nil) {…} func fail() {…} }
  17. Con fi guration - Native처럼 사용자에게 이질감 없는 Interaction, UI를

    제공하기 위해 
 웹뷰를 구성할 때 사전에 약속된 값으로 UI 핸들링 - 타이밍상 Native에서 더 자연스러운 기능은 Native에서 구현하고 Con fi guration으로 구분 - 웹뷰 UI 설정 값을 Con fi guration으로 받아 WebViewController 객체 생성 struct Configuration { let statusBarStyle: UIStatusBarStyle let navigationBarType: NavigationBarType let interfaceStyle: UIUserInterfaceStyle let skeletonType: SkeletonType }
  18. // type: transparent func updateNavigationBar(_ type: NavigationBarType) { navigationController?.setNavigationBarHidden(true, animated:

    false) if let leftButton = leftNavigationButtons.first?.superview { view.addSubview(leftButton) } if let rightButton = rightNavigationButtons.first?.superview { view.addSubview(rightButton) } } // statusBarStyle override var preferredStatusBarStyle: UIStatusBarStyle { return configuration.statusBarStyle }
  19. // type: transparent func updateNavigationBar(_ type: NavigationBarType) { navigationController?.setNavigationBarHidden(true, animated:

    false) if let leftButton = leftNavigationButtons.first?.superview { view.addSubview(leftButton) } if let rightButton = rightNavigationButtons.first?.superview { view.addSubview(rightButton) } } // statusBarStyle override var preferredStatusBarStyle: UIStatusBarStyle { return configuration.statusBarStyle }
  20. // type: transparent func updateNavigationBar(_ type: NavigationBarType) { navigationController?.setNavigationBarHidden(true, animated:

    false) if let leftButton = leftNavigationButtons.first?.superview { view.addSubview(leftButton) } if let rightButton = rightNavigationButtons.first?.superview { view.addSubview(rightButton) } } // statusBarStyle override var preferredStatusBarStyle: UIStatusBarStyle { return configuration.statusBarStyle }
  21. // type: transparent func updateNavigationBar(_ type: NavigationBarType) { navigationController?.setNavigationBarHidden(true, animated:

    false) if let leftButton = leftNavigationButtons.first?.superview { view.addSubview(leftButton) } if let rightButton = rightNavigationButtons.first?.superview { view.addSubview(rightButton) } } // statusBarStyle override var preferredStatusBarStyle: UIStatusBarStyle { return configuration.statusBarStyle }
  22. // type: transparent func updateNavigationBar(_ type: NavigationBarType) { navigationController?.setNavigationBarHidden(true, animated:

    false) if let leftButton = leftNavigationButtons.first?.superview { view.addSubview(leftButton) } if let rightButton = rightNavigationButtons.first?.superview { view.addSubview(rightButton) } } // statusBarStyle override var preferredStatusBarStyle: UIStatusBarStyle { return configuration.statusBarStyle }
  23. 1. 페이의 웹뷰 2. 페이 구버전 웹뷰와 한계점 3. 페이

    웹뷰와 JSAPI의 활용 4. 리팩토링 (with 페이의 framework 관리) 5. Shared Framework 코드 관리의 장단점
  24. 동일한 코드 2개 운영 Talk, Pay 별도 운영 시 필요

    개선점 개발, 테스트, 빌드 등 2번 작업에 따른 신규 개발 및 유지보수 비용 증가 Human Error 발생 가능성 존재 시간이 갈수록 App별로 일부 코드가 달라지면서 히스토리 파악 어려움 발생 100% 동일 코드가 아니어서 레거시 코드 정리에 대한 심리적 부담감 존재
  25. class PayWebViewController: UIViewController, WKScriptMessageHandler { public private(set) weak var webView:

    WKWebView! var binder: PayServiceWebViewBinderable { // Appীࢲ ࢤࢿೠ webViewBinder } public init(url: URL?, configuration: Configuration) {} open override func viewDidLoad() { super.viewDidLoad() configureWebView() } } PayService로 웹뷰 통합
  26. class PayWebViewController: UIViewController, WKScriptMessageHandler { public private(set) weak var webView:

    WKWebView! var binder: PayServiceWebViewBinderable { // Appীࢲ ࢤࢿೠ webViewBinder } public init(url: URL?, configuration: Configuration) {} open override func viewDidLoad() { super.viewDidLoad() configureWebView() } } PayService로 웹뷰 통합
  27. func handleWebViewPlugin(_ request: WebViewPluginRequest) { // Appীࢲ Handle غ঻ח૑ check

    guard !binder.isHandledWebViewPlugin(request) else { return } switch request.command { case “open”: requestOpen(request) case “hide”: requestHide(request) default: request.fail() } } PayService JSAPI
  28. PayService JSAPI func handleWebViewPlugin(_ request: WebViewPluginRequest) { // Appীࢲ Handle

    غ঻ח૑ check guard !binder.isHandledWebViewPlugin(request) else { return } switch request.command { case “open”: requestOpen(request) case “hide”: requestHide(request) default: request.fail() } }
  29. App Level JSAPI func isHandledWebViewPlugin(_ request: WebViewPluginRequest) -> Bool {

    var isHandled: Bool = true switch request.command { case “app_function_1”: requestAppFunction_1(request) case “app_function_2”: requestTalkFunction_2(request) default: isHandled = false } return isHandled }
  30. App Level JSAPI func isHandledWebViewPlugin(_ request: WebViewPluginRequest) -> Bool {

    var isHandled: Bool = true switch request.command { case “app_function_1”: requestAppFunction_1(request) case “app_function_2”: requestTalkFunction_2(request) default: isHandled = false } return isHandled }
  31. 리팩토링 결과 As - Is To - Be 비동기 로직

    Promises Async - Await PayService Binder - WebViewBindable Interceptor - Interceptor로 원하는 로직 추가 가능 JSAPI 호출 로직 command —> function command —> function
  32. 리팩토링 결과 As - Is To - Be 비동기 로직

    Promises Async - Await PayService Binder - WebViewBindable Interceptor - Interceptor로 원하는 로직 추가 가능 JSAPI 호출 로직 command —> function command —> function
  33. let selector = NSSelectorFromString(command) func perform( _ aSelector: Selector!, with

    object: Any! ) -> Unmanaged<AnyObject>! Command 값에 해당되는 Selector로 호출 리팩토링 트러블 슈팅 - JSAPI 호출
  34. // ৘࢚غח case UnitTest enum MockCommand: String { case ੿࢚

    = “test_load” case হחೣࣻ = “test_load_load” case ౵ۄ޷ఠ୶о = “test_load:” case ౵ۄ޷ఠఋੑࠛੌ஖ = “test_reload” var value: String { “\(self.rawValue):” } } @objc func test_load(_ request: MockWebViewPluginRequest) {} @objc func test_reload(_ request: MockWebViewPluginRequest, _ title: String) {} 리팩토링 트러블 슈팅 - JSAPI 호출
  35. // ৘࢚غח case UnitTest enum MockCommand: String { case ੿࢚

    = “test_load” case হחೣࣻ = “test_load_load” case ౵ۄ޷ఠ୶о = “test_load:” case ౵ۄ޷ఠఋੑࠛੌ஖ = “test_reload” var value: String { “\(self.rawValue):” } } @objc func test_load(_ request: MockWebViewPluginRequest) {} @objc func test_reload(_ request: MockWebViewPluginRequest, _ title: String) {} 리팩토링 트러블 슈팅 - JSAPI 호출
  36. func call(_ command: MockCommand) -> Bool { let request =

    MockWebViewPluginRequest(command: command) let selector = NSSelectorFromString(request.command.value) if webViewController.responds(to: selector) { // command stringҗ زੌೠ naming੄ ೣࣻ ഐ୹ webViewController.perform(selector, with: request) return true } else { return false } } func test_call_function() throws { XCTAssertTrue(call(.੿࢚)) XCTAssertTrue(call(.হחೣࣻ)) XCTAssertTrue(call(.౵ۄ޷ఠఋੑࠛੌ஖)) // crash }
  37. func call(_ command: MockCommand) -> Bool { let request =

    MockWebViewPluginRequest(command: command) let selector = NSSelectorFromString(request.command.value) if webViewController.responds(to: selector) { // command stringҗ زੌೠ naming੄ ೣࣻ ഐ୹ webViewController.perform(selector, with: request) return true } else { return false } } func test_call_function() throws { XCTAssertTrue(call(.੿࢚)) XCTAssertTrue(call(.হחೣࣻ)) XCTAssertTrue(call(.౵ۄ޷ఠఋੑࠛੌ஖)) // crash }
  38. 1. 페이의 웹뷰 2. 페이 구버전 웹뷰와 한계점 3. 페이

    웹뷰와 JSAPI의 활용 4. 리팩토링 (with 페이의 framework 관리) 5. Shared Framework 코드 관리의 장단점
  39. Shared Framework 코드 관리의 장단점 As - Is 
 App별

    코드 운영 To - Be 
 Shared Framework 초기 설계 난이도 하 중~상 효율성 중 최상 신규 기능 및 유지보수 
 개발소요시간(비율) 1.0 0.6 (40% 감소) Code lines(비율) 1.0 0.6 (40% 감소) Dependency 강 약 초기 설계 시 구조 고민 필요
  40. Shared Framework 코드 관리의 장단점 As - Is 
 App별

    코드 운영 To - Be 
 Shared Framework 초기 설계 난이도 하 중~상 효율성 중 최상 신규 기능 및 유지보수 
 개발소요시간(비율) 1.0 0.6 (40% 감소) Code lines(비율) 1.0 0.6 (40% 감소) Dependency 강 약 효율성, 유지보수 용이성 향상 Dependency 감소로 향후 모듈화 가능 Code lines 감소
  41. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인 - App Scheme 및

    Script 방식의 한계점 존재 - 웹뷰 활용 니즈 향상으로 개선 필요성 증대 Wrap - up JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0) - Native와 FE의 제한없는 소통 가능, UI 확장성 향상 - JSAPI에 할당된 id 값으로 간편한 디버깅 앱별 코드 Shared Framework로 통합 - 개발 효율성, 유지보수 용이성 향상 - 앱에 대한 웹뷰 코드의 Dependency 감소
  42. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인 - App Scheme 및

    Script 방식의 한계점 존재 - 웹뷰 활용 니즈 향상으로 개선 필요성 증대 Wrap - up JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0) - Native와 FE의 제한없는 소통 가능, UI 확장성 향상 - JSAPI에 할당된 id 값으로 간편한 디버깅 앱별 코드 Shared Framework로 통합 - 개발 효율성, 유지보수 용이성 향상 - 앱에 대한 웹뷰 코드의 Dependency 감소
  43. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인 - App Scheme 및

    Script 방식의 한계점 존재 - 웹뷰 활용 니즈 향상으로 개선 필요성 증대 Wrap - up JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0) - Native와 FE의 제한없는 소통 가능, UI 확장성 향상 - JSAPI에 할당된 id 값으로 간편한 디버깅 앱별 코드 Shared Framework로 통합 - 개발 효율성, 유지보수 용이성 향상 - 앱에 대한 웹뷰 코드의 Dependency 감소