그동안 학습했던 MVVM, URLSession, URLComponent를 사용해서 각 나라의 주요 도시 날씨를 확인하는 간단한 앱을 만들어 볼 것이다.
OpenWeather
예제에 사용한 RestAPI는 OpenAPI인 OpenWeather을 사용할 것이다.
파싱하면 위와 같은 JSON 데이터를 얻을 수 있다.
WeatherApp - 앱 설계
디자인 패턴 | MVVM |
UI | Only Code |
프레임워크 | 사용하지 않음 |
API | OpenWeather |
기능 | 각 나라별 주요 도시의 현재 날씨를 확인할 수 있음 |
기타 | 상수는 Constants에서 따로 관리한다. |
❗️ UI 관련 코드는 따로 다루지 않겠다.
// Model
struct GroupResponse: Codable {
let list: [WeatherResponse]
static let EMPTY = GroupResponse(list: [])
}
struct WeatherResponse: Codable {
let weather: [Weather]
let main: Main
let name: String
}
struct Weather: Codable {
let main: String
let icon: String
let description: String
}
struct Main: Codable {
let temp: Double
let temp_max: Double
let temp_min: Double
}
파싱한 데이터를 받아 올 구조체이다. Model 부분에 해당한다.
WeatherApp - API 파싱
API 가이드 문서를 참고하여 아래와 같이 파싱할 URL을 설계한다.
struct URLComponents {
static let WEATER_API_SCHEMA: String = "https"
static let WEATHER_API_HOST: String = "api.openweathermap.org"
static let WEATHER_API_PATH: String = "/data/2.5/group"
}
쿼리 파라미터는 id, appid, units, lang이므로 아래와 같이 적절하게 URLComponents를 작성해준다.
var urlComponents: URLComponents = URLComponents()
urlComponents.scheme = Constants.URLComponents.WEATER_API_SCHEMA
urlComponents.host = Constants.URLComponents.WEATHER_API_HOST
urlComponents.path = Constants.URLComponents.WEATHER_API_PATH
urlComponents.queryItems = [
URLQueryItem(name: "id", value: query),
URLQueryItem(name: "appId", value: appId),
URLQueryItem(name: "units", value: units),
URLQueryItem(name: "lang", value: lang)
]
올바른 요청을 하고 응답을 잘 받는지 Postman을 이용하여 확인해본다.
// WeatherAPI 전체코드
class WeatherAPI {
init() { }
private let session = URLSession(configuration: .default)
var apiKey: String = ApiKey.ApiKey
var responseDatas: GroupResponse = GroupResponse.EMPTY
private func getComponents(query: String, appId: String, units: String, lang: String) -> URLComponents {
var urlComponents: URLComponents = URLComponents()
urlComponents.scheme = Constants.URLComponents.WEATER_API_SCHEMA
urlComponents.host = Constants.URLComponents.WEATHER_API_HOST
urlComponents.path = Constants.URLComponents.WEATHER_API_PATH
urlComponents.queryItems = [
URLQueryItem(name: "id", value: query),
URLQueryItem(name: "appId", value: appId),
URLQueryItem(name: "units", value: units),
URLQueryItem(name: "lang", value: lang)
]
return urlComponents
}
func getItems(qurry: String, completion: @escaping (GroupResponse) -> Void) {
guard let url = getComponents(query: qurry, appId: self.apiKey, units: "metric", lang: "kr").url else {
return
}
self.session.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
let hasData = try JSONDecoder().decode(GroupResponse.self, from: data)
self.responseDatas = hasData
completion(self.responseDatas)
} catch let error as NSError {
print("err \(error)")
return
}
}.resume()
}
}
요청을 확인했다면 나머지 코드도 작성한다.
✅ getComponents 함수 : URLComponents를 반환하는 함수
✅ getItems 함수 : 파싱한 데이터를 반환하는 함수
⭐️ 이전 글의 예제와 달리 파싱한 데이터를 로그로 확인하는 것이 아니라 파싱한 데이터를 가공하여 사용자에게 출력할 것이므로 그에 맞는 어떠한 동작을 해야하기 때문에 컴플리션 핸들러를 리턴한다.
WeatherApp - ViewModel 설계
뷰 모델을 설계할 차례이다.
class Observable<T> {
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
self.listener?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(listener: Listener?) {
listener?(value)
self.listener = listener
}
}
간단하게 Observable 클래스를 작성해준다.
class WeatherViewModel {
var network: WeatherAPI
var data: Observable<[WeatherResponse]> = Observable([])
init(network: WeatherAPI = WeatherAPI()) {
self.network = network
}
func fetchData(_ query: String) {
print("fetch Start")
self.network.getItems(qurry: query) { GroupResponse in
let observable = Observable(GroupResponse.list)
self.data.value = observable.value
}
}
}
뷰모델에서 정의할 동작은 쿼리 파라미터에 맞게 데이터를 파싱해오는 동작이다.
예를 들면 디폴트 값은 한국이다. 만약 사용자가 프랑스 태그를 선택하면 옵저버가 상태 변화(여기서는 한국 -> 프랑스)를 감지하여 프랑스의 날씨를 파싱하여 값을 전달하는 것이다.
WeatherApp - bind 정의
...
// WeatherTableView
...
func bind(_ viewModel: WeatherViewModel) {
viewModel.data.bind { [weak self] _ in
guard let self = self else { return }
DispatchQueue.main.async {
self.reloadData()
}
}
}
WeatherTableView에서는 파싱한 데이터를 받아와 리로딩하는 동작을 해야하므로 그 동작을 담을 bind를 구현한다.
...
// MainViewController
...
private let viewModel = WeatherViewModel()
private func setUpData() {
self.viewModel.fetchData("1835847,1841610,1843125,1845106,1845105,1845789,1845788,1841597,1902028,1846265")
}
func bind(_ viewModel: WeatherViewModel) {
self.WeatherTv.bind(viewModel)
}
MainViewController에서는 setUpData 메소드를 통해 디폴트 값(한국)을 파싱 해오고, WeatherTableView에서 정의한 bind에 인자로 값을 전달해 화면에 출력한다.
WeatherApp - 태그 클릭시 기능 구현
...
// MainViewController
...
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
let query = Constants.TagCellData().data[indexPath.row]
self.viewModel.fetchData(query)
self.WeatherTv.setContentOffset(.zero, animated: true)
}
}
태그 클릭시 쿼리 파라미터로 해당 국가의 CityId를 인자로 전달한다. 이때 옵저버가 상태 변화를 감지하여 WeatherTableView에서 정의한 bind를 통해 파싱한 데이터를 가져와 리로딩한다.
요악해보자면
디폴트 값(한국) setUp
⬇️
ViewModel에서 fetchData를 통해 디폴트 값(한국) 데이터 파싱
⬇️
WeatherTableView에서 정의한 bind 메소드에 파싱한 데이터를 인자로 전달하여 리로딩
⬇️
상태 변화 감지시 setUp과정 제외하고 위의 과정 반복
MVVM 패턴을 활용한 간단한 예제를 만들어 보았다. MVVM 패턴의 장단점을 확실하게 느낄 수 있었다.
먼저 장점이다.
1️⃣ ViewController의 부담을 줄일 수 있었다.
- 실제로 예제의 ViewController의 코드의 대부분은 UI관련 코드이다. 만약 MVC 패턴을 적용해서 개발했다면 데이터를 파싱해오는 것부터 출력하고 가공하는 것 모두 ViewController에 작성했을 것이고, ViewController의 크기가 커졌을 것이다.
2️⃣ 재사용성
- 만약 내가 MVC로 개발했다면 getKorea, getFrance, getUSA처럼 해당 국가에 맞는 데이터를 파싱하는 메소드를 일일이 작성했을 것이다. 하지만 ViewModel에서 구현한 fetchData를 통해 일일이 작성하지 않고 재사용하면 됐기에 개발하는데 있어서 정말 편했다.
단점
1️⃣ Project By Project
- 나처럼 정말 소규모의 프로젝트에서 MVVM패턴은 너무 과하다고 느꼈다. 물론 장점도 있지만 간단한 처리도 데이터 바인딩을 해야하기 때문에 추가적인 개발이 필요하기에 "굳이 이걸 이렇게 해야하나" 라는 생각이 들기도 했다.
2️⃣ 복잡하다.
- 설계하는데 있어서 너무 복잡하다.
MVVM 패턴은 유지보수가 용이하고 테스트가 용이한 패턴이다. 개발하면서 "간단하게 작성해도 될 걸 이렇게 길게 작성하는데 좋은거 맞나?" 하는 생각이 들었지만 위의 예제는 정말 간단한 예제이기에 테스팅, 유지보수, 새 기능추가 등을 하지 않았다. 만약 규모가 큰 프로젝트를 한다면 정말 유용한 디자인 패턴임은 틀림없다. 현업에서는 유지보수, 새 기능 추가, 테스팅이 중요하기 때문에 현업에서 왜 MVVM패턴을 채택하는지 알게 되었다.
'iOS' 카테고리의 다른 글
[iOS] KeyChain(키체인)에 대해 알아보자. (0) | 2024.11.05 |
---|---|
[iOS] 앱스토어에 앱을 출시해보자 (0) | 2024.01.27 |
[iOS] Identifiers(식별자) 신규 등록 (1) | 2024.01.27 |