"링크모아" 는 사용자의 북마크를 쉽게 관리하고 공유할 수 있는 앱 서비스입니다. 20년 12월 기획을 시작으로 디자이너 1명, 백엔드 개발자 2명, iOS 개발자 2명이 협업하며 21년 3월에 개발을 완료했습니다.
iOS 개발자와 처음으로 협업을 하며 이론적으로만 알고있던 git-flow 를 적용하고 merge 시에는 코드 리뷰를 꼭 거치도록 했습니다.
잠깐이지만 앱 스토어 생산성 부문에서 160위 내에 들었고 동아리 데모데이 대상을 받는 등, 저에게 있어 무척 뜻 깊은 프로젝트입니다.
데모데이 이후에 프로젝트가 마무리될거라고 생각했지만 같이 협업을 했던 iOS 개발자분이 한 가지 제안을 주셨습니다.
"현업에서 사용하는 기술들을 적용해보고 지속적으로 같이 리팩토링 해봐요."
매번 기능 개발만 진행하고 프로젝트가 종료되어서 유지보수와 좋은 코드에 대한 아쉬움이 컸었는데요. 팀원분이 주신 제안에 깊히 공감하게 되어 개발 완료 이후에 리팩토링을 진행하게 되었습니다.
RxSwift 공부하고 적용하기
RxSwift 는 마이크로소프트에서 만든 ReactiveX 를 Swift 로 구현한 프레임워크입니다.
비동기 코드를 "함수형과 선언형 프로그래밍" 통해 간단히 작성할 수 있습니다.
MVVM 코드 패턴과 같이 사용하면 데이터 바인딩을 간결하게 할 수 있다는 장점이 있습니다.
여러 MVVM 패턴의 장단점을 정리한 내용을 아래 포스팅으로 남겼습니다.
iOS MVVM 패턴 구현 정리: https://linux-studying.tistory.com/28
또한 복잡한 비동기 코드를 작업할 때 위와 같이 클로저가 중첩되며 가독성이 무척 떨어지는 문제점이 있었는데요. RxSwift 를 사용하면 flatMap 을 통해 여러 비동기 코드를 체이닝으로 연결하여 작업할 수 있습니다.
위처럼 RxSwift를 사용하여 비동기 클로저가 중첩하여 발생하는 코드 Depth 를 상당히 줄일 수 있습니다. 그 외에도 RxDataSource, RxGesture, RxMoya... 등등 여러 서포트 프레임워크를 통해 가독성 좋은 코드를 작성할 수 있었습니다.
Tuist 와 앱 모듈화
Tuist 는 Xcode 프로젝트 파일을 Swift 로 관리할 수 있도록 도와주는 프레임워크입니다.
git binary merge 설정을 해도 발생하는 프로젝트 충돌을 해결하기 위해 도입했습니다.
다만 Tuist 적용을 위해 예시 프로젝트를 살펴보니 상당히 많은 앱들이 모듈로 구성되어 있었습니다. 전에 Line 기술 블로그를 보며 "앱이 수백 개의 모듈로 구성되어 있다." 라는 의미를 정확하게 이해하지 못했는데요. Tuist 의 방향성에 맞게 링크모아에도 앱 모듈화가 필요하다는 판단을 하여 학습하게 되었습니다.
<참고한 자료 및 프로젝트>
민소네님, Let us go 2019 프레임워크 주도 개발: https://www.youtube.com/watch?v=YiEpuZQPuko
Soojin Ro님, BookStore-iOS: https://github.com/nsoojin/BookStore-iOS
라인 개발자가 말하는 대규모 iOS 앱 개발 썰: https://youtu.be/wZh8WXPAIeg?t=583
Kanghoon님, Xcode 프로젝트 관리를 위한 Tuist 알아보기: https://okanghoon.medium.com/xcode...
앱 모듈화의 장점은 다음과 같습니다.
1. 빌드 속도를 줄일 수 있습니다.
( * 클린 빌드는 예외, 코드가 변경된 모듈만 빌드하기 때문에 빌드 속도 개선이 가능합니다. )
2. 큰 프로젝트에서 많은 개발자와 협업을 진행할 수 있는 환경입니다.
( * 기능들을 모듈로 나누고 각각 Git 서브 모듈로 구성하면 한 앱을 여러개의 프로젝트로 작업할 수 있습니다. )
3. 각 기능별 의존성을 명확하게 지정할 수 있습니다.
( * 기본 키워드인 Internal 는 다른 모듈에서 접근이 불가하기 때문에 접근하려면 명시적으로 Open 또는 Public 을 사용해야 합니다. )
4. 기능 간 결합도를 낮추어서 유연한 서비스를 제작할 수 있습니다.
( * 모듈을 통해 쉽게 다른 프레임워크로 교체할 수 있고 또한 필요한 모듈은 새로운 프로젝트에 다시 재사용할 수 있습니다. )
<리팩토링 후 링크모아 구조>
- LinkMoa: iOS 앱
- LinkMoaShareExtension: Share Extension ( 공유하기 기능 )
- LinkMoaWidget: 위젯
- LinkMoaBottomSheet: Custom Bottom Sheet 프레임워크
- LinkMoaCore: 네트워크 및 저장소 프레임워크
- LinkMoaKit: Assets, Fonts 및 Helper Class 프레임워크
링크모아에서는 Tuist 를 통해 프로젝트 머지 충돌을 해결할 수 있었고 앱 모듈화를 통해 각 코드에 대한 의존성을 명확하게 구분할 수 있었습니다.
테스트 가능한 코드로 리팩토링하기
앱 구조 리팩토링 이후에 프로젝트 코드를 검증하기 위해 테스트 코드를 작성을 시작했습니다. 다만 테스트를 작성하면서 FIRST 원칙이 전혀 지켜지지 않고 있다는 생각이 들었습니다.
네트워크는 실제 서버 API에 호출하여 매우 느리고 반복하기에는 상당히 불안정했습니다. 또한 UserDefault는 실제 앱에서 사용하는 저장소에 테스트 코드가 접근할 수 있었고 이는 전혀 고립되지 않은 테스트 환경이였습니다.
따라서 먼저 Testable 한 코드를 파악하는 능력을 기르고 리팩토링을 진행하고자 하였습니다.
WWDC 에서 학습한 테스트 가능한 코드의 특성은 다음과 같습니다.
1. 모든 입력 값들은 제어 가능해야 합니다.
2. 생성되는 출력을 검사할 수 있는 방법을 제공해야 합니다.
3. 코드의 동작의 영향을 미칠 수 있는 내부 상태에 의존하지 않습니다.
위 코드는 싱글톤을 사용하므로 전역 시스템 상태에 의존하게 됩니다. 또한 open 을 통해 실제로 원하는 동작이 수행됐는지 유닛 테스트를 통해 확인하기 어렵습니다.
이를 프로토콜과 매개변수화를 통해 testable 하게 코드를 리팩토링할 수 있습니다. 필요한 의존성을 모두 외부에서 주입하도록 합니다. 그리고 필요한 동작을 정의한 Protocol 을 가르키게 합니다.
위와 같이 변경하면 해당 Protocol 를 준수한 어떠한 객체도 주입이 가능하게 됩니다. 이때 Protocol 을 준수한 Mock 을 주입하면 행위와 상태 검증을 수행할 수 있습니다.
링크모아에서도 내부에 의존성을 가지고 있는 경우는 의존성 주입을 통해 외부에서 전달받도록 변경했습니다. 또한 모의 객체가 필요한 부분은 Protocol 를 구성하여 테스트에 필요한 Mock 객체를 만들었습니다.
테스트 코드 작성하기
유닛 테스트 작성시에 유의했던 부분은 "개발자의 관점에 따라 테스트 입력 값이 변경될 수 있다." 였습니다.
sungdoo 님, [서평] 지식 제로부터 배우는 소프트웨어 테스트: https://www.sungdoo.dev/post/review/book/...
위와 같이 양의 정수만을 문자열로 변경하는 프로그램이 있을 때, 음수가 들어가는 경우에 대한 테스트가 꼭 필요합니다.
하지만 실제 프로젝트의 함수는 더 복잡하고 많은 일을 수행합니다. 따라서 테스트 코드 작성시에는 더 작은 기능들로 분리하여 함수의 역할을 명확하게 하거나 테스트시에 어떤 값이 들어갈 수 있는지 한번 더 고민하였습니다. 또한 테스트 코드 가독성을 위해 Given, When, Then 을 구분지어 사용하는 GWT 방식을 사용했습니다.
heoblitz, iOS URLSession Test Double 만들기: https://linux-studying.tistory.com/31
heoblitz, iOS 뷰 컨트롤러 간단하게 테스트 코드 연습하기: https://linux-studying.tistory.com/33
그 결과 Kit, Core, BottomSheet 각 프레임워크에 대한 테스트 코드 작성을 하였고 평균 테스트 커버리지는 70%, 테스트 메서드 개수는 174개로 구성되었습니다.
중복되는 코드를 줄이고 재사용하기
Apple 에서는 Swift 를 프로토콜 지향 언어라 말합니다. POP 라 불리는 프로토콜 중심 프로그래밍을 통해 수평 확장이 가능하고 또한 OOP 를 통한 수직 확장을 통해 여러 중복 코드를 제거하고 재사용할 수 있습니다.
Protocol
Protocol 과 extension 을 조합하여 만들고 특정 객체에서 채택하면 cellIdentifier를 일일히 지정하지 않아도 됩니다.
위 예시도 BackgroundBlur 를 채택하면 필요한 메서드를 간단하게 확장하여 사용할 수 있습니다.
Inheritance
위 처럼 프로젝트에서 반복되어 사용되는 코드가 있다고 가정해보겠습니다. 이때 LinkMoaNavigationController 를 상속하여 사용하면 코드 중복을 상당히 줄일 수 있습니다.
혹은 UI 가 동일하지만 다른 동작이 필요할 때 상속(Inheritance) 과 재정의(Override) 를 통해 자식 객체에서 필요한 기능을 구현할 수 있습니다. 상속을 활용하면 AddLinkVC 내부에 분기처리를 제거할 수 있으므로 SOLID 의 개방-폐쇄 원칙을 준수할 수 있습니다.
더 나아가야 할 방향
UI 컴포넌트 재사용하기
웹 프론트엔드 분야에서 재사용 가능한 컴포넌트로 작업하는 것은 무척 중요합니다. 반복되는 UI를 컴포넌트화하고 이를 재사용하여 중복코드를 줄일 수 있습니다.
iOS 앱인 링크모아에서도 TableView 혹은 CollectionView 에서 사용하는 Cell 이나 Supplementary View 는 XIB 로 구현하여 다른 화면에서 재사용하고 있습니다.
하지만 동일한 Style 의 버튼이나 Label, 특정 View 들은 Storyboard 에서 각각 구현되어 있는 경우가 많습니다. 혹은 커스텀 클래스로 구현되어 있는 경우에도 @IBDesignable 어노테이션이 없어서 Storyboard 에서 바로 확인할 수 없습니다.
개발 당시에는 UI 구조화에 대한 이해도가 높은 편이 아니였고 기능 개발에만 집중했기 때문에 뷰 재사용이 미숙했다고 생각됩니다. 가능하면 리팩토링과 더불어 다음 프로젝트에서는 UI 컴포넌트 재사용에 대해서 고민해보고 작업을 할 계획입니다.
Storyboard 일 필요가 없다면 XIB 로 생성하기
링크모아는 Storyboard 가 많아 질수록 느려지는 문제를 해결하기 위해 Storyboard 하나당 한개의 Initial ViewController 가 할당되어 있습니다. 개발 당시에는 가장 좋은 방식이라고 생각했었습니다.
하지만 Storyboard 는 여러개의 뷰 컨트롤러의 전체적인 흐름을 파악하는데 장점이 있지만 링크모아와 같이 한개의 뷰 컨트롤러만 할당한다면 XIB 가 더 적합하다고 생각합니다.
그 이유는 Storyboard 는 파라미터가 필요한 ViewController를 초기화할 때 더 복잡한 방식을 거치기 때문입니다.
위처럼 스토리보드를 생성하고 다시 instantiateInitialViewController 를 통해 다시 ViewController를 생성해야 합니다. 따라서 UIKit 을 사용한 새로운 프로젝트를 진행할 때에는 XIB 로 제작해야겠다는 생각을 했습니다.
'iOS Dev > iOS' 카테고리의 다른 글
정적 분석으로 iOS 앱 구조 파악하기 (0) | 2022.12.03 |
---|---|
iOS Dependency Injection 개념 파악하기 (0) | 2021.11.03 |