paneiOS / DDaRa_Rx

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

MVVM-C/Rx를 적용한 라디오 프로젝트

목차

📻 프로젝트 소개

다양한 방송국을 보유하고있는 라디오앱입니다.

네트워크통신으로 서버에서 데이터를 받아 CollectionView로 라디오화면을 만들고 TableView 로 즐겨찾기화면을 만들었습니다.

대부분의 스트리밍URL은 공식홈페이지에서 개발자모드로 찾았으나 특정 방송국에서는 1차로 요청을 해야하는 API주소가 있었습니다. 특정 방송국의 경우 .pls파일을 다운받지 않고 데이터를 변환하여 스트리밍URL(.m3u8)을 찾아야 했으며 Json타입에서 스트리밍URL을 사용하였습니다.

MainTabBar로 3가지화면(설정화면을 포함하여)을 묶고 1개의 PlayStatusView를 공유하고있습니다.

MVVM-CCleanArchiTecture 를 적용했습니다.

Test자동화를 위해 Git Actions을 적용했습니다.

사용한 라이브러리: RxSwift, RxCocoa, RxDataSources, SnapKit, Kingfisher, Nimble

  • 참여자 : Pane @kazamajinz (1명)

1. MenuBar 2. 목록 스크롤 3. 재생화면 4. 외부제어 5. 자동종료화면

📻 Architecture

image

📻 Foldering

├── DDaRa
│   ├── App
│   ├── Data
│   │   ├── NetworkProvider
│   │   ├── ServiceAPI
│   ├── Domain
│   │   ├── DefaultStationsUseCase
│   ├── Presentation
│   │   ├── MainTabBarView
│   │   ├── SubComponents
│   │   │   ├── StationView
│   │   │   │   ├── View
│   │   │   │   ├── ViewModel
│   │   │   ├── FavoriteView
│   │   │   │   ├── View
│   │   │   │   ├── ViewModel
│   │   │   ├── PlayStatusView
│   │   │   │   ├── View
│   │   │   │   ├── ViewModel
│   │   │   ├── ActionSheetView
│   │   │   │   ├── View
│   │   │   │   ├── ViewModel
│   │   │   ├── Setting
│   │   │   │   ├── SettingViewController
│   │   │   │   ├── SubComponents
│   │   │   │   │   ├── SleepSettingViewController
│   ├── Common
│   │   ├── Protocol
│   │   ├── Timer
│   ├── Extension
│   └── Extension+Rx
└── DDaRaTestsTests
    └──Mock

📻 Feature-1. Architecture에 대한 고민

1-1 고민한 점

1️⃣ MVVM-C, Clean Architecture + MVVM 적용

명확한 계층분리를 위해 MVVM구조에서 Coordinator를 통해 view들의 계층을 관리하며 의존성을 주입했습니다. UseCaseNetworkService를 주입하고 ViewModel은 UseCase를 주입하고 가독성과 유지보수를 위해 프로토콜 ViewModel을 채택하여 Input/Output을 적용하였습니다. NetworkProvider에서 서버와의 통신에서는 URLSession을 주입하여 작동하지만 Test시에는 MockURLSession을 주입하여 작동합니다. 간단한 로직을 구현하는데 상당히 많은 양의 클래스가 필요했습니다. 이를위해 필요없는 요소를 축약하고 통합하였습니다.

2️⃣ ViewModel에서 RxCocoa를 쓰면 안티패턴인가

ViewModel에서 RxCocoa를 import하고 있는데 RxCocoa를 import를 하고 있는게 안티패턴인가는 생각이 들었는데 많은 스타를 받은 다른분들의 MVVM의 아키텍처를 보면 ViewModel에서 input, output를 Driver로 전달하고 있는 예제들이 많이 있었습니다. 개인적인 생각은 써서 문제가 있다기보다는 필요성이 있을때 쓴다면 문제가 없는것 같은데 ViewModel에서 UI작업을 위한 메인스레드 작업할 일이 있는 경우가 당장 생각나진 않않습니다.

📻 Feature-2. 네트워크 구현

2-1 고민한 점

1️⃣ Unit Test

MockURLSession을 구현한 이유

  1. 실제 서버와 통신할 경우 테스트의 속도가 느려짐
  2. 인터넷 연결상태에 따라 테스트 결과가 달라지므로 테스트 신뢰도가 떨어짐
  3. 실제 데이터와 테스트를 통신을 하게 되면 불필요하게 업로드가 되는 Side-Effect를 방지할 수 있음.
  4. JSON파일로 추가함으로 데이터를 추가하기가 용이함.

2️⃣ API 추상화

배민의 기술블로그에서는 Alamofire를 한번 더 추상화하여 구현된 라이브러리인 Moya를 이용하여 UnitTest를 사용하고 Quick/Nimble을 사용하면 더 편하다고 언급하고 있습니다. DDaRa에서는 Nimble은 사용하고있지만 Moya는 사용하고 있지 않습니다. 이전 프로젝트에서 MoYa를 이용해서 열거형으로 만들었었으나 API추가할때마다 case가 늘어나고 switch문을 매번 수정하는게 생각보다 불편하여 이번에는 URLSession을 직접 만들고 불편한 부분을 개선해보았습니다.

  1. API마다 독립적인 구조체 타입으로 관리되도록 만듬.(ex StationListAPI, StreamingAPI)
  2. URL 프로퍼티 외에도 HttpMethod 프로퍼티를 추가한 APIProtocol타입을 채택
  3. 협업시에 각자 담당한 API 구제초만 관리하면 되기 때문에 충돌을 막을 수 있음.
  4. 현재 post타입은 사용하고 있지않지만 추가작업을 위해 추가해놓음.

2-2 Trouble Shooting

1️⃣ Mock 데이터 접근 시 Bundle에 접근하지 못하는 문제

  • 문제점 : JSON Decoding 테스트를 할 때, Bundle.main.path를 통해 Mock 데이터에 접근하도록 했는데, path에 nil이 반환되는 문제가 발생했습니다. LLDB 확인 결과 Mock 데이터 파일이 포함된 Bundle은 DDaRaTests.xctest이며, 테스트 코드를 실행하는 주체는 DDaRa App Bundle임을 파악했습니다.
  • 해결방법 : 현재 executable의 Bundle 개체를 반환하는 Bundle.main (즉, App Bundle)이 아니라, 테스트 코드를 실행하는 주체를 가르키는 Bundle(for: type(of: self)) (즉, XCTests Bundle)로 path를 수정하여 문제를 해결했습니다.

📻 Feature-3. 재생상태 하단바 구현

3-1 고민한 점

1️⃣ PlayStatusView 공유

일반적인 Music앱들을 보면 하단의 재생창을 공유하고있다. 그렇기에 뷰의 계층안에서는 PlayStatusView가 가장 위에 있게 하는것이 목표였다. 단순하게 View를 최상위로 올리는것만 아니라 다른 View(StationView, FavoriteView, SettingViewController)에서 음악을 재생하였을때 PlayStatusView에서도 음악재생에 맞는 기능이 작동해야됩니다.

  1. StationView와 FavoriteView에서 음악을 재생하면 PlayStatusVie로 전달되야합니다.
  2. 전달받으면 PlayStatusView에서는 커버이미지, 제목, 재생애니메이션, 상단의 상태창(MPNowPlayingInfoCenter)을 업데이트하고 PlayStatusViewModel에서는 AVPlayer를 통해 재생과 정지 작업을 한다.

3-2 Trouble Shooting

1️⃣ Mock PlayStatusView를 여러곳에서 bind하기 때문에 생기는 중복 스트림 방출

  • 문제점 : 즐겨찾기에서 재생시에 2번 재생버튼이 눌리며 노래가 재생후 바로 일시정지하는 상태가 발생했습니다. .debug()를 통해 중복 스트림이 발생하는 것을 확인했습니다.
  • 해결방법 : .share()를 추가하여 1번만 발생하도록 수정하였습니다.

📻 Feature-4. 방송국화면 구현

4-1 고민한 점

1️⃣ RxDataSources 사용

처음에는 DiffableDataSource를 사용하려고 하였으나 DiffableDataSource는 다른프로젝트에서 많이 사용해봤기 때문에 이번에는 RxDataSources를 사용하였습니다. 기본적으로 CollectionView에 나타낼 데이터 타입은 DiffableDataSource와 같이 Hashable을 채택하여 구분해야했습니다. SectionHashable를 채택하여 Dictionary로 재구성하였고 SectionValueSection의 헤더 타이틀로 나타냈습니다.

2️⃣ PlayStatuView의 위치

Coordinator에서 모든 화면의 ViewControllerViewModel을 초기화하여 의존성을 관리하고, 화면 전환을 담당하도록 구현했습니다. 이때 화면 전환에 필요한 작업은 Coordinator에서 정의하여 클로저 타입의 변수로 구성된 action에 저장해두고, ViewModel에서 해당 action에 접근하여 클로저를 실행하도록 했습니다.

3️⃣ Observable Subscribe 최소화

Stream이 발생하는 경우, Observable을 최종 사용하는 위치에서만 Subscribe하여 Stream이 끊기지 않도록 구현했습니다. 따라서 Observable을 생성하고 이를 처리하는 중간 단계에서는 flatmap, map, filter, compactMap 등을 사용하여 필요한 형태로 변경만 해준 뒤 Observable 타입을 반환하도록 구현했습니다.

4️⃣ Flow Coordinator 활용

Coordinator에서 모든 화면의 ViewControllerViewModel을 초기화하여 의존성을 관리하고, 화면 전환을 담당하도록 구현했습니다.

📻 Feature-5. 즐겨찾기화면, 방송상세화면 구현

5-1 고민한 점

1️⃣ TableView 사용

즐겨찾기 화면의 경우 이용자는 듣는게 우선적인 목표이기에 단순하고 간단하게 만들기 위해 TableView를 사용하였습니다.

2️⃣ 상세화면은 ActionSheet 활용

ActionSheet에 재생과 좋아요를 넣어 모든것을 조작할 수 있도록 구현했습니다. ActionSheet를 사용한 이유는 라디오를 실행할때 상세화면으로 한번 더 진입하는것이 유저에게는 불편할 수 있기때문에 ActionSheet를 활용하였습니다. 화면창을 커스텀하여 팝업하는 방법도 있지만 ActionSheetView를 넣어보고싶어서 ActionSheet로 진행해보았습니다.

5-2 Trouble Shooting

1️⃣ 초기화면 설정

  • 문제점 : 테스트과정에서 초기 이용자입장에서는 데이터가 많은 StationView가 좋지만 즐겨찾기를 이용하는 이용자입장에서는 한번 이동해야하는 번거로움이 있다는 피드백을 받았습니다.
  • 해결방법 : FolwCoordinator로 관리하기 때문에 FlowCoordinator에서 즐겨찾기의 유무로 초기화면을 바뀌도록 구현했습니다.

2️⃣ TableView와 ActionSheet의 좋아요버튼

  • 문제점 : 좋아요 버튼을 누를때 ActionSheet의 좋아요가 눌리면 TableView의 좋아요 버튼이 업데이트가 되어야 했습니다.
  • 해결방법 : TableView의 전체 리로드를 피하기 위해 ActionSheet가 나온 cellindex만 리로드하여 업데이트하였습니다.

📻 Feature-6. 설정화면 구현

6-1 고민한 점

1️⃣ Flow Coordinator 활용(화면전환, 자동꺼짐 설정)

화면 전환에 필요한 작업과 자동꺼짐에 필요한 작업 2가지를 Coordinator에서 정의하여 클로저 타입의 변수로 구성된 SleepSettingAction 저장해두고, ViewController에서 해당 action에 접근하여 클로저를 실행하도록 했습니다.

2️⃣ 자동꺼짐 설정

Flow Coordinator에서 전달한 action을 통해서 위의 정지 기능을 클로저로 실행합니다. 자동으로 꺼지기 위해서는 PlayStatusView의 UI 정지상태 업데이트, PlayStatusViewModel의 AVPlayer 정지, 외부 Controller 정지가 필요합니다.

6-2 Trouble Shooting

1️⃣ Timer 백그라운드

  • 문제점: Timer()의 scheduler를 이용하여 타이머를 만들어서 백그라운드로 이동할경우 정상적으로 방송이 멈추지 않았습니다.
  • 해결방법: Timer()의 scheduler를 사용하지 않고 DispatchSourceTimer를 사용하여 해결하였습니다.

📻 Feature-7. 심사상태(리젝)

7-1 고민한 점

1️⃣ 타사의 StreamURL을 사용

심사를 받았으나 방송국의 Streaming 서비스를 이용하는 것은 법적으로 문제가 없음을 증명해야한다는 이유로 리젝되었습니다. 추가적으로 알아본 결과 보통 방송국마다 라이센스가 필요하다고 합니다. 많은 방송사들이 들어있기 때문에 출시를 포기하였습니다.

2️⃣ 아이패드

DDaRa의 경우 아이패드의 경우는 고려하지 않았지만 작년에 애플에서 아이패드를 사용하지 않는 어플이어도 정상작동해야한다고 하였습니다.

7-2 Trouble Shooting

  • 문제점: 아이패드로 실행시 ActionSheet가 Alert으로 팝업되었다. Alert로 팝업되면서 기본크기이기 떄문에 오른쪽 텍스트가 잘리는 현상이 구현되었습니다.
  • 해결방법: Alert으로 구현하지 않고 View를 커스텀하여 팝업창처럼 만들어 해결하고자하였습니다. 다만 법적문제를 해결할 방법이 없어 출시포기로 팝업창을 구현하지 않았습니다.

📻 Feature-8. 추가이슈

8-1 Trouble Shooting

1️⃣ 깃 폴더 대소문자

  • 문제점: 깃에서 새로 다운받아서 빌드할 경우 cell의 배경색이 적용되지 않았습니다. Assets에서 color를 설정해놨는데 black를 한번 Black로 만들어서 깃에 올린 이력이 있었습니다. 깃은 폴더명, 파일명이 대소문자를 가리지 못합니다. Assets를 설정하면 black.colorset이름의 폴더가 생성되는데 이때 Black.colorset로 생성되고 수정하여도 변동되지 않았습니다.
  • 해결방법: 1. git mva -> C -> A 와같이 바꿔준다. 2. `git config core.ignorecase false를 설정하고 git rm -r --cached . 후 커밋을 하면 해결됩니다.

2️⃣ 유닛 테스트

  • 문제점: Mock에서 URLSessionDataTaskinit()deprecated되어 14.0이상의 기기에서 Test에 성공한것처럼 나오지만 실제로는 테스트가 되지 않았습니다.
  • 해결방법: URLSessionDataTask을 상속받아서 Mock을 만들고자하는 과정에서 생긴 문제였기에 URLSessionDataTask프로토콜로 만들어서 만들고 URLSessionextension에 기본메서드를 dataTask를 구현하였습니다. 그리고 URLSessionDataTask을 반환하는곳을 전부 URLSessionDataTaskProtocol로 반환하도록 변경하여 해결했습니다.

3️⃣ Git Actions(미해결)

  • 문제점: Atifact.ipa파일을 올리는것은 가능하지만 테스트플라이트로 올리는것은 안되는 점, 에러내용(Failed to generate JWT token)
  • 추가시도한방법
  1. APPSTORE_API_PRIVATE_KEYbase64디코딩~/private_keys/ 경로에 Auth_를 붙여서 파일을 저장하고 xcrun altool명령어로 시도.
  2. 2번은 명령어로 디코딩을 하였으나 직접 디코딩한 다음 secret에 추가하여 값을 직접 사용하여 시도.
  • 해결을 위한 시도: 현재 gitLab 커뮤니티에 질문을 올린 상태다.

About


Languages

Language:Swift 99.6%Language:Ruby 0.4%