https://explorer89.tistory.com/294
ViewModel
https://explorer89.tistory.com/82 ObservableObject와 @Published개체가 변경되기 전에 내보내는 게시자가 있는 개체 형식이다.? @Published와 ObservableObject는 스위프트의 Combine 프레임워크에서 사용되는 속성 래
explorer89.tistory.com
왜 ViewModel을 사용하는가?
ViewModel의 주요 목적은 UI와 비즈니스 로직을 분리하는 것입니다. 이 패턴은 MVVM(Model-View-ViewModel) 구조에서 사용되며, 다음과 같은 이유로 선택됩니다:
- UI와 로직 분리
- ViewController(UIKit)나 View(SwiftUI)에 로직이 포함되면 코드가 복잡하고 유지보수가 어려워집니다.
- ViewModel을 사용하면 UI 업데이트 로직과 데이터 처리 로직이 분리되므로, 코드를 더 이해하기 쉽고 테스트 가능하게 만듭니다.
- 데이터 바인딩
- ViewModel은 데이터를 가공하거나 변경 사항을 UI에 전달하는 중간 계층 역할을 합니다.
- UIKit에서 Combine, KVO, 또는 NotificationCenter 등을 사용해 UI와 데이터 상태를 동기화할 수 있습니다.
- SwiftUI에서는 ObservableObject와 @Published를 사용해 뷰를 자동으로 업데이트합니다.
- 테스트 가능성
- ViewModel은 UIKit에 의존하지 않기 때문에, 독립적으로 단위 테스트를 작성할 수 있습니다.
- 반대로, ViewController나 다른 UIKit 구성 요소와 강하게 결합된 로직은 테스트가 어렵습니다.
struct RegisterUserRequest {
let username: String
let email: String
let password: String
}
func registerUser(request: RegisterUserRequest) {
// 1. Firebase Auth로 사용자 생성
Auth.auth().createUser(withEmail: request.email, password: request.password)
}
RegisterUserRequest 구조체만 사용한다면?
RegisterUserRequest 구조체를 정의하고, 별도의 registerUser(request:) 메서드에서 FirebaseAuth를 호출하는 접근은 충분히 유효합니다. 하지만 이 방법은 다음과 같은 한계를 가질 수 있습니다:
- 데이터 상태 관리
- 단순히 구조체와 함수만 사용하면, 현재 유효성 검증 상태(예: 이메일이 올바른지, 비밀번호 길이가 적절한지)와 같은 상태를 관리하기 어렵습니다.
- ViewModel에서는 이런 상태를 @Published 프로퍼티로 관리해 UI에서 쉽게 참조하고 업데이트할 수 있습니다.
- 중복된 로직
- 여러 화면에서 사용자 등록 로직을 사용한다면, 데이터 검증 및 Firebase 호출과 같은 로직이 중복될 가능성이 큽니다.
- ViewModel은 이 로직을 중앙화하여 중복을 줄이는 역할을 합니다.
- 비즈니스 로직과 UI의 결합
- ViewController에서 직접 Firebase를 호출하면, 비즈니스 로직과 UI 코드가 강하게 결합됩니다.
- 결과적으로 코드 재사용성과 테스트 가능성이 낮아지고, UI 변경 시 로직에 영향을 미칠 가능성이 커집니다.
간단한 구조라면 이렇게 구현 가능
ViewModel 없이 단순히 구조체와 함수를 활용하는 방법도 가능합니다.
struct RegisterUserRequest {
let username: String
let email: String
let password: String
}
class AuthManager {
static let shared = AuthManager()
func registerUser(request: RegisterUserRequest, completion: @escaping (Result<User, Error>) -> Void) {
Auth.auth().createUser(withEmail: request.email, password: request.password) { authResult, error in
if let error = error {
completion(.failure(error))
} else if let user = authResult?.user {
completion(.success(user))
}
}
}
}
let request = RegisterUserRequest(username: "JohnDoe", email: "john@example.com", password: "password123")
AuthManager.shared.registerUser(request: request) { result in
switch result {
case .success(let user):
print("User registered: \(user.email ?? "")")
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}
언제 ViewModel을 사용해야 할까?
ViewModel은 다음과 같은 경우에 특히 유용합니다:
- UI와 데이터를 더 잘 분리하고 싶을 때.
- 상태 관리가 필요한 경우 (예: 폼 검증).
- 앱이 복잡해지고, 코드 재사용성과 테스트 가능성이 중요해질 때.
직접 구현한 경우 (RegisterUserRequest 사용)
특징
- 모든 로직이 ViewController에 포함되어 있습니다.
- Validator, AuthService, AlertManager를 직접 호출하고, 사용자 입력 데이터를 바로 처리합니다.
장점
- 간단하고 빠르게 구현 가능:
- 로직이 모두 한곳에 있어서 작성 속도가 빠르고, 코드가 단순합니다.
- 작은 규모의 프로젝트에 적합:
- UI와 데이터 로직이 섞여 있어도 관리가 어렵지 않은 경우 사용해도 문제없습니다.
단점
- 결합도가 높음:
- UI (ViewController)가 로직 (Validator, AuthService)과 강하게 연결되어 있어, 이 로직을 다른 화면에서 재사용하려면 코드 복사가 필요합니다.
- 예를 들어, 같은 registerUser 로직을 재사용하려면 또다시 ViewController에 비슷한 코드가 추가될 가능성이 높습니다.
- 테스트가 어려움:
- ViewController는 UIKit 의존성이 강하므로, 단위 테스트를 작성하기 어렵습니다.
- 이메일, 비밀번호 검증 로직과 Firebase 호출이 UI 코드와 얽혀 있어 독립적인 테스트가 힘듭니다.
ViewModel을 사용한 경우
특징
- UI와 로직이 분리되어 있습니다.
- 사용자 입력 데이터는 ViewModel에서 관리되고, UI는 ViewModel의 상태를 관찰(sink)하여 반응합니다.
장점
- 결합도 감소:
- UI는 단순히 ViewModel의 상태를 관찰하고 업데이트합니다.
- 로직은 ViewModel에 집중되어 있어, 다른 화면에서도 동일한 로직을 재사용하기 쉽습니다.
- 상태 관리가 용이:
- isRegistrationFormValid와 같은 상태를 @Published로 관리하면, UI와 데이터가 동기화됩니다.
- 예를 들어, 입력값이 바뀌면 자동으로 버튼의 활성화 상태가 변경됩니다.
- 테스트 가능성 증가:
- ViewModel은 UIKit과 분리되어 있으므로, 단위 테스트를 작성하기 쉽습니다.
- 예: 이메일과 비밀번호 검증 로직을 테스트하거나, Firebase 호출 결과를 테스트할 수 있습니다.
단점
- 초기 구현 비용:
- ViewModel을 정의하고 바인딩 로직을 작성해야 하므로 초기 작업이 다소 복잡합니다.
- 작은 규모에서는 과할 수 있음:
- 간단한 앱에서는 ViewModel을 사용하지 않아도 충분히 관리 가능한 경우가 많습니다.
'UIKIT' 카테고리의 다른 글
Combine을 활용한 함수 (0) | 2025.01.08 |
---|---|
viewModel.$user에서 user 앞에 $를 붙이는 이유 (0) | 2025.01.08 |
UITabBarAppearance (0) | 2025.01.05 |
UICollectionReusableView와 UIView의 차이 + headerSection (0) | 2025.01.05 |
UIFontMetrics (0) | 2025.01.05 |