[아키텍처] MVVM

한지민·2022년 3월 25일
0

아키텍처

목록 보기
3/7
post-thumbnail

MVVM?

  • Model
  • View
  • ViewModel

위 3가지 요소로 구성된 패턴으로 MVVM이라 한다.

Model
데이터 구조 및 처리

View
화면 표현과 사용자와의 상호작용 담당

ViewModel
View의 추상화, View에 맞게 데이터를 변환해서 전달


여전히 비즈니스 로직은 ViewModel에서 담당하게 된다.
다만 로직의 관점이 데이터 중심으로 옮겨갔다는 점이 큰 차이가 있다고 생각한다.

View와 ViewModel간의 의존성 제거를 위해
데이터 바인딩과 커맨드 패턴을 활용한다는 점 또한 눈여겨 봐야한다.

구현

이전 MVC 포스트에서 구현한 기능을 MVVM으로 변환하고자 한다.
실제 동작 결과는 MVC와 동일하지만 내부적인 구현 차이를 보기 위함이다.

IOS에서 MVVM은 약간 이질감이 느껴진다고 생각한다.
개념적으로 View에 묶인 기능들이 View와 ViewController로 나뉘기 때문으로 보인다.

SwiftUI를 활용하는 경우 MVVM의 장점이 명확하게 드러나지 않을까 한다.

Model

struct Repository: Codable {
    let id: Int
    let node_id: String
    let full_name: String
    let description: String?
    let url: String
    let html_url: String
    let created_at: String
    let updated_at: String
}
struct SearchResponse: Codable {
    var total_count: Int = 0
    var incomplete_results: Bool = false
    var items: [Repository] = []
}

View

class SearchTableViewCell: UITableViewCell {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!
    
    func setData(data: Repository) {
        nameLabel.text = data.full_name
        descriptionLabel.text = data.description
    }
}

여기서 ViewController는 View 범주에 해당하게 된다.
ViewController는 ViewModel에 대한 정보를 가지고 있지만 Input, Ouput 프로토콜에 정의된 내용에 한정된다.
의존성 주입으로 변경하는 것도 가능하다.

class SearchViewController: UIViewController {
    var viewModel: (SearchViewModelInput & SearchViewModelOutput) = SearchViewModel()
}

뷰에서 이벤트 발생시 해당하는 비즈니스 로직을 호출하도록 구현한다.
로직에 대한 제어의 위치가 ViewController에서 ViewModel로 이동됐다.

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    self.viewModel.fetchRepositories(text: searchBar.text)
}

데이터 바인딩은 KVO, Notification, Rx, Combine 등으로 구현할 수 있고,
여기선 Combine을 사용하였다.

self.viewModel.repositoriesPublisher
    .receive(on: DispatchQueue.main)
    .sink { [weak self] repositories in
        self?.displayedRepositories = repositories
        self?.tableView.reloadData()
    }.store(in: &cancelBag)

ViewModel

데이터에 대한 조회, 관리 등을 담당하는 로직을 구현한다.
ViewModel은 본인을 참조하는 View에 대한 존재를 알지 못한다.

class SearchViewModel: SearchViewModelProtocol {
    @Published var repositories: [Repository] = []
    var repositoriesPublisher: Published<[Repository]>.Publisher { $repositories }
    
    var cancelBag = Set<AnyCancellable>()
    
    func fetchRepositories(text: String?) {
        var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
        urlComponents?.queryItems = [
            URLQueryItem(name: "q", value: text)
        ]
        
        URLSession.shared.dataTaskPublisher(for: (urlComponents?.url)!)
            .map { $0.data }
            .decode(type: SearchResponse.self, decoder: JSONDecoder())
            .map { $0.items }
            .replaceError(with: [])
            .assign(to: \.repositories, on: self)
            .store(in: &cancelBag)
    }
}

결과

전체 코드는 깃허브에 있습니다.

profile
IOS Developer

0개의 댓글