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

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

kakao
PRO
December 08, 2022

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

#iOS #WebView #리팩토링

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

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

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

kakao
PRO

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

    View Slide

  2. Pay App Talk App

    View Slide

  3. 1. 페이의 웹뷰


    2. 페이 웹뷰 1.0 그리고 한계점


    3. 페이 웹뷰 2.0의 JSAPI 활용


    4. 리팩토링 (with 페이의 framework 관리)


    5. Shared Framework 코드 관리의 장단점

    View Slide

  4. Full Screen

    View Slide

  5. Bottom Sheet

    View Slide

  6. NavigationBar


    Hidden
    Custom


    NavigationBar

    View Slide

  7. 1. 페이의 웹뷰


    2. 페이 웹뷰 1.0 그리고 한계점


    3. 페이 웹뷰 2.0의 JSAPI 활용


    4. 리팩토링 (with 페이의 framework 관리)


    5. Shared Framework 코드 관리의 장단점

    View Slide

  8. 페이 구버전 iOS 웹뷰의 소통 방식 (페이 웹뷰 1.0)
    Web
    Native
    App Scheme
    Script

    View Slide

  9. 페이 구버전 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 - 호출

    View Slide

  10. // 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 - 호출 구분

    View Slide

  11. // 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 - 필요값 수신

    View Slide

  12. // 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 정보 전달

    View Slide

  13. 정보의 한계


    - URL Parameter 값을 인코딩해야 하는 경우 또는 url이 길어지는 경우 한계 존재


    - JavaScript로 전송할 수 있는 정보의 한계 존재
    페이 구버전 iOS 웹뷰의 한계점
    디버깅 이슈


    - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능


    - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생
    더 많은 니즈


    - 웹뷰의 활용 범위가 커지면서 더 많은 정보를 주고받을 니즈 발생


    - 다양한 UI 핸들링의 한계점 발생

    View Slide

  14. 더 많은 니즈


    - 웹뷰의 활용 범위가 커지면서 더 많은 정보를 주고받을 니즈 발생


    - 다양한 UI 핸들링의 한계점 발생
    페이 구버전 iOS 웹뷰의 한계점
    정보의 한계


    - URL Parameter 값을 인코딩해야 하는 경우 또는 url이 길어지는 경우 한계 존재


    - JavaScript로 전송할 수 있는 정보의 한계 존재
    디버깅 이슈


    - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능


    - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생

    View Slide

  15. 더 많은 니즈


    - 웹뷰의 활용 범위가 커지면서 더 많은 정보를 주고받을 니즈 발생


    - 다양한 UI 핸들링의 한계점 발생
    페이 구버전 iOS 웹뷰의 한계점
    정보의 한계


    - URL Parameter 값을 인코딩해야 하는 경우 또는 url이 길어지는 경우 한계 존재


    - JavaScript로 전송할 수 있는 정보의 한계 존재
    디버깅 이슈


    - 동일한 App Scheme 중복 호출 시 Web에서 호출에 대한 응답을 특정 불가능


    - 여러 App Scheme이 동시에 빠르게 호출될 경우 누락되는 문제 발생

    View Slide

  16. 1. 페이의 웹뷰


    2. 페이 웹뷰 1.0 그리고 한계점


    3. 페이 웹뷰 2.0의 JSAPI 활용


    4. 리팩토링 (with 페이의 framework 관리)


    5. Shared Framework 코드 관리의 장단점

    View Slide

  17. JSAPI
    Script Con
    fi
    gure
    Native의 단방향 전송 Native와 FE의 양방향 소통 UI 속성 설정

    View Slide

  18. 뉴버전 iOS 웹뷰의 소통 방식 (페이 웹뷰 2.0)
    Web
    Native
    JSAPI
    Script
    App Scheme

    View Slide

  19. 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)

    View Slide

  20. /// 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의 결과 값 전달

    View Slide

  21. /// 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로 웹뷰 값 수신

    View Slide

  22. /// 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 값을 통해 간편한 디버깅 가능

    View Slide

  23. // 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() {…}


    }

    View Slide

  24. 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


    }

    View Slide

  25. // 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


    }

    View Slide

  26. // 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


    }

    View Slide

  27. // 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


    }

    View Slide

  28. // 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


    }

    View Slide

  29. // 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


    }

    View Slide

  30. 1. 페이의 웹뷰


    2. 페이 구버전 웹뷰와 한계점


    3. 페이 웹뷰와 JSAPI의 활용


    4. 리팩토링 (with 페이의 framework 관리)


    5. Shared Framework 코드 관리의 장단점

    View Slide

  31. Pay App
    Talk App

    View Slide

  32. 동일한 코드 2개 운영
    Talk, Pay 별도 운영 시 필요 개선점
    개발, 테스트, 빌드 등 2번 작업에 따른 신규 개발 및 유지보수 비용 증가


    Human Error 발생 가능성 존재


    시간이 갈수록 App별로 일부 코드가 달라지면서 히스토리 파악 어려움 발생


    100% 동일 코드가 아니어서 레거시 코드 정리에 대한 심리적 부담감 존재

    View Slide

  33. Combine & Refactoring

    View Slide

  34. Talk App Pay App
    PayAccount
    PayService
    PayUI
    PayFoundation
    Kakaopay Layer
    Pay Biz App
    Application
    Shared
    Foundation

    View Slide

  35. 리팩토링 - 웹뷰 통합작업
    PayService
    Talk Pay

    View Slide

  36. 리팩토링 - 웹뷰 통합작업
    PayService


    WebView
    App WebViewBinder

    View Slide

  37. 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로 웹뷰 통합

    View Slide

  38. 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로 웹뷰 통합

    View Slide

  39. 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

    View Slide

  40. 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()


    }


    }

    View Slide

  41. 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


    }

    View Slide

  42. 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


    }

    View Slide

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

    View Slide

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

    View Slide

  45. let selector


    = NSSelectorFromString(command)


    func perform(


    _ aSelector: Selector!,


    with object: Any!


    ) -> Unmanaged!


    Command 값에 해당되는 Selector로 호출
    리팩토링 트러블 슈팅 - JSAPI 호출

    View Slide

  46. // ৘࢚غח 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 호출

    View Slide

  47. // ৘࢚غח 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 호출

    View Slide

  48. 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


    }

    View Slide

  49. 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


    }

    View Slide

  50. 트러블 슈팅 결과 - 검증된 안전한 코드 적용하기

    View Slide

  51. 1. 페이의 웹뷰


    2. 페이 구버전 웹뷰와 한계점


    3. 페이 웹뷰와 JSAPI의 활용


    4. 리팩토링 (with 페이의 framework 관리)


    5. Shared Framework 코드 관리의 장단점

    View Slide

  52. Shared Framework 코드 관리의 장단점
    As
    -
    Is

    App별 코드 운영
    To
    -
    Be

    Shared Framework
    초기 설계 난이도 하 중~상
    효율성 중 최상
    신규 기능 및 유지보수

    개발소요시간(비율)
    1.0 0.6 (40% 감소)
    Code lines(비율) 1.0 0.6 (40% 감소)
    Dependency 강 약
    초기 설계 시 구조 고민 필요

    View Slide

  53. 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 감소

    View Slide

  54. Wrap
    -
    up

    View Slide

  55. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인


    - App Scheme 및 Script 방식의 한계점 존재


    - 웹뷰 활용 니즈 향상으로 개선 필요성 증대
    Wrap
    -
    up
    JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0)


    - Native와 FE의 제한없는 소통 가능, UI 확장성 향상


    - JSAPI에 할당된 id 값으로 간편한 디버깅
    앱별 코드 Shared Framework로 통합


    - 개발 효율성, 유지보수 용이성 향상


    - 앱에 대한 웹뷰 코드의 Dependency 감소

    View Slide

  56. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인


    - App Scheme 및 Script 방식의 한계점 존재


    - 웹뷰 활용 니즈 향상으로 개선 필요성 증대
    Wrap
    -
    up
    JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0)


    - Native와 FE의 제한없는 소통 가능, UI 확장성 향상


    - JSAPI에 할당된 id 값으로 간편한 디버깅
    앱별 코드 Shared Framework로 통합


    - 개발 효율성, 유지보수 용이성 향상


    - 앱에 대한 웹뷰 코드의 Dependency 감소

    View Slide

  57. 페이 구버전 웹뷰(웹뷰 1.0)의 한계점 확인


    - App Scheme 및 Script 방식의 한계점 존재


    - 웹뷰 활용 니즈 향상으로 개선 필요성 증대
    Wrap
    -
    up
    JSAPI를 통한 웹뷰 사용성 향상 (웹뷰 2.0)


    - Native와 FE의 제한없는 소통 가능, UI 확장성 향상


    - JSAPI에 할당된 id 값으로 간편한 디버깅
    앱별 코드 Shared Framework로 통합


    - 개발 효율성, 유지보수 용이성 향상


    - 앱에 대한 웹뷰 코드의 Dependency 감소

    View Slide