RxSwift를 활용한 iOS Reactive 네트워크 환경 구성

ReactiveX Reactive X 의 로고

Introduction

iOS 앱 개발자에게 2018년은 ‘RxSwift’라는 단어를 빼놓을 수 없는 한 해였지 않나 생각됩니다. 특히 iOS 앱 개발자들이 대거 참석하는 Let’s Swift 2018에서도 RxSwift의 언급이 상당히 많았고 카카오톡 오픈채팅방인 iOS Developers KR에서도 자주 언급되었음에도 RxSwift 오픈채팅방이 따로 생길만큼의 이슈를 끌었습니다. ReactiveX에 대한 설명은 워낙 잘 설명된 글과 훌륭한 강의 영상이 많고 홈페이지의 Documents도 아주 친절하니 꼭 참고해보시길 바라며 RxSwift를 처음 접하시는 분들은 곰튀김님의 ‘RxSwift 4시간 안에 끝내기’ 라는 유튜브 강의를 꼭 한번 들어보시길 강력 추천합니다.

Background

너드팩토리에서 개발한 i-Checker는 안드로이드, iOS에서 작동하는 사용자(근로자)가 근태 처리를 하는 부분과 해당 사유를 처리하거나 연차, 근태 관리를 하는 웹 관리자 페이지까지 사실 사용하는 사람들 입장에서는 규모가 크지 않은 심플한 앱이지만 관리자들이 사용하는 웹페이지까지 포함된 서비스이다 보니 실제 작동하는 백엔드는 복잡한 구조를 가지고 있습니다. 사실 동작 자체는 아래 그림보다 훨씬 유기적이고 복잡한 흐름을 갖게 되지만 n개가 될 수 있는 부서관리자와 기업까지 감안해서 유동적으로 반응할 수 있는 구성이 필요했고 근로자의 수도 당연히 그만큼 늘어날 수 있고 다양한 기능이 제공되면 시스템 자체가 갖는 복잡도가 증가하므로 근로자가 사용하게 되는 기능을 제한적으로 운영하게끔 구성했습니다.

i-Checker_flow i-Checker 의 워크 플로우

위 그림의 어떤 사람들이 어떻게 사용할지 아주 대략적인 그림으로 파악할 수 있듯 근로자는 근태체크와 사유 전송만 제한적으로 제공했고 이를 심플한 서비스라고 포장해서 개발에 착수했습니다. (PM은 개발도 병행하면 안 되는 이유이기도…) 당연히 그림과 부연 설명으로 알 수 있듯 iOS 플랫폼이 서버와 주고받는 양이 많지 않아 흔히 말하는 ‘콜백지옥’은 크게 경험할 일도 없었지만 습관적으로 사용하던 Reactive Swift 덕에 비동기 처리를 유기적으로 할 수 있었습니다. 하지만 정작 런칭이 된 이후에 사용자가 늘어나고 더 많은 이슈에 대응하다보니 각 플랫폼 개발자가 한 자리에 모여 커뮤니케이션을 할 수 있는 상황이나 서로 크로스 체크가 불가능한 상황이 되었고 모바일 프론트엔드만 걱정하면 되던 이전과는 다르게 i-Checker는 다른 플랫폼들도 마치 하나의 플랫폼인 것처럼 유기적으로 작동하길 원하게 되었습니다. 이러한 동작은 사용자들에게 하나의 사용자 경험을 제공할 수 있다는 장점이 될 것이라고 기대했지만 문제는 iOS부터 웹까지 서로 다른 용어를 사용하고 다른 방식으로 비동기 처리를 하다 보니 서로의 커뮤니케이션을 이해하거나 전달하기 위해서는 굉장한 시간 소모를 필요로 했고 2018년 가장 이슈가 컸던 RxSwift를 돌아보는 계기가 되었습니다.

Dependencies

iOS 단일 플랫폼만 놓고 이야기하자면 Reactive Swift + Moya + Alamofire + ObjectMapper를 사용했던 이전의 네트워크 체계가 RxSwift + Moya + Alamofire + ObjectMapper로 변경되는 것이라 크게 의미가 없는 작업으로 느껴지기도 했습니다. 어차피 기존에도 FRP(Fuctional Reactive Programming)를 하려고 노력했던 코드였기에 사실상 그냥 라이브러리 교체로 인한 수고로움이 더 증가하고 잘 돌아가는 서비스에 위험요소를 개발자 스스로 던지는 것 같이 느껴졌기 때문입니다.

물론 이런 부분은 Kotlin이나 React JS를 사용하는 다른 플랫폼 개발자들도 같은 느낌을 받을 수밖에 없었고 초반에는 Rx 사용에 대한 의구심이 더 커지고 있었기에 RxSwift를 먼저 적용해보고 실제 어떤 효과가 있는지를 보여주는 쪽으로 방향을 선회했습니다. (이럴 땐 iOS 개발자가 한명인 게 도움이 되기도 합니다.) 물론 개인적으로는 Reactive Swift를 RxSwift로 바꾸는 것 외에도 다른 라이브러리에 의존성이 높았던 현재의 서비스에 불만이 많았던 편이었습니다. Swift4가 나온 뒤로 Codable이 생겼고 ObjectMapper를 굳이 사용하지 않아도 된다고 여겨졌기 때문에 작업 초반부터 podFile에서 Reactive Swift를 RxSwift로 바꾸고 ObjectMapper를 지워버리는 강수를 두었으나 Codable을 사용하자 어마어마한 오류 처리 작업 때문에 우선 당초 목표대로 RxSwift에만 집중하기로 했습니다. (다음엔 ObjectMapper를 버린 글을 꼭 기고해보도록 노력하겠습니다.)

Result

나중에 나와야 하는 이야기겠지만 RxSwift를 사용하면서 애초에 불만으로 쌓여있던 다른 라이브러리에 대한 높은 의존성을 버리고 싶었고 Dependencies가 많은 Moya와 Alamofire 모두 없애버리고 싶었고 후보군으로 TinyNetworking까지 사용해봤으나 아직 덜 성숙한 TinyNetworking은 하루만에 제외했고 커스텀 개발은 꿈도 못 꾸었습니다. 결과적으로 우려했던 대로 Reactive Swift를 RxSwift로 교체하는 고된 작업을 시작하게 되었고 다행히도(?) Moya를 그대로 두었으니 네트워크 체계 자체가 크게 변경되진 않았습니다. 우선 내 정보를 get하기 위한 me()함수를 예로 들면 Reactive 관련 라이브러리를 사용하지 않은 상태에서 도의 Completion을 사용하지 않는다고 가정한 Moya와 ObjectMapper의 활용법은 아래와 같은 코드로 선언할 수 있었습니다.

static func me() -> SignalProducer<User, Moya.MoyaError> {
        return MoyaProvider.request(.me(), completion: { _ in})
            .attemptMap { response in
                guard response.statusCode != 401 else {
                    return Result.failure(.statusCode(response))
                }
                guard case 200...299 = response.statusCode else {
                    return Result.failure(.statusCode(response))
                }
                guard let json = try? response.mapJSON(), let user = Mapper<User>().map(JSONObject: json) else {
                    return Result.failure(.jsonMapping(response))
                }
                return Result.success(user)
        }
    }

이랬던 코드를 Reactive를 활용하면 아래와 같이 변하게 되는데

static func me() -> SignalProducer<User, Moya.MoyaError> {
        return MoyaProvider.reactive.request(.me())
            .attemptMap { response in
                guard response.statusCode != 401 else {
                    return Result.failure(.statusCode(response))
                }
                guard case 200...299 = response.statusCode else {
                    return Result.failure(.statusCode(response))
                }
                guard let json = try? response.mapJSON(), let user = Mapper<User>().map(JSONObject: json) else {
                    return Result.failure(.jsonMapping(response))
                }
                return Result.success(user)
        }
    }

겨우 , completion: { _ in} 없어진 것 가지고 뭐가 좋은거냐 하시는 분도 계시겠지만 사실 더 복잡한 프로세스를 갖게 되면 앞서 언급한 콜백지옥을 맛볼 수 밖에 없습니다. 물론 Moya의 Dependencies 중 Results라는 매우 훌륭한 라이브러리도 있지만 어쨋든 이 모든 것들을 리뷰하기엔 본 기고의 목표에 어긋나므로 생략하겠습니다. 사실 코드 자체가 reactiveSwift를 아주 잘 활용했다고 표현하긴 어렵습니다만 어쨋든 결과적으로 .reactive 선언 하나만으로 Completion으로 일일이 처리해야했던 스트림의 비동기 처리를 할 수 있게 되었었습니다. 그리고 RxSwift를 사용하면 아래와 같이 한번 더 변하게 되는데

static func me() -> Observable<User> {
        return MoyaProvider.rx.request(.me())
            .asObservable()
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .map { json in
                let user = Mapper<User>().map(JSONObject: json)
                return user!
        }
    }

물론 statusCode를 일일이 관리하려 했던 이전과 접근법 자체가 차이가 나긴 하지만 결과적으로 아주 높은 가독성을 갖는 Observable한 스트림을 만들 수 있게 되었습니다. 물론 나중에 RxCocoa까지 다루게 되겠지만 RxCocoa를 제외하고도 RxSwift를 적용한 가장 큰 강점 중 하나라면 위와 같은 코드를 기반으로 RxJS를 사용하거나 RxJava 또는 RxKotlin을 사용하는 타 플랫폼 개발자들과 동일한 수준에서 동일한 용어를 사용해 협업이 가능해진다는 것입니다. 이게 갖는 장점은 온라인 상에서 많이 제시되고 있습니다만 제가 직접 경험해보고 체감한 강점은 네트워크를 구성하며 백엔드 개발자가 여러 플랫폼에 각기 다른 대응을 할 필요가 없고 하나의 API에 서로 다른 처리에 대해 Sync를 맞추기 위해 서로 다른 Think를 통일 시켜야 했던 일련의 회의시간이 어마어마하게 단축되었다는 것입니다.

예를 들어 iOS 개발자가 먼저 백엔드 연동하며 구현한 코드를 예시로 보여주며 리뷰를 하고 함께 개발하면 3개의 플랫폼을 다루며 커뮤니케이션이 불가능했던 개발자 3명이 Rx쪽 대화에서는 마치 3명의 같은 언어를 사용하는 개발자가 되는 경험을 할 수 있었습니다.

개인적으로 Rx를 도입하고 사용하기까지 과도하게 사람들의 예찬론에만 움직여서 함께 개발하는 사람들의 러닝커브만 높이는 것은 아닐까 우려했었습니다. 사실 아직 Moya를 기반으로 RxSwift의 맛만 본 상태이고 아직도 View에 RxSwift를 도입하기 위해 훨씬 더 높은 벽을 체감하며 공부하고 있지만 분명한 것은 저도 어느새 이 강력한 도구를 사용하지 않아야 할 이유를 찾기가 더 어려워졌다는 것입니다. 혹시라도 Rx 적용을 고민하고 계시다면 긍정적으로 도입을 검토하시며 조금씩 적용해보시길 권하며 다음에 더 좋은 글로 찾아뵙겠습니다.