날씨앱을 만들어보며 api에 대한 이해를 높여가는 시간을 가졌다.
우선 ViewController
에 기본이 되는 라벨들을 배치해주고 스냅킷을 이용해 오토레이아웃까지 잡아준다.
오토레이아웃까지 야무지게 잡은 모습.
그리고 강의를 들을 때마다 느끼지만 configureUI
만들 때마다 forEach
문으로 addSubview
처리하는게 가독성도 좋고 너무 편하다.
그리고 스택뷰를 이용했다면 스택뷰 안에 들어간 레이블들은 따로 빼서 view.addSubview
가 아닌
StackView.addArrangedSubview
로 해주어야한다.
이 코드는 네트워크 요청과 JSON 데이터를 처리하는데 필수적인 함수다.
제너릭과 Decodable
을 활용해 확장성과 재사용성을 높이면서 iOS 개발에서 API를 호출할 때 자주 사용된다.
"제너릭 JSON 네트워크 요청 함수"로 부르면 될 듯 하다.
네트워크 요청
.dataTask
를 사용해 비동기적으로 데이터를 가져온다.응답 처리
데이터 디코딩
Decodable
프로토콜을 준수하는 타입으로 변환한다.비동기 콜백 제공
completion
클로저를 통해 호출한 곳으로 전달한다. 성공 시 디코딩된 데이터를 반환하고, 실패 시 nil
을 반환한다.T
가 Decodable
을 준수하도록 제한하여, JSON 디코딩이 가능한 타입만 사용할 수 있게 했다.새로 만든 CurrentWeatherResult.swift
파일에 현재 날씨 결과를 가져오는 코드를 짜야하는데,
이 때 Codable
이라는 프로토콜을 사용해야한다.
Codable은 위에서 본 것처럼 두 개의 프로토콜인 Encodable
과 Decodable
의 조합이다.
Codable
은 JSON
데이터와 Swift
객체 간의 변환을 매우 간단하고 안전하게 만들어주고,
Swift 기반의 REST API
호출 작업에서 매우 중요한 도구인데, 데이터를 다룰 때 가장 많이 쓰이는 프로토콜이다.
구조체와 함께 사용할 때 가장 효과적이라고 하고, JSONDecoder
와 JSONEncoder
와 함께 작동한다.
자세한 내용은 디코딩과 인코딩 포스트에 있으니 꼬옥 읽어보길 추천한다.
그리고 오픈웨더 API 주소에 들어서 보면.
우리는 저 네모박스로 쳐준 부분만 사용할건데,
아래 main
에서도 사용할 거는 temp
로 써진 것들까지 사용할 것이다.
이렇게 하면 CurrentWeatherResult
코드를 스위프트 코더블로 받아올 수 있게 완료를 했다.
그리고 다시 ViewController로 돌아와서 ,
아까 작성한 코더블 코드 아래에 프라이빗한 함수를 하나 만들어준다.
URL 컴포넌츠에는 아까 날씨 api 사이트에서 나온 주소 중에
이렇게 표시한 부분까지 쿼리 전까지만 복사를 해줘서 넣어준 것이다.
queryItems
를 따로 설정하는 이유는 URL에 쿼리 파라미터(query parameter)를 추가해서
서버로 요청을 보낼 때 유연하고 명확하게 작업할 수 있기 때문이고,
URLComponents
는 URL을 구성하는 각 부분(스킴, 호스트, 경로, 쿼리 등)을 손쉽게 다룰 수 있도록 도와주는 구조체다.
오픈 웨더에서 queryItems
에 들어가야할 네가지다.
이렇게 URL 쿼리에 넣을 아이템들에 서울역 위경도 등 넣어주었다. ( 모두 String 값으로 넣어야한다 )
그리고 마지막 units
는 온도 데이터를 사용자가 원하는 단위로 설정하기 위한 파라미터인데,
metric
을 사용했기 때문에 섭씨로 반환되고, 한국과 같이 섭씨 단위를 사용하는 경우에 적합해서 사용했다.
설정하지 않으면 기본값으로 켈빈 단위를 반환하기 때문에 따로 계산이 필요하다.
API는 기본적으로 켈빈 단위로 데이터를 반환하는데, 켈빈 값은 섭씨로 변환하기 위해 계산이 필요하다.
변환 공식)
𝐶 = 𝐾 − 273.15
(섭씨 C는 켈빈 K 값에서 273.15를 뺀 값)
다시 fetchCurrentWeatherData
함수로 돌아와 코드 이어서 작성한다.
위 코드는 OpenWeatherMap API
를 사용해서 현재 날씨 데이터를 가져오기 위해 URL을 생성하고 유효성을 확인하는 단계를 포함하고 있는 것이고,
각 부분이 하는 역할과 코드 작성 이유를 설명해보겠다.
URLComponents
URLComponents
는 URL을 구성하기 위한 객체다. 이 코드를 통해 URL의 기본 구조를 설정한다.URLComponents
를 사용하면 URL의 쿼리 파라미터를 쉽게 추가하고 관리할 수 있고,urlComponents?.queryItems
self.urlQueryItems
는 미리 정의한 쿼리 아이템 배열로, 예를 들어 lat
, lon
, appid
, units
와 같은 정보를 담고 있다.queryItems
를 통해 쿼리 파라미터를 직관적으로 관리할 수 있고,guard let url = urlComponents?.url
urlComponents?.url
은 URLComponents를 기반으로 URL 객체를 반환한다.nil
을 반환한다. 이걸 방어적으로 처리하기 위해서 guard
문을 사용한 것이다.이제 url을 넣어줘야하는데,
아래에 코드를 작성했다.
result: CurrentWeatherResult?
로 타입을 명시하면 위에 썼던 T?
로 인식이 되어서
모든 T
들이 CurrentWeatherResult
로 인식한다.
fetchData
는 네트워크 요청을 처리하고, 응답 데이터를 디코딩하여 completion
클로저로 전달하는 역할을 한다.
weak self
를 사용해서 클로저의 순환 참조를 방지했는데,
네트워크 응답이 클로저 안에서 처리되기에, self
를 약한 참조로 유지하여 메모리 누수를 방지할 수 있게 된다.
요청 결과가 없거나 디코딩이 실패한 경우 다음 단계를 진행하지 않기 위해 guard let
을 사용했다.
DispatchQueue
부터는 API로부터 받은 날씨 데이터를 UI에 반영했다.
result.main.temp
, result.main.tempMin
, result.main.tempMax
는 각각 현재 기온, 최저 기온, 최고 기온을 나타낸다.
네트워크 작업은 백그라운드 스레드에서 이루어져서 UI 업데이트는 메인 스레드에서 처리해야 애플리케이션이 정상적으로 동작한다.
그리고 기온 데이터를 Int
로 변환해 보기 쉬운 형식으로 표시한다.
그리고 fetchData
역시 재사용이 가능한 로직이다. (댕꿀)
잘 반영이 된다. 근데 왜 최소 온도랑 최고 온도가 같은지 모르겠네.. ㅜ
아이콘 관련 주소에 들어가서 보면,
이렇게 나오는데 원하는 아이콘이 10d
이면 아래 주소처럼 기입해라 라는 뜻이다.
guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else { return }
result.weather[0].icon
은 API에서 받은 날씨 정보 중 첫 번째 아이콘 값을 가져오는 부분이다.icon
값이 01d
라면 "https://openweathermap.org/img/wn/01d@2x.png" 같은 URL로 하면 된다.URL(string:)
메서드를 사용하여 위 URL 문자열을 URL
객체로 변환하려고 시도한다.guard
구문에 의해 함수가 종료된다. (return
)if let data = try? Data(contentsOf: imageUrl) {
Data(contentsOf:)
는 주어진 URL에서 데이터를 다운로드하는 메서드인데, 이 부분에서는 날씨 아이콘 이미지를 URL에서 직접 다운로드하고 있다.try?
는 오류가 발생할 수 있는 코드에 사용되며, 오류가 발생하면 nil
을 반환하게 된다.data
변수에 할당이 된다.if let image = UIImage(data: data) {
UIImage(data:)
는 Data
객체로부터 UIImage
객체를 생성하는 메서드인데, 다운로드한 이미지 데이터를 UIImage
로 변환하여 image
변수에 할당한다.nil
이 되어 해당 코드는 실행되지 않는다.DispatchQueue.main.async {
self.imageView.image = image
}
DispatchQueue.main.async
를 사용해 메인 스레드에서 실행되도록 한다.self.imageView.image = image
는 imageView
라는 UIImageView
에 다운로드한 이미지를 설정하는 부분이다.imageView
에 표시하는 것이다.지금 각 테이블셀들에 대한 데이터를 가지고 있어야하는데 없어서 오류가 난건데,
그 데이터 소스를 self
로 해서 현재 ViewController
에서 정의한다 라는 거라서.. 하단에 extension으로 추가하면 된다.
하단에 코드를 쓰다보면 자기가 Fix
해주겠다고 난리가 난다.
역할에 따라 코드를 분리를 위함인데, 데이터 소스와 델리게이트는 보통 많은 메서드를 필요로 한다.
이것을 extension
으로 분리하면 뷰 컨트롤러 본체에 UI 설정 및 중요한 로직만 남길 수 있다.
뿐만 아니라 가독성 향상에도 좋은 것이, 뷰 컨트롤러 본체가 복잡해지는 것을 방지하고 특정 역할의 메서드를 쉽게 찾을 수 있게 해준다.
따로 tableViewCell
이라는 swift 파일을 추가해서 안에 cell
에 대한 내용을 추가해야한다.
cell은 각자의 고유한 id를 가지고 있어야하기 때문에 static
한 프로퍼티를 사용했다.
그리고 날짜와 시간을 알려주는 라벨과 온도를 알려주는 라벨이 필요하기에 두 라벨만 만들었고,
오버라이드 된 이니셜라이저는 TableView
의 style
과 id
로 초기화할때 사용하는 코드다.
그렇게 스타일과 아이덴티티파이어를 써주면 자꾸 Fix
가 뜬다.
required init?
은 커스텀한 UI를 생성할 때 인터페이스 빌더를 통해 셀을 초기화 할 때 사용하는 코드다.
여기서는 fatalError
를 통해 명시적으로 인터페이스 빌더로 초기화 하지 않음을 나타낸다.
UITableViewCell
에서 contentView
를 사용하는 이유는 셀의 구조와 성능 최적화 때문입니다. 아래에서 이를 자세히 설명한다.
contentView
란?contentView
는 UITableViewCell
의 주요 서브뷰로, 셀 내부에 개발자가 추가하는 모든 서브뷰를 담는 컨테이너 역할을 한다.UITableViewCell
)에 추가하지 않고 contentView
를 사용하는가?UITableViewCell
은 다양한 시스템 서브뷰를 포함하는 계층 구조를 가진다.
contentView
: 셀의 실제 콘텐츠를 배치하는 공간.backgroundView
: 셀의 배경을 나타내는 뷰.selectedBackgroundView
: 셀이 선택되었을 때 표시되는 배경 뷰.
contentView
는 사용자 정의 서브뷰를 추가하기 위한 지정된 영역이다.
뷰 재사용
UITableView
는 셀을 재사용(dequeueReusableCell
)하며 성능을 최적화한다.contentView
내부의 서브뷰는 이 재사용 메커니즘에 포함되어 효율적으로 관리된다.backgroundView
, selectedBackgroundView
등)와 독립적으로 UI 요소를 안전하게 배치할 수 있다.contentView
를 사용하면, 시스템이 셀의 레이아웃을 관리하는 방식과 일치하며, 레이아웃 깨짐을 방지할 수 있다.contentView
는 iOS가 제공하는 자동 레이아웃 및 UI 업데이트 기능과 잘 통합된다.contentView
의 크기와 위치는 항상 UITableViewCell
의 크기에 맞게 조정된다. 이를 통해 레이아웃 코드가 간단해지고 유지보수가 쉬워진다.contentView
는 UITableViewCell
에서 개발자가 커스터마이즈할 수 있는 안전한 영역이다.contentView
를 사용해 서브뷰를 추가하면, 레이아웃 충돌과 성능 문제를 방지할 수 있다.UITableViewCell
만들 때 contentView
로 해.위에 테이블뷰셀에 대해 으쌰으쌰 넣고 하단에는 퍼블릭한 컨피규어 셀 함수를 만든다.
그리고 새로운 swift 파일을 만들어 거기에 5일간의 날씨 정보를 받아와야하니 디코딩 작업을 해줘야한다.
api 홈페이지로 가서 5일간의 날씨 데이터를 가져와서 코더블 객체를 만들어준다.
아까 적다 말았던 컨피규어셀도 적어주고,
main
에는 이미 전에 사용했던 WeatherMain
에도 똑같은 내용이 있엇기에 재활용했다.
그렇게 5일간의 날씨정보를 입력해준다. 이제 슬슬 힘들어온다. 벌써 이러면 안되는데.
그리고 위에서 다 적지 못했던 익스텐션 부분을 적어준다.
오류가 발생해서 코드를 구석구석 뒤져보았다. 코드내용을 보면 fatalError("Expected superview but found nil when attempting make constraint equalToSuperview.") 라고 한다.
이 에러는 SnapKit에서 특정 뷰가 superview에 추가되지 않은 상태에서 제약 조건을 설정하려고 할 때 발생한다고 하는데,
뷰가 계층 구조에 올바르게 추가되지 않았음을 의미한다라는 걸 알았다.
여기서 이상한 점 역시나 발견...
addSubview 안에 테이블뷰가 없었다^^
그렇게 테이블 뷰까지 추가한 뒤 드디어 나온 뷰!!!!
날씨앱은 코드를 하나하나 보면서 이해하려고 했지만 시간이 너무 오래 걸렸다.
디코딩도 너무 힘들었고, 디코딩 하면서 사용한 제네릭이라던지, 하나하나 파헤쳐가며 복습을 했지만..
과연 제대로 기억했을까 걱정이 된다.
아 그리고 API 호출 시 발생할 수 있는 여러 오류들에 대한 처리가 부족했던 것 같다..
그리고 이번 강의에서는 에러를 단순히 출력하는 방식으로 처리했지만,
실제 앱에서는 사용자에게 적절한 오류 메시지를 표시하거나 오류 처리 로직을 추가하는 것이 중요하다고 느꼈다.
UI가 매우 간단하게 구현되어 있어서 사용자에게 더 직관적이고 좀더 예쁜 UI를 제공하기 위해 추가적인 디자인의 조정도 했으면 더 좋았을 것 같다.