[if kakao 2022] 카카오페이 iOS 웹뷰 소개, 그리고 세션에서 못다한 이야기

[if kakao 2022] 카카오페이 iOS 웹뷰 소개, 그리고 세션에서 못다한 이야기

시작하며

🔗 if(kakao) 발표 영상 보러 가기: 카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기

안녕하세요. 카카오페이 iOS 개발팀에서 카카오톡, 카카오페이 iOS 앱을 개발 중인 Joey와 Sean입니다.

if(kakao)에서 카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기를 통해 카카오페이 iOS 웹뷰에 대해 소개해 드렸습니다. 카카오페이 iOS 웹뷰 1.0에서 2.0으로 발전하게 된 내용에서부터 Framework 통합과 함께 진행한 리팩토링까지, 페이 웹뷰에 대한 역사를 담은 세션이었습니다. 만약 아직 영상을 보지 못하셨다면 영상 먼저 보고 오시길 추천드립니다. 영상을 본 뒤 포스팅을 읽으면 이해도 쉽고 더 많은 팁을 얻으실 수 있습니다.

본 포스팅에서는 if(kakao) 세션에서 시간 관계상 다루지 못했던 내용을 공유해보려고 합니다. 더 많은 코드 레벨의 내용이 궁금하셨던 분들에게 도움이 될만한 내용이 될 것 같습니다. 그럼 시작해보겠습니다. 😃

페이 iOS 웹뷰 1.0

세션에서 소개해 드렸던 것처럼 카카오페이 서비스가 시작되었던 2017년, iOS 웹뷰 1.0이 시작됩니다. App Scheme을 통해 FE로부터 URL을 전달받아 Path를 통해 호출해야할 로직을 구분하고 Parameter를 통해 세부 값을 받는 방식입니다. Native의 로직 수행 후 Script Call Back 방식으로 결과를 전달했습니다.

페이 iOS 웹뷰 1.0의 정보전달 방식
페이 iOS 웹뷰 1.0의 정보전달 방식

단순한 로직은 문제없지만, 복잡한 비동기 처리가 필요할 때 주고받을 수 있는 정보에 한계가 있었습니다. 그래서 JSAPI를 중심으로 한 페이 iOS 웹뷰 2.0으로 넘어가게 됩니다.

페이 iOS 웹뷰 2.0

세션에서도 소개해드렸던 것처럼 페이 iOS 웹뷰 2.0의 특징은 Script, JSAPI, Configure 3가지 키워드로 말할 수 있습니다. - Script: Native에서 단방향 정보를 보낼 때 사용합니다.

  • JSAPI: Native와 FE의 정보 전달에 사용합니다. 양방향 소통이 가능합니다.
  • Configuration: Native에서 UI와 관련된 값을 정의할 때 사용합니다.

페이 iOS 웹뷰 2.0의 정보전달 방식
페이 iOS 웹뷰 2.0의 정보전달 방식

Script와 JSAPI에 대한 설명은 세션에서 다루었습니다. 이번에는 비교적 가볍게 언급하고 넘어간 Configuration에 대해 더 자세히 알아보겠습니다.

Configuration

처음에는 UI값을 웹뷰 객체 생성 parameter로 넣어주었습니다. 예를들면 다음과 같은 방식입니다.

public init(animated: Bool = true, presentationStyle: ModalPresentationStyle = .fullScreen) {
    ...
}

위와 같은 방식은 여러 불편함을 초래했습니다. 처음에는 parameter 값이 몇 개 없었으나 점점 parameter 값이 많아져 함수가 길어지면서 가독성이 떨어지고 한눈에 initialize 함수를 파악하기 어려워졌습니다. 게다가 initialize 인터페이스가 여러 개다 보니 모든 함수에 parameter 값을 추가해야 하는 수고스러움은 덤입니다. 여기서 끝이 아닙니다. 카카오톡, 카카오페이 앱에 소스 코드 복사&붙여넣기 후 빌드하여 UI를 확인해보고, 수정사항이 있을 경우 수정&빌드를 2번씩 반복합니다. 이처럼 UI 관련 값을 하나 추가하는데도 적지 않은 시간이 소요됩니다.

웹뷰에서 다른 웹뷰를 띄울 때 URL에서 UI 관련된 parameter 값을 추출하여 적용할 때도 매번 Dictionary의 key-value 비교를 통해 넣어줘야하는 번거로움이 있습니다.

if let parameters: [String: Any] = urlParameters as? [String: Any] {
    let url: URL? = parameters[url] as? URL
    let animated: Bool = parameters[url] as? Bool
    let statusBarStyle: StatusBarStyleType? = .init(rawValue: parameters["status_bar_style"] as? String ?? "default")
    let webViewController = WebViewController(url: url, statusBarStyle: statusBarStyle)
    navigationController?.pushViewController(webViewController, animated: animated ?? true)
}

이러한 불편함을 없애고자 Configuration 구조체를 만들어 통합하게 되었습니다. 웹뷰는 카카오페이의 모든 서비스에서 사용하는 공통 모듈이어서 사용하는 쪽에서 Configuration이라는 구조체를 만들고 웹뷰 객체를 생성할 때 넣어주고 있습니다.

public struct Configuration: OneWebViewConfigurationType {
    let animated: Bool
    let statusBarStyle: StatusBarStyleType
    let presentationStyle: ModalPresentationStyle
    let interfaceStyle: UIUserInterfaceStyle
    let backgroundType: BackgroundType
}

기본 UI에 대해 Configuration 기본값이 지정되어 있습니다. 때문에 값을 넣어주지 않으면 기본 UI로 보여지게 됩니다. Custom이 필요한 항목이 있을 경우 그 부분만 변경하면 됩니다.

Configuration은 Native에서 웹뷰 객체를 생성할 때뿐만 아니라, URL로부터 parameter를 받는 경우에도 아래 코드처럼 Configuration 구조체를 생성할 수 있습니다. Configuration을 통해 웹뷰를 생성하는 여러 진입점에서 동일한 인터페이스를 가지기 때문에 UI항목이 추가될 때도 유지보수 및 신규 개발 용이성이 향상되었습니다. 새로운 값이 추가되어도 Configuration 구조체에만 추가하고 진입점 코드는 수정하지 않아도 되기 때문입니다. UI 값을 컨트롤하는 통합 센터가 생긴 셈입니다.

// Configuration initialize
init(parameter: [String: Any]) {
    self.animated = (parameter["animated"] as? Bool) ?? true
    self.statusBarStyle = (StatusBarStyleType.init(rawValue: (parameter["status_bar_style"] as? String))) ?? .default
}

Shared Framework

카카오페이 iOS Framework 관리

레거시 코드 정리, 리팩토링, 더 나은 개발 환경 구축은 모든 개발자의 숙명일 것입니다. 카카오페이 iOS 개발자들도 계속해서 더 나은 개발 환경과 높은 품질의 코드를 위해 노력하고 있습니다. 카카오페이 서비스는 카카오페이 사용자들에게 더 나은 경험을 전달하기 위해 서비스 업데이트와 개편이 잦은 편입니다. 업데이트가 잦은 카카오톡, 카카오페이 2개의 앱을 동시에 개발하는 iOS 개발팀은 2022년에 보다 나은 개발 환경을 구축하고자 했습니다. 그 방법 중 하나가 Shared Framework를 적극 활용하는 것입니다.

현재 카카오페이에서 빈번하게 사용하고 있는 Framework는 아래 이미지와 같습니다. 카카오톡, 카카오페이, 카카오페이 비즈니스 앱을 개발할 때 사용하고 있습니다.

KakaoPay Frameworks
KakaoPay Frameworks

단순하게 생각하면 2개로 나누어져 있던 코드를 1개로 합치는 작업입니다. 코드를 2개 관리할 때와 1개로 통합해 관리할 때의 장점은 작성할 코드 라인 수만 비교해봐도 뚜렷한 장점이 있습니다. 카카오톡, 카카오페이 앱별로 매월 100줄의 코드를 추가한다고 가정했을 때 코드를 1개로 통합한 경우 1년이면 1,200줄의 코드 작성을 하지 않아도 되는 것입니다. 디버깅을 위한 빌드 횟수와 누적된 빌드 시간까지 포함하면 많은 비용을 아낄 수 있습니다. 또한 동일한 기능의 코드를 중복 구현할 필요가 없어져 유지보수 용이성은 증가하고 휴먼에러 가능성은 감소했습니다.

웹뷰의 JSAPI 예시 코드를 보겠습니다. 아래 코드는 사용자의 Device App Setting으로 이동시키는 코드로 카카오톡, 카카오페이 앱 구분 없이 공통으로 사용할 수 있습니다. 카카오톡, 카카오페이 앱 두 곳에 작성된 중복 코드를 통합해 1개의 코드로 관리할 수 있습니다.

// 카카오톡, 카카오페이 앱
func openAppSettings() {
    guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
    UIApplication.shared.open(settingsURL, options: [:]) { success in
        ...
    }
}

아래 코드는 자연스러운 다크모드 대응을 위해 필요한 코드입니다. 사용자 기기의 다크모드 여부를 확인해서 FE에게 전달하는 코드입니다. 카카오페이 앱은 사용자 기기 정보 값을 따라가지만 카카오톡 앱의 경우 사용자가 light, dark 모드를 고정해서 사용할 수 있고 테마도 적용할 수 있기 때문에 앱별 코드 분기가 필요합니다.

// 카카오톡 앱
func getColorScheme() {
    var isDarkMode: Bool = (UIScreen.main.traitCollection.userInterfaceStyle == .dark)
    if isDefaultKakaoTheme {
        isDarkMode = isDarkKakaoTheme
    } else {
        isDarkMode = false
    }
    request.success("isDarkMode": isDarkMode)
}

// 카카오페이 앱
func getColorScheme() {
    let isDarkMode: Bool = (UIScreen.main.traitCollection.userInterfaceStyle == .dark)
    request.success("isDarkMode": isDarkMode)
}

이 코드에 대해 Shared Framework 통합 및 리팩토링한 결과는 이어서 다음 장에서 살펴보겠습니다.

리팩토링으로 얻은 것

레거시 코드 정리 및 Framework 통합

세션에서 소개했듯이 각 앱에서 관리하던 웹뷰 코드는 동일한 코드로 시작했지만 점차 달라져 히스토리 파악에 어려움이 있었습니다. 웹뷰 담당 개발자가 아니고서는 함수의 존재 이유와 흐름을 한 번에 이해하기가 쉽지 않았습니다. PayService1로의 통합 리팩토링에서 가장 크게 얻은 부분은 웹뷰 load 흐름에 따른 코드 정리와 2개로 나누어져 있던 코드를 하나로 통일한 것입니다.

레거시 코드 정리

전반적으로 다음과 같이 레거시 코드를 정리하고 업데이트했습니다.

  • 진입점 인터페이스 일원화
  • Configuration 도입
  • 불필요한 flag용 변수 정리
  • 함수 접근제어 정리
  • Load, Navigation, Action, Scheme, Delegate 순서로 코드 흐름 정리
  • 써드파티 비동기 라이브러리 Promises 제거

Framework 통합

앞에서 살펴보았던 코드는 PayService framework의 웹뷰 코드에서 아래처럼 변경되었습니다.

// 카카오톡, 카카오페이 앱에서 이 코드는 제거, PayService로 이동
func openAppSettings() {
    guard let settingsURL = URL(string: UIApplication.openSettingsURLString)
    UIApplication.shared.open(settingsURL, options: [:]) { success in
        ...
    }
}

// 카카오톡, 카카오페이 앱에서 구현된 binder의 값을 활용
func getColorScheme() {
    let isDarkMode: Bool = binder.isDarkMode
    request.success("isDarkMode": isDarkMode)
}

Shared Framework 통합 리팩토링을 통해 전체적으로 코드 라인수는 40% 정도 감소되었습니다. 앞으로 관리해야 할 코드가 40% 줄어든 셈입니다.

Interceptors

PayService로 웹뷰의 거주지가 이동하면서 앱의 의존성이 약해졌고 예외처리에 대한 인터페이스가 필요해졌습니다. 카카오톡, 카카오페이 앱 각각에 웹뷰 코드가 있을 때는 웹뷰 코드에 언제든지 접근해 Request나 Delegate 함수에서 예외처리를 할 수 있었습니다. 각 앱에 의존성이 있었기 때문에 가능한 방법이지만, 공통 로직이 아닌 코드가 섞여있게 되는 단점이 있습니다. PayAccount2의 HTTPClient(PayClient) 코드를 참고해서 웹뷰에도 Interceptor 구조를 도입했고, 웹뷰 객체 생성 시 Interceptor를 추가해 호출하는 쪽에서 예외처리를 직접 다룰 수 있게 되었습니다.

protocol WebViewInterceptor {
    func adapt(urlRequest: URLRequest) async -> URLRequest
}

class WebViewController: UIViewController {
    init(url: URL, interceptors: [WebViewInterceptor] = [], configuration: Configuration = .init) {
        self.url = url
        self.interceptors = binder.interceptors + interceptors
        self.configuration = configuration
    }

    func requestURL() async -> URLRequest {
        var request = URLRequest(url: self.url)
        for interceptor in self.interceptors {
            request = await interceptor.adapt(urlRequest: request)
        }
        return request
    }
}

웹뷰 내부의 어떤 함수에든 interceptor가 필요하면 WebViewInterceptor protocol에 추가해서 구현해주면 됩니다.

마치며

본 포스팅에서는 if(kakao) 카카오페이 iOS 웹뷰 소개 및 리팩토링 이야기 세션에서 시간 관계상 다루지 못했던 이야기를 소개해드렸습니다. 웹뷰 리팩토링 이후 근황을 공유해드리면, 현재 DocC 작업이 진행 중이고 일부 남은 상속 구조 제거 및 JSAPI 의존성 제거 등을 계획하고 있습니다. 사용하는 쪽에서는 더 간단하게 접근하고, 유지보수 및 신규 개발 시간은 단축될 수 있도록 카카오페이 iOS 웹뷰는 계속해서 진화하고 있습니다.

서비스를 개편하다 보면 레거시 정리 및 리팩토링에 많은 시간을 할애하기는 어려운 부분이 있는데요. 계속 생각만 해오던 웹뷰 리팩토링을 if(kakao) 덕분에 진행하게 되어 다행이라는 생각이 듭니다. 카카오페이 iOS 개발팀의 하고싶은 일을 지지해주는 문화 덕분에 가능했다는 생각이 듭니다. 😋 더 많은 카카오페이 iOS 코드가 궁금하고, iOS 앱을 함께 만들어 가고 싶은 분들은 언제든지 채용의 문을 두드려주세요. 기다리고 있겠습니다. 감사합니다.

🔗 카카오페이 iOS 개발자 영입 채용 공고 확인하기

Footnotes

  1. 카카오페이에서 사용하는 Service 관련 코드가 포함된 Framework입니다.

  2. 카카오페이에서 사용하는 Token, HTTPClient 등 계정 관련 코드가 포함된 Framework입니다.

joey.con
joey.con

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

sean.me
sean.me

삽질을 좋아하는 개발자입니다.