Skip to content

jife-archive/RIBs-x-ReactorKIt-Example

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

searchBook


0. 서론

제가 설계한 커스텀 아키텍처는 각 RIB의 명확한 분리, 요소별 책임(화면전환, UI로직, 의존성 주입) 의 분할, 그리고 구조적 강제성이라는 RIBs의 장점을 수용했습니다. 동시에, 실제 프로젝트에서 발생할 수 있는 과도한 복잡도와 높은 유지보수 비용을 줄이기 위해, 실용성과 확장성 측면을 고려하여, 적합하다고 판단하여 재구성한 조합입니다.

물론, 제가 설계한 방식이 언제나 정답이라고 생각하지는 않습니다.

그래서 보다 객관적인 평가를 위해, 널리 사용되는 ReactorKit 단일 아키텍처 기반의 프로젝트도 함께 제출하였습니다. 이렇게 두 아키텍처를 병렬적으로 보면 제가 커스텀한 아키텍처의 장단점을 더욱 명확하게 판단하실 수 있을거라 생각했습니다.

감사합니다.


1. 빌드 방법 및 사용 프레임워크 및 라이브러리

  1. 프로젝트 파일을 다운로드하고 압축을 해제합니다.
  2. SearchBook.xcworkspace 파일을 Xcode 16.2 버전으로 실행합니다.
  3. 프로젝트 파일의 Signing & Capabilities 탭에서 본인의 Apple Developer 계정으로 서명을 설정합니다.
  4. 시뮬레이터 또는 iOS 16.0 이상의 실제 기기를 선택하여 빌드 및 실행하시면 됩니다.

사용 프레임워크 및 라이브러리

구분 내용 비고
언어 Swift 5
최소 지원 버전 iOS 16.0
Xcode Version 16.2
UI Framework UIKit
로컬 데이터 저장 UserDefaults 즐겨찾기 목록을 저장하는 데 사용

서드파티 라이브러리

구분 라이브러리명 비고
Architecture RIBs, ReactorKit
Reactive Programming RxSwift, RxCocoa
UI SnapKit, Then
Networking Moya
Image Kingfisher

2. 프로젝트 구조

2 - 1 공통 기반: 클린 아키텍처

두 프로젝트는 모두 클린 아키텍처(Clean Architecture) 를 공통 기반으로 삼습니다. 이를 통해 전체 구조를 세 가지 논리적 계층으로 분리하였습니다.

  • Data Layer: 네트워크 API, 로컬 DB 등 데이터 소스를 직접 다루며, Repository의 구현체를 포함합니다.
  • Domain Layer: 앱의 핵심 비즈니스 로직을 담당합니다. UseCase와 Entity를 포함하며, 다른 계층에 의존하지 않는 순수한 로직을 다룹니다.
  • Presentation Layer: UI와 사용자 상호작용을 처리합니다. 두 프로젝트의 핵심적인 차이가 바로 이 계층을 어떻게 구성했는가에 있습니다.

2 - 2 Presentation Layer 아키텍처 비교

A. 단일 Reactor 방식

  • 상태 관리: ReactorKit이 담당합니다. 각 화면은 자신만의 Reactor를 가지며, 사용자의 액션(Action)을 받아 상태(State)를 변경하고 UI를 갱신하는 단방향 데이터 흐름을 따릅니다.
  • 화면 흐름 제어: 코디네이터 담당합니다. 화면 간의 이동, 데이터 전달, 의존성 주입 등 내비게이션 로직을 ViewController로부터 분리하여 독립적으로 관리합니다.
  • 의존성 주입 : 앱 시작 시점에 DIContainer를 생성하여 프로젝트 전반에 필요한 의존성을 등록합니다. 코디네이터는 이 컨테이너를 통해 필요한 의존성(UseCase, Repository 등)을 가져와, 자신이 생성하는 화면(ViewController)과 Reactor에 주입하는 역할을 합니다.

B. RIBs (Router, Interactor, Builder) 방식

  • 상태 관리 + 비즈니스 로직: Interactor가 담당합니다. Interactor는 Domain 계층의 UseCase를 실행하여 비즈니스 로직을 처리하고, 화면에 필요한 상태를 관리합니다. 이 프로젝트에서는 Interactor가 ReactorKit을 소유하여 상태 관리를 위임하는 방식으로 두 패턴을 결합했습니다.
  • 화면 흐름 제어: Router가 담당합니다. 다른 RIB을 현재 RIB 트리에 붙이거나(Attach) 떼어내는(Detach) 방식으로 화면 전환을 처리합니다.
  • 의존성 주입 (DI): RIBs 아키텍처 자체의 빌더(Builder)와 컴포넌트(Component)가 담당합니다. 각 RIB의 Builder는 부모 RIB으로부터 dependency 프로토콜을 통해 필요한 의존성을 전달받습니다. 이 의존성을 사용하여 자신의 Interactor, Router 등 구성요소들을 생성하고 주입하는 계층적 DI 구조를 가집니다.

2 - 3 폴더링 구조

📁 SearchBook/
├── 📂 Sources/
│   ├── 📂 Application     # AppDelegate, SceneDelegate 등 앱의 생명주기 관리
│   ├── 📂 Core            # 네트워크, 로컬 저장소 등 앱의 핵심 기능 모듈
│   │   ├── 📂 Network
│   │   └── 📂 Storage
│   ├── 📂 Data            # [Data Layer] Repository 구현체, DTO, DAO 등
│   │   ├── 📂 Favorite
│   │   └── 📂 Search
│   ├── 📂 Domain          # [Domain Layer] UseCase, Entity, Repository 인터페이스
│   │   ├── 📂 Common
│   │   ├── 📂 Favorite
│   │   └── 📂 Search
│   ├── 📂 Feature(Present) # [Presentation Layer] 화면 단위 모듈
│   │   ├── 📂 BookList
│   │   ├── 📂 BottomSheet
│   │   ├── 📂 DetailBook
│   │   ├── 📂 FavoriteBookList
│   │   └── 📂 Root(MainTab) # 각 화면이 RIBs 또는 ReactorKit+Coordinator로 구현
│   └── 📂 Shared           # 공통으로 사용되는 UI 컴포넌트, Extension 등
│       ├── 📂 Base
│       ├── 📂 DesignSystem  # 커스텀 뷰, 컴포넌트 등
│       ├── 📂 Extensions
│       └── 📂 Utils
└── 📂 SearchBook.xcodeproj

3. 구현 기능 소개

3.1. 탭 바 (Tab Bar)

  • 검색 탭
  • 즐겨찾기 탭

3.2. 도서 검색 탭 (기능)

  • 실시간 도서 검색: 카카오 책 검색 API와 연동하여 사용자가 입력하는 검색어를 기반으로 실시간 검색 결과를 제공합니다.
  • 카드형 UI: 검색된 도서는 이미지, 제목, 저자, 출판사, 출간일, 가격 등 핵심 정보가 포함된 직관적인 카드 형태로 표시됩니다.
  • 정렬 기능: 사용자는 바텀시트(Bottom Sheet)를 통해 정확도순/ 발간일순으로 검색 결과를 정렬할 수 있습니다.
  • 페이지네이션(Paging): 한 번에 20개의 검색 결과를 불러오며, 스크롤을 끝까지 내리면 다음 페이지를 자동으로 로드합니다.
  • 즐겨찾기 및 상태 연동: 각 도서 카드에 있는 버튼을 통해 손쉽게 즐겨찾기에 추가하거나 해제할 수 있습니다. 이 정보는 기기 내 로컬 저장소에 안전하게 보관되며, 즐겨찾기 탭에서 바로 확인할 수 있습니다.
  • 상세 화면 이동: 특정 도서 카드를 선택하면 해당 도서의 상세 정보를 볼 수 있는 화면으로 이동합니다.

3.3. 즐겨찾기 탭 (기능)

  • 즐겨찾기 목록 표시: 사용자가 '도서 검색' 탭에서 즐겨찾기한 도서들을 모아서 보여줍니다. 검색 화면과 동일한 카드 UI를 사용하여 일관성을 유지합니다.
  • 로컬 데이터 검색: 저장된 즐겨찾기 목록 내에서 제목을 기준으로 원하는 도서를 빠르게 검색할 수 있습니다.
  • 정렬 및 필터링: 사용자는 바텀시트(Bottom Sheet)를 통해 다양한 조건으로 목록을 재구성할 수 있습니다.
    • 정렬: 도서 제목을 기준으로 오름차순/내림차순 정렬
    • 필터: 사용자가 지정한 금액 범위(Price Range) 에 해당하는 도서만 필터링
  • 즐겨찾기 관리: 목록 내 각 카드에서 즉시 즐겨찾기를 해제할 수 있습니다.
  • 상세 화면 이동: 검색 탭과 마찬가지로, 도서 카드를 선택하면 상세 화면으로 이동합니다.

3.4. 도서 상세 화면

  • 상세 정보 제공: 리스트에서 선택된 도서의 큰 이미지, 제목, 저자, 역자, 출판사, 가격, 상세 설명 등 풍부한 정보를 제공합니다.
  • 즐겨찾기 상태 연동: 상세 화면 내에서도 즐겨찾기 상태를 확인하고 변경할 수 있으며, 이 변경 사항은 앱의 모든 화면에 실시간으로 반영됩니다.

4. 주요 구현 포인트, 커스텀 Protocol 및 Extension 문서

4.1. 비동기 처리 전략: async/await와 RxSwift의 공존

저는 사내 프로젝트의 기술 전환 방향을 고민하던 중, 전체 시스템을 한 번에 바꾸기보다 안정적인 단계별 전환이 필요하다고 판단했습니다.

그 첫 단계로, 전체 아키텍처의 계층을 명확히 분리하고 Data와 Domain 계층에 먼저 Swift Concurrency를 도입했습니다. 이를 통해 RxSwift의 사용은 Presentation 영역에 한정시켜, 핵심 비즈니스 로직을 특정 UI 프레임워크로부터 독립시켰습니다.

이렇게 설계한 가장 큰 이유는, 추후 Presentation 영역을 SwiftUI와 Combine으로 전환할 것을 염두에 두었기 때문입니다. 현재는 ReactorKit의 장점을 활용하면서도, 미래에는 더 적은 노력으로 Presentation Layer를 현대적인 스택으로 교체할 수 있는 유연성을 확보한 것입니다.

이러한 계층별 전략 분리로 인해, async/await로 작성된 Domain의 UseCaseRxSwift 기반의 Reactor가 호출하기 위한 '브릿지(Bridge)' 역할의 코드가 필요했고, 이를 위해 아래와 같은 커스텀 Extension을 구현했습니다.

4.2. 커스텀 Single Extension

  • Single.fromAsync(_:)
    • async/await로 작성된 함수를 RxSwiftSingle 스트림으로 변환하는 정적(static) 함수입니다.
    • 역할
      • Domain 계층의 async throws 함수를 감싸 Presentation 계층의 Reactor가 소비할 수 있는 Single 시퀀스로 만들어주는 브릿지 역할을 수행합니다.
  • mapToMutation(success:error:)
    • Single 스트림의 결과를 ReactorKitMutation으로 변환하는 과정을 단순화하는 함수입니다.
    • 역할
      • UseCase의 실행 결과를 Single로 맵핑한 후, 성공(success) 케이스와 실패(error) 케이스를 각각 적절한 Mutation으로 매핑하는 상용구 코드를 줄여줍니다.

4.3. UI 이벤트 충돌 방지를 위한 Rx Extension

문제 상황: 바텀시트 내 슬라이더 터치 이벤트 충돌

가격 범위를 설정하는 필터 바텀시트(Bottom Sheet) 내부에 커스텀 SBRangeSlider가 위치합니다. 이때 사용자가 슬라이더를 좌우로 드래그하는 제스처와 바텀시트를 아래로 끌어내려 닫는 제스처가 충돌하여, 슬라이더를 조작하는 도중 바텀시트가 닫히는 사용성 문제가 발생했습니다

해결 방안: isTracking Observable Extension 구현

이 문제를 해결하기 위해, 슬라이더에 대한 사용자의 터치 상태를 외부로 알려주는 Rx Extension을 구현했습니다.

  • SBRangeSlider.rx.isTracking: 커스텀 슬라이더의 onTrackingStateChanged 클로저를 활용하여, 사용자가 슬라이더를 터치하기 시작하면 true, 터치를 떼면 false를 방출하는 Observable<Bool> 스트림을 생성합니다.
  • 이벤트 제어
    • 필터 바텀시트는 이 isTracking 스트림을 구독합니다. 스트림으로부터 true 값이 전달되면 (사용자가 슬라이더를 조작 중이면) 바텀시트의 드래그 제스처를 일시적으로 비활성화하고, false 값이 전달되면 다시 활성화합니다.

4.4. 제네릭 프로토콜을 이용한 기능 재사용

정렬 기능의 공통적인 부분을 제네릭 프로토콜로 묶어 추상화했습니다. SortableReactor라는 프로토콜을 중심으로, 정렬 기능이 필요한 Reactor가 어떤 상태(State)와 동작(Action)을 가져야 하는지를 정의했습니다.

  • SortOptionType: 정렬 옵션으로 사용될 enum이 반드시 가져야 할 속성(문자열 이름, 순회 가능 등)을 정의한 프로토콜입니다. BookListSortTypeFavoriteListSortType이 이를 따릅니다.
  • SortableReactor: 정렬 기능이 필요한 Reactor가 채택하는 핵심 프로토콜입니다. associatedtype을 통해 어떤 종류의 SortOption을 사용할지 지정할 수 있어, 각기 다른 정렬 옵션을 가진 Reactor들이 동일한 인터페이스를 가질 수 있게 됩니다.
  • SortStateProvider / SortActionProvider: SortableReactorStateAction이 반드시 포함해야 할 요소(전체 정렬 옵션 목록, 현재 선택된 옵션, 정렬 옵션 변경 액션 등)를 각각 정의합니다.
  • RIBs x ReactorKit 아키텍처에서는 SortableReactor 프로토콜을 사용하지 않습니다. RIBs 아키텍처는 Listener 패턴을 통해 부모와 자식 RIB 간의 데이터 흐름이 명확하게 정의되어 있기 때문입니다. 즉, 정렬 바텀시트 RIB이 자신의 Listener를 통해 부모 RIB(검색 RIB, 즐겨찾기 RIB)에게 정렬 옵션이 변경되었음을 알리면 되므로, 별도의 프로토콜 없이도 충분히 기능을 재사용하고 확장할 수 있기 때문에, 해당 프로토콜을 사용하지 않았습니다.

About

Search sample app built with Concurrency, RIBs, ReactorKit

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Languages