일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- react
- Privacy manifest
- requirenativecomponent
- boilerplate 제거
- 라이브러리 없이
- Android
- launch screen
- React-Native
- React Native
- 360도 뷰어
- 3b52.1
- 360도 이미지 뷰어
- 리액트 네이티브
- ios
- launchscreen
- native
- 스플래시스크린
- 리액트
- 스켈레톤 UI
- 앱 성능 개선
- Skeleton UI
- 스켈레톤 통합
- react-native-fast-image
- 파노라마 뷰
- 360도 이미지
- 리엑트 네이티브
- privacyinfo.plist
- Native Module
- panorama view
- 네이티브
- Today
- Total
Neoself의 기술 블로그
SwiftUI에서 MVVM + 서비스 레이어 패턴 구현하기 본문
SwiftUI에서 MVVM (Model-View-ViewModel) 패턴을 어떻게 효과적으로 구현할 수 있는지, 제가 이전에 진행했던 프로젝트인 "TyTE"를 예로 들어 설명해드리고자 합니다.
MVVM은 사용자 인터페이스 로직을 비즈니스 로직과 분리하여 개발하는 아키텍처 패턴입니다. 이 패턴은 다음과 같은 세 가지 주요 구성 요소로 이루어져 있습니다.
- Model: 데이터 담당
- View: 사용자 인터페이스를 표현
- ViewModel: View와 Model 사이의 중개자 역할, UI 로직 처리
TyTE 프로젝트의 경우 위 MVVM 패턴에서 데이터 처리, 네트워크 통신과 같은 데이터접근 함수들을 담당하는 서비스 레이어을 추가적으로 분리하여 전체 프로젝트 구조를 정립하였는데요. 이 구조는 다음과 같이 도식화 해볼 수 있습니다.
사용자는 View(User Interface)와 직접적인 상호작용을 진행하게 됩니다. 이때 View는 ViewModel을 @ObservedObject를 통해 관찰하고 있기 때문에, 사용자의 입력을 ViewModel에 전달할수 있게 되며, ViewModel은 사용자 입력에 따라 앱의 데이터가 포함된 Model을 업데이트하는 일종의 중간다리 역할을 담당하게 됩니다.
만약에 MVVM 패턴을 따르지 않고, 컴포넌트를 구성하게 되면 어떻게 될까요?
struct BadHomeView: View {
@State private var todos: [Todo] = []
@State private var todosForDate = []
@State private var isLoading = false
@State private var sortOption = "default"
@State private var isBottomSheetPresented:Bool = false
@State private var isPopupPresented:Bool = false
...
var body: some View {
VStack {
if isLoading {
ProgressView()
} else {
List(filteredTodos) { todo in
Text(todo.title)
.onTapGesture {
toggleTodo(todo)
}
}
}
Button("정렬 변경") {
changeSortOption()
}
}
.onAppear {
fetchTodos()
}
}
private var filteredTodos: [Todo] {
return todos
}
private func fetchTodos() {
isLoading = true
// 여기에 네트워크 요청 코드...
// URLSession.shared.dataTask(...) 등
}
private func toggleTodo(_ todo: Todo) {
// 여기에 Todo 상태 변경 및 서버 업데이트 코드...
}
private func changeSortOption() {
// 정렬 옵션 변경 및 데이터 재정렬 로직...
}
}
위 코드는 프로젝트 극 초창기에 제가 진행했던 구조인데요... MVVM 패턴을 따르지 않고, View 파일 내부에 데이터 fetching, 상태 관리, UI 로직을 모두 처리하고 있는 코드입니다. 위 코드의 경우 아래와 같은 문제점을 뽑아볼 수 있습니다.
- 재사용성 저하: 이 View는 다른 곳에서 재사용하기 어렵습니다. 따라서 레이아웃이 유사한 다른 화면을 구현하고자 할때에도, 처리 로직이 조금이라도 다르면 Boilerplate가 발생할 수밖에 없습니다.
- 확장성 부족: 새로운 기능을 추가하거나 기존 기능을 수정하기 어렵습니다.
- 상태 관리의 복잡성: @State를 과도하게 사용하여 상태 관리가 복잡해집니다.
그러면 이제, 제가 진행했던 프로젝트의 MVVM 패턴 + 서비스 레이어 구조를 보여드리도록 하겠습니다.
1. Model
struct Todo: Identifiable, Codable {
let id: String
let user: String
var tagId: Tag?
var raw: String
var title: String
var isImportant: Bool
var isLife: Bool
var difficulty: Int
var estimatedTime: Int
var deadline: String
var isCompleted: Bool
}
2. ViewModel
class HomeViewModel: ObservableObject {
@Published var selectedTags: [String] = []
@Published var sortOption: String = "default"
@Published var currentTab: Int = 0
private let todoService: TodoService
private let sharedVM: SharedTodoViewModel
@Published var isLoading: Bool = false
private var cancellables = Set<AnyCancellable>()
init(
sharedVM: SharedTodoViewModel,
todoService: TodoService = TodoService()
) {
self.sharedVM = sharedVM
self.todoService = todoService
fetchTodos()
self.selectedTags = sharedVM.tags.map{$0.id}
}
var filteredTodos: [Todo] {
// 필터링 로직...
}
func fetchTodos() {
isLoading = true
todoService.fetchAllTodos(mode: sortOption)
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
// 에러 처리...
} receiveValue: { [weak self] todos in
// 데이터 처리...
}
.store(in: &cancellables)
}
// 기타 메서드들...
}
3. Service Layer
class TodoService {
private let apiManager = APIManager.shared
func fetchAllTodos(mode:String) -> AnyPublisher<[Todo], APIError> {
let endpoint = APIEndpoint.fetchTodos(mode)
return Future { promise in
self.apiManager.request(endpoint) { (result: Result<[Todo], APIError>) in
promise(result)
}
}.eraseToAnyPublisher()
}
func fetchTodosForDate(deadline: String) -> AnyPublisher<[Todo], APIError> {
let endpoint = APIEndpoint.fetchTodosForDate(deadline)
return Future { promise in
self.apiManager.request(endpoint) { (result: Result<[Todo], APIError>) in
promise(result)
}
}.eraseToAnyPublisher()
}
...
}
4. View
struct HomeView: View {
@ObservedObject var viewModel: HomeViewModel
@ObservedObject var sharedVM: SharedTodoViewModel
var body: some View {
VStack(spacing: 0) {
// UI 구성...
if viewModel.filteredTodos.isEmpty {
EmptyStateBox()
} else {
List {
ForEach(viewModel.filteredTodos) { todo in
// Todo 아이템 표시...
}
}
}
}
.background(.gray00)
}
}
먼저, Todo.swift 파일에서 데이터 구조를 정의합니다. 그 후, ViewModel에서 @Published 프로퍼티를 사용해 View에 바인딩될 데이터를 정의하고, 비즈니스 로직을 구현합니다. 이때 저는 네트워크 통신(API 함수)을 비롯한 데이터접근 기능들을 TodoService.swift 파일로 또 다시 분리해 서비스 레이어를 나누었습니다. 마지막으로 사용자에게 직접 보여지는 HomeView.swift 뷰 파일에서 @ObservedObject를 사용해 viewModel의 변경사항을 관찰하며, 데이터 표시 및 사용자 인터랙션 처리에 집중합니다.
위 구조와 같이 프로젝트를 MVVM + Service Layer로 분리해 진행하게 될 경우, 아래와 같은 이점이 있습니다.
- 관심사의 분리: UI 로직과 비즈니스 로직 그리고 데이터접근 로직이 명확히 분리되기 때문에, 가독성이 높아지고 코드 유지보수가 쉬워집니다.
- 코드 재사용: ViewModel을 여러 View에서 재사용할 수 있기에, 코드 중복을 줄일 수 있습니다.
- 확장성: 새로운 기능이나 데이터 소스를 추가할때, 큰 리소스 할애없이 새로운 서비스 추가가 가능해집니다.
감사합니다.
'개발지식 정리 > Swift' 카테고리의 다른 글
SwiftUI에서 애플, 구글 로그인 구현하기 (with Node.js) (1) | 2024.10.09 |
---|---|
React Native와 비교해보는 Swift,SwiftUI (5) | 2024.10.08 |
Keychain과 UserDefaults를 활용한 사용자 정보 관리 (3) | 2024.10.07 |
Swift에서 싱글톤 패턴 활용하기 (0) | 2024.10.02 |
ARC와 약한 참조를 바탕으로 하는 메모리 관리 최적화 (1) | 2024.10.02 |