- 이전
- 문제점
- 모델
- iOS와 위젯이 같은 모델을 사용하는 데 사용하는 방법은 조금씩 다름
- 그래서 iOS의 로직을 변경하고 싶어도 변경하지 못하거나, 기존 코드를 지우지 못함
- iOS와 위젯이 같은 모델을 사용하는 데 사용하는 방법은 조금씩 다름
- 로직
- 데이터를 변경하는 로직이 흩어져있어 로직을 파악하기 어려움
- 결합도가 높아 하나의 메서드를 고치기 위해 전체적으로 수정해야 함
-
class MainViewModel: ObservableObject { /// 현재 선택된 요일 @Published var selectedWeekComponent: WeekComponent? = .today /// 네트워크 요청을 통해 받은 응답 목록 @Published var cafeteriaResponseArray = [CafeteriaResponse]() /// 모달 시트 여부 @Published var isSheetShow = false /// 선택한 캠퍼스 @Published var selectedCampus: Campus = .부산 { didSet { fetchCafeteriaArray() } } /// 사용자가 설정한 앱 시작 시 기본으로 보여줄 캠퍼스 @Published var defaultCampus: Campus = .부산 { didSet { saveDefaultCampus() } } /// 사용자가 설정한 앱 시작 시 먼저 보여줄 식당 목록 @Published var bookmark: [Cafeteria] = [] { didSet { saveBookmark() } } // ... }
- 네트워킹
- 모델과 같은 이유로 request 모델의 흐름을 파악하기 어려움
- 하드 코딩에 가까워서 다른 api에서 사용하기 힘듦
-
struct FilterByCampusRequest: Codable { init(queryType: QueryType, campus: Campus) { // 중략 self.filter = Filter(and: [Filter.Or(or: code), Filter.Or(or: condition)]) } init(queryType: QueryType, name: String, category: String) { // 중략 self.filter = Filter(and: [Filter.Or(or: code), Filter.Or(or: [condition]), Filter.Or(or: [categoryType])]) } var filter: Filter struct Filter: Codable { var and: [Or] struct Or: Codable { var or: [ConditionalExpression] struct ConditionalExpression: Codable { var property: String var rich_text: RichText struct RichText: Codable { var equals: String } } } } }
- MVVM
- MVVM을 적용했다고 생각하고 있었는데, 위 문제들이 발생하는 게 코드를 모델, 뷰, 뷰 모델로 나누기만 했을 뿐 아키텍처를 제대로 적용하지 못하고 있기 때문이라고 생각
- 모델
- 문제점
- 개선
- MVVM + 클린 아키텍처
- 일전에 학습한 프로젝트를 베이스로 리팩토링
- 목표
- 단계 별 추상화 및 역할의 분리
- 뷰
- 이벤트에 대한 로직을 보유하지 않고, 이벤트가 발생했다는 걸 뷰 모델에 전달
- 뷰 모델의 데이터를 표현만 함
- 뷰 모델
- init 시 AppState의 데이터 바인딩
- 뷰로부터 전달받은 이벤트를 서비스를 통해 수행
- 서비스
- 상세한 로직 구현
- 수행 결과를 앱 상태 혹은 뷰 모델로 전달
- 직접 데이터를 보유하지 않음
- 레포지터리
- 네트워킹, 유저 데이터와 같은 데이터와 관련한 로직 담당
- 앱 상태(AppState)
- 현재 앱의 상태를 나타냄
- 예를 들어, 현재 선택된 캠퍼스 혹은 요일 탭
- 뷰
- 의도한 바
- 뷰: 내가 보여줄 건 이거(뷰 모델의 데이터)고, 이 이벤트는 쟤(뷰 모델)가 할 거야
- 뷰 모델: 쟤(뷰)가 시킨 건 얘(서비스)가 해서 알려줄 거야.
- 서비스: 이건(이벤트) 이런 방식으로 하면 되고, 저건(데이터 이벤트) 쟤(레포지터리)가 해줄 거야.

- 단계 별 추상화 및 역할의 분리
- 목표
- 일전에 학습한 프로젝트를 베이스로 리팩토링
- 네트워킹
- 기존 콜백 함수를 continuation으로 래핑 하여 사용하고 있던 걸 concurrency로 교체
- 로직
- didSet, onChange를 combine으로 뷰 모델 Init 시 파이프라인 연결 후 가공하도록 수정
- MVVM + 클린 아키텍처
- 이후
- 결과
- iOS와 위젯 분리
- 공통으로 사용하던 모델, 로직을 위젯 용, iOS 용으로 분리해서 사용하게 변경
- 같은 파일이 위젯과 iOS에 각각 있음
- 추후, 추상화해서 확장해서 사용할 생각

- 데이터 관리
- 전역적으로 사용하는 데이터는 AppState에서 보유, 뷰 모델에서 연결
-
init(container: DIContainer) { self.container = container let appState = container.appState self._bookmark = .init(initialValue: appState.value.userData.bookmark) self._filterdCafeteriaResponseArray = .init(initialValue: appState.value.cafeteria.filterByDay) self.cafeteria = appState[\.cafeteria.list] bind() } func bind() { let appState = container.appState cancelBag.collect { appState.map(\.cafeteria.filterByDay) .removeDuplicates() .assign(to: \.filterdCafeteriaResponseArray, on: self) // 후략 } }
- 공통으로 사용하던 모델, 로직을 위젯 용, iOS 용으로 분리해서 사용하게 변경
- 네트워킹
- Concurrency로 교체 및 Moya 제거 후 URLSession 사용
- request 모델을 좀 더 재사용 가능하게 변경
-
protocol ConditionOperator: CustomStringConvertible { } struct SingleFilter: CustomStringConvertible { let filter: any ConditionOperator init(_ filter: any ConditionOperator) { self.filter = filter } var description: String { """ { "filter": \(filter) } """ } } struct MultiFilter: CustomStringConvertible { let filter: [any ConditionOperator] var description: String { """ { "filter": [ \(filter.map { $0.description + ",\n" }) ] } """ } } struct And: ConditionOperator, CustomStringConvertible { let and: [any ConditionOperator] init(_ and: [any ConditionOperator]) { self.and = and } var description: String { """ { "and": \(and) } """ } } struct Or: ConditionOperator, CustomStringConvertible { let or: [any ConditionOperator] init(_ or: [any ConditionOperator]) { self.or = or } var description: String { """ { "or": \(or) } """ } } struct RichTextExpression: ConditionOperator, CustomStringConvertible { let property: String let rich_text: RichText init(property: String, rich_text: String) { self.property = property self.rich_text = .init(equals: rich_text) } struct RichText: Codable { let equals: String } var description: String { """ { "property": "\(property)", "rich_text": { "equals": "\(rich_text.equals)" } } """ } }
- iOS와 위젯 분리
- 결과
- 후기
-

- 코드를 분리해서 수정이 있을 때 개발하기 좀 더 편해짐
- 레이어가 깊어질수록 구현의 정도가 높아져서 서비스나 레포지터리의 변경 때문에 뷰나 뷰 모델의 코드를 변경하지 않아도 됨
- 아키텍처 개념이 부족한 상태에서 시작해서 중간에 브랜치 날리고 처음부터 했음
- AppState에서 데이터를 관리하는데, 전역적으로 관리가 필요한 데이터가 아니면 이 프로젝트처럼 Input, Output으로 전달해도 좋을 것 같다.
- 테스트 코드를 도입할 생각이었는데, 리팩토링이 끝난 마당에 도입하긴 애매한 것 같다
- 만약, 다시 리팩터링 한다면 요구사항에 맞는 테스트 코드를 작성하고 리팩토링 할 듯
- 감명 깊게 본 영상
- Robert C Martin - Clean Architecture and Design
-
'Project > 뿌대식: 부산대학교 학식 알리미' 카테고리의 다른 글
| 뿌대식: CI/CD (0) | 2024.12.01 |
|---|---|
| 뿌대식: 네트워크 관련 개선 (1) | 2024.08.23 |
| 뿌대식: 네트워크 분석 (0) | 2024.08.19 |
| 뿌대식: 뷰 컴포넌트 분리 (0) | 2024.08.18 |
| 뿌대식: 응답 모델 개선 (0) | 2024.08.18 |
