iOS
여기어때 iOS 앱의 네트워크 모듈 리팩토링&화면 로딩 기능 개선 이야기
Draak여기어때
2024년 9월 26일
원문에서 보기 ↗안녕하세요, 여기어때컴퍼니 iOS개발팀 드락입니다.
iOS개발팀은 우리의 일터인 여기어때 앱 프로젝트에 적용된 기술 스택이 업계 메인스트림에서 뒤쳐지지 않도록 항상 노력 중인데요, 여기어때 기술블로그라는 공간을 통해 최근에 진행한 여기어때 앱의 Network/Adapter 모듈 구조 개선에 대해 한 번 이야기해볼까 합니다.
이번 구조 개선은 기술적 개선&기능적 개선 두 가지 전부 이루어졌는데요, 개선사항은 다음과 같습니다.
Network, Adapter 모듈과 인터페이스에 RxSwift, RxCocoa 의존성 제거
UI 레벨에서 네트워크 로딩 상태를 자유롭게 핸들링 가능한 구조로 전환
우선, 첫 번째로 언급한
Network, Adapter 모듈, 인터페이스에 RxSwift, RxCocoa 의존성 제거
부터 살펴보겠습니다.
바로 제거하기 전에 어쩌다 RxSwift에 의존하게 되었는지, 이제와서 굳이 왜 제거하려 하는지를 우선 살짝 짚고 넘어가자면,
대략 2018년도 즈음.. 앱개발 씬이 ReactiveX Hype 상태일 시절, 세상이 iOS개발자에게 RxSwift & RxCocoa 스킬을 필수 소양으로 요구했었고, 저 역시 RxSwift와 RxCocoa를 익히며 앱의 코어부터 UI레벨에 거쳐 사방팔방에 Rx를 적용했습니다.
그런데…이제 상황이 약간 변했습니다?
우선, 1st-Party인 애플에서 Built-in으로 Rx대안 솔루션인 Combine을 발표했고, 지속적으로 지원해주고 있습니다.
거기에, 기존의 callback 방식을 대체하는 형태로 많이 활용되던 RxSwift 코드들을 대체할 수 있는 차세대 비동기 처리 솔루션인 Async/Await도 발표되었죠.
1st-party 솔루션을 사용하는 것은 많은 부분에서 이점을 가져다줍니다.
- 우선 신뢰도가 높고요, 플랫폼 전반에 걸친 호환성을 보장합니다.
- Swift 컴파일러는 당연하게도 1st-party 타입을 가장 잘 이해하고 있기 때문에, 최적화된 코드를 생성할 수 있고, IDE 에서도 가장 매끈한 개발 지원이 이루어집니다.
- 1st-party에서 제공하는 타입들은 통상적으로 플랫폼에 최적화된 성능을 제공하며 지속적인 업데이트와 개선이 이루어집니다.
- Swift 개발자들 사이에서 공통된 이해를 기반으로 하기 때문에, 누구나 빠르게 코드에 접근하고 수정할 수 있습니다.
요약: 1st-Party 솔루션은 3rd-party 솔루션에 비해 안정성, 성능, 유지보수, 보편성, 프로젝트 러닝커브등 여러가지를 고려했을 때 많은 이점이 있습니다.
1부터 10,000까지의 값들에 2를 곱하는 작업을 RxSwift와 Combine을 활용해서 각각 100회씩 수행하는 command line tools 프로젝트를 만든 뒤 Instruments를 통해 성능을 측정해 보았습니다.

좌측 상단부터 시계 방향으로 - 할당된 후 해제되지 않고 계속 유지되고 있는 메모리 블록, 할당된 총 메모리의 양, 발생한 메모리 할당 횟수, 작업 완료까지 걸린 시간
아무래도 근본 태생이 다른 만큼 성능 면에서도 상당한 차이를 보입니다.
그렇다면, 우리 프로젝트도 RxSwift에 대한 의존도를 조금이라도 줄이는 방향으로 리팩토링하는 쪽이 유리하다는 판단 아래, View와 ViewModel 을 제외한 프로젝트의 모든 영역에서 탈 Rx하기로 결정합니다.

안녕 RxSwift..
여기어때 앱은 1개의 Network, 100여개의 서비스별 NetworkAdapter, 수백개의 Reactor-View 로 구성되어 있습니다.
우선 앱 내 RxSwift를 활용하던 부분들을 Combine 으로 변경하고요(대부분 간단히 대체됩니다).
추가로 Network 와 Adapter, 그 사이 인터페이스에서 RxSwift 의존성을 제거하는 작업을 진행합니다.
기존 구조는 Network 모듈 내에서 RxSwift 를 활용하여 데이터를 Observable 타입으로 감싼 뒤 Adapter 레이어로 리턴해주고, Adapter 역시 같은 사고방식으로 Observable 로 감싸진 데이터를 리턴 타입으로 Reactor 까지 인터페이스하고 있었는데요.
이렇게 모듈간 인터페이스가 Observable 타입으로 되어있는 구조는 Network 와 Adapter 본체를 비롯해 이들과 인터페이스 할 모든 모듈에 RxSwift 의존성을 강제하게 되는 구조입니다.
이를 모듈간 인터페이스 역시 가능한 1st-party 에서 제공하는 기본 타입을 활용하여 최대한 표준화되도록 정리합니다.

표준화된 인터페이스는 모듈간 호환성 문제를 미연에 방지해 줍니다.
ReactorKit을 사용하고 있는 여기어때 iOS 앱의 구조상 RxSwift 와 밀접하게 연관되어 있는 Reactor 영역의 각종 Observable 인터페이스들을 표준 인터페이스로 바꾸기 위한 컨버팅 코드가 Reactor 내부에 생겨나 코드의 복잡도가 올라가고 보일러플레이트성 코드가 생겨날 수도 있습니다.
그렇지만, 이를 일정 부분 감수하더라도 프로젝트 아키텍쳐적인 시각에서 보았을 때 모듈 사이의 인터페이스 정돈이 리액터의 컨버팅 코드로 인한 복잡도 증가보다 더 가치가 있다고 생각했습니다.
인터페이스 통일은 좀더 큰그림 관점에서 가야할 올바른 방향이고 리액터들 내부의 컨버팅 코드가 지저분하게 늘어난 것은 좀 미안하게 됐지만 당분간은 리액터들이 감수해야 할 사정인거죠! 그 사정은 추후 리액터 영역을 RX 디펜던시에서 자유로운 다른 대체 코드로 적당한 때에 리팩토링하는 형태로 정돈해 나가려는 계획입니다.
결과적으로, 아래와 같이 RX 제거 작업이 완료되었습니다 :)

여기어때 iOS 앱의 모든 영역에 전환 작업이 완료되었다는 건 아니고요….
앱의 내정보 화면 영역에 해당 리팩토링 건이 첫 반영되어 상용 출시까지 완료되었습니다.
여기어때처럼 2주 간격으로 새로운 개발과 실험들이 바삐 업데이트되는 상용 앱에 대한 리팩토링 건들은 설계도 중요하지만, 운영 퍼포먼스에 영향이 없도록 점진적으로 어떻게 전환해 나갈지가 더욱 중요합니다. 전환이 앱 전 영역에 거쳐 단번에 이루어질 수 없고, 서비스 안정성에 영향이 없어야 하며, 전환 작업이 상시 병렬로 진행중인 다른 개발건들을 가로막으면 안되기 때문인데요.
이를 위해 RxSwift 제거작업이 완료된 새로운 Network & Adapter 를 프로젝트에 별도로 추가해두고, 각각의 Reactor 와 View 리팩토링 시점에 새로운 Network & Adapter 로 갈아타는 형태로 점진적 리팩토링 전략을 세워서 진행 중입니다 :)
이제, 두 번째 개선된 사항인
화면에 컴포넌트별 개별 로딩 Status 핸들링 지원
에 대해 이야기해보겠습니다.
Observable 구독 형태와 다르게 Async/Await 으로 전환된 구조 기반에서는 뷰모델에서 Adapter 에 요청하는 Network Request Task 를 좀 더 쉽게 핸들링할 수 있습니다. 여기에 알맞게 네트워크가 앱 전면에서 로딩바를 동작시키던 기존 구조에서 각각의 뷰 레벨에서 자체적으로 컨트롤 하는 구조로 역할과 책임을 분산시켰습니다.
이게 무슨뜻이냐면..
네트워크 모듈에서 앱의 전면 로딩바를 동작시키는 기존 구조에서는 로딩 프로그래스바가 동작하는 동안에는 앱의 모든 기능이 막힌다는 정책만 수용 가능했습니다. 네트워크 입장에서는 현재 화면에 구동중인 뷰의 구성이나 사정을 일일히 알 수 없으니까요.
이 로딩바를 각각의 뷰 레벨에서 해당하는 화면 상황에 맞는 정책으로 개별 구성하는 구조가 가능해졌습니다. 화면 상단의 뒤로가기 버튼을 예시로 간단한 영상을 통해 한 번 살펴보겠습니다.
이전의 구조에서는 네트워크 속도가 느린 경우, 로딩이 완료될 때까지 전면 로딩바가 앱과 사용자의 모든 상호작용을 가로막게 됩니다. 그래서 뒤로가기를 포함한 앱의 모든 기능이 일시적으로 사용 불가능했습니다. 로딩이 완료될 때까지 기다린 뒤에 뒤로가기를 눌러서 빠져나가거나, 로딩이 너무 느리면 그냥 앱을 꺼버리고 다시 켜기도 했었죠.
https://youtube.com/shorts/jFlrDq8PdlM
네트워크 작업들에 대한 컨트롤이 화면에서 가능해졌으니 로딩 도중에도 뒤로가기 버튼을 열어두고 언제든 네트워크 작업들을 정리하면서 화면을 탈출하는 식의 동작이 가능해졌습니다:)
https://youtube.com/shorts/yKWImoF42J4
화면 내에 여러 개의 UI모듈이 동시에 로드되는 구성이라면, 모든 모듈 로딩이 완료되지 않은 상황에서도 먼저 로딩이 완료된 다른 모듈의 터치부터 우선 활성화시켜 앱을 계속해서 사용할 수 있도록 처리하는 것도 가능합니다.
이런 기능은 여기어때 앱 내에 최근 리뉴얼된 카테고리홈 화면에서 이미 구현된 적이 있는데요, 화면 전체 로딩 완료 후에 사용 가능한 것이 아니라 주변 추천 숙소 모듈이 아직 로딩중인 상태에서도 뒤로가기 또는 지역선택 UI등에 대한 상호작용은 막히지 않습니다. 따라서 유저가 기다리지 않고 앱을 계속해서 사용할 수 있도록 개발되어 있습니다.
https://youtube.com/shorts/A0a_sjtetqk
이때는 기능 테스트 겸 카테고리홈 한정으로 개발자가 열심히 커스텀으로 개발해서 해당 기능이 동작되게 만든거구요 ^_^
이제는 여기어때 iOS 앱의 어느 화면에서든 해당 스펙을 공식적으로 지원할 수 있는 기반이 아키텍처 차원에서 정돈되었다는 것에 의미가 있습니다!!
물론 마법처럼 알아서 동작하는건 아니고요.
화면 내에 비동기적으로 동작하는 개별 리퀘스트들을 다룰 정책 & 개발 복잡도가 과도하게 올라가지 않도록 적절한 스펙 설정이 필요합니다. 로딩별로 어떤 인터렉션을 허용/비허용 처리할지, 난해한 화면 로딩 정책으로 인해 유저의 사용성이 오히려 떨어지는 상황이 발생하지는 않는지 UX측면에서의 많은 고민도 필요하구요.
앞으로 이런 부분들에 대해 개발팀과 UX팀이 열심히 논의하면서 화면별 로딩을 적용할 계획이구요, 조금 더 쾌적하게 사용할 수 있는 여기어때 앱을 기대할 수 있을 것 같습니다!
이렇게 여기어때 iOS 개발팀은, 앱의 기능적인 개선, 장기적 관점에서 유지보수, 기술 부채 레벨 관리, 개발자로서 기술 습득 등 여러마리 토끼를 다 잡기 위해 항상 분주히, 또 즐겁게 일하고 있습니다.
읽어주셔서 감사합니다!!