33. 스파르타 코딩 클럽 - 날씨 앱 만들기
Open Weather API를 사용해서 날씨 앱 만들기
🧑💻 Open Weather API
- Open 소스 API란 모두가 사용할 수 있게 공공적으로 열어놓은 API를 말합니다.
- Open 소스 API 중 날씨 데이터를 제공하는 Open Weather API를 사용합니다.
Weather API - OpenWeatherMap
Please, sign up to use our fast and easy-to-work weather APIs. As a start to use OpenWeather products, we recommend our One Call API 3.0. For more functionality, please consider our products, which are included in professional collections.
openweathermap.org
이번 날씨 앱 프로젝트에서는 2개의 API를 사용한다.
☀️ 날씨 앱 만들기
아래 데이터들을 화면에 노출합니다.
- 현재 기온
- 최소 기온
- 최고 기온
- 날씨 이미지
- UITableView를 사용한 5일 간 예보
URLSession 사용
[ fetchData() ]
// 서버 데이터를 불러오는 메서드 (재활용이 가능하도록 일반적인 함수로 작성)
private func fetchData<T: Decodable>(url: URL, completion: @escaping (T?) -> Void) {
let session = URLSession(configuration: .default)
session.dataTask(with: URLRequest(url: url)) { data, response, error in
guard let data, error == nil else {
print("데이터 로드 실패")
completion(nil)
return
}
// http status code 성공 범위는 200번대.
let successRange = 200..<300
if let response = response as? HTTPURLResponse, successRange.contains(response.statusCode) {
guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
print("JSON 디코딩 실패")
completion(nil)
return
}
completion(decodedData)
} else {
print("응답 오류")
completion(nil)
}
}.resume()
}
URLSession을 사용하여 데이터를 불러오려면 위의 코드와 같은 메서드가 필요하다.
재활용이 가능하도록 일반적인 함수로 작성하여야 한다.
let successRange = 200..<300
중간에 successRange 변수는 http status code의 성공 범위가 200번대이기 때문에
200..<300 사이의 값으로 선언하였고, response.statusCode의 값이 successRange안에 포함된다면
정상적으로 데이터를 로드에 성공했다는 뜻이다.
try? JSONDecoder().decode(T.self, from: data)
JSONDecode에 성공하면 completion에 decodedData를 넣어준다.
[ fetchCurrentWeatherData() ]
private let urlQueryItems: [URLQueryItem] = [
URLQueryItem(name: "lat", value: "37.5"),
URLQueryItem(name: "lon", value: "126.9"),
URLQueryItem(name: "appid", value: "3d1be1b2d3419223212333eb2388ba4a"),
URLQueryItem(name: "units", value: "metric")
]
// 서버에서 현재 날씨 데이터를 불러오는 메서드
private func fetchCurrentWeatherData() {
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")
urlComponents?.queryItems = self.urlQueryItems
guard let url = urlComponents?.url else {
print("잘못된 URL")
return
}
fetchData(url: url) { [weak self] (result: CurrentWeatherResult?) in
guard let self, let result else { return }
// UI 작업은 메인 쓰레드에서 작업
DispatchQueue.main.async {
self.tempLabel.text = "\(Int(result.main.temp))°C"
self.tempMinLabel.text = "최소: \(Int(result.main.temp_min))°C"
self.tempMaxLabel.text = "최고: \(Int(result.main.temp_max))°C"
}
guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else { return }
// image 를 로드하는 작업은 백그라운드 쓰레드 작업
if let data = try? Data(contentsOf: imageUrl) {
if let image = UIImage(data: data) {
// 이미지뷰에 이미지를 그리는 작업은 UI 작업이기 때문에 다시 메인 쓰레드에서 작업.
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
}
}
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")
urlComponents?.queryItems = self.urlQueryItems
사용할 API를 호출하는 방법은 https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}
이지만 쿼리문 앞에까지만 URLComponents에 작성하고 뒤에 쿼리 부분은 미리 선언해 둔 urlQueryItems를 사용한다.
fetchData(url: url) { [weak self] (result: CurrentWeatherResult?) in
guard let self, let result else { return }
// UI 작업은 메인 쓰레드에서 작업
DispatchQueue.main.async {
self.tempLabel.text = "\(Int(result.main.temp))°C"
self.tempMinLabel.text = "최소: \(Int(result.main.temp_min))°C"
self.tempMaxLabel.text = "최고: \(Int(result.main.temp_max))°C"
}
쿼리문을 추가한 url을 fetchData에 넣어주고 데이터 통신을 시도한다.
통신이 성공하여 정상적으로 데이터를 받으면 각 라벨에 값을 넣어주는데
UI를 그리는 작업은 메인 쓰레드에서 작업해야 하기 때문에
DispatchQueue.main.async {
// UI를 그리는 작업
}
DispatchQueue.main.async 안에서 UI 작업 코드를 작성한다.
guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else { return }
이미지는 해당 url을 이용하는데 CurrentWeather에서 받아온 result.weather[0].icon의 값을 이용해서 필요한 이미지를 선정한다.
if let data = try? Data(contentsOf: imageUrl) {
if let image = UIImage(data: data) {
// 이미지뷰에 이미지를 그리는 작업은 UI 작업이기 때문에 다시 메인 쓰레드에서 작업.
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
마찬가지로 이미지 뷰를 그리는 UI 작업은 메인 쓰레드에서 작업해야 하기 때문에
DispatchQueue.main.async 안에서 코드를 작성한다.
[ fetchForecastData() ]
private func fetchForecastData() {
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/forecast")
urlComponents?.queryItems = self.urlQueryItems
guard let url = urlComponents?.url else {
print("잘못된 URL")
return
}
fetchData(url: url) { [weak self] (result: ForecastWeatherResult?) in
guard let self, let result else { return }
// result 콘솔에 찍어보기
for forecastWeather in result.list {
print("\(forecastWeather.main)\n\(forecastWeather.dtTxt)\n\n")
}
// UI 작업은 메인 쓰레드에서
DispatchQueue.main.async {
self.dataSource = result.list
self.tableView.reloadData()
}
}
}
ForecastData도 마찬가지로
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/forecast")
urlComponents?.queryItems = self.urlQueryItems
해당 url을 이용하며 쿼리의 앞 부분까지만 URLComponents에 String값으로 주소를 넣어주고
필요한 쿼리문은 urlQueryItems로 추가한다.
fetchData(url: url) { [weak self] (result: ForecastWeatherResult?) in
guard let self, let result else { return }
// result 콘솔에 찍어보기
for forecastWeather in result.list {
print("\(forecastWeather.main)\n\(forecastWeather.dtTxt)\n\n")
}
// UI 작업은 메인 쓰레드에서
DispatchQueue.main.async {
self.dataSource = result.list
self.tableView.reloadData()
}
}
정상적으로 데이터를 받아오는 것에 성공하면
result.list들의 값들을 콘솔에 print 해보고
tableView에 들어갈 dataSource에 result.list를 넣어준다.
그리고 테이블 뷰를 reloadData() 시켜준다.
마찬가지로 UI 작업은 메인 쓰레드에서 작업하기 때문에
DispatchQueue.main.async 안에서 코드를 작성한다.
Alamofire 사용
🧑💻 Alamofire 란?
- Alamofire 는 Swift 의 HTTP 네트워킹 라이브러리.
- 내부적으로 URLSession 을 사용.
- URLSession 을 한단계 감싸서 네트워크 코드 사용성에 편의를 제공.
- AF.request(url) 메서드를 통해서 네트워크 통신 수행.
- AF.request(url).responseDecodable(of: xxx) 를 통해서 네트워크 통신과 동시에 response 를 디코딩 가능.
- Result<T, AFError> 타입으로 response 반환
[ fetchDataByAlamofire() ]
위 URLSession 으로 작성했었던 fetchData() 메서드는, Alamofire로 작성하면 아래와 같습니다.
import Alamofire
// 서버 데이터를 불러오는 메서드
private func fetchDataByAlamofire<T: Decodable>(url: URL, completion: @escaping (Result<T, AFError>) -> Void) {
AF.request(url).responseDecodable(of: T.self) { response in
completion(response.result)
}
}
- URLSession 과 비교했을 때
- successRange 를 정의해주지 않아도, Alamofire 가 success 와 failure 여부를 판단.
- responseDecodable 메서드를 통해서 JSONDecoder().decode() 과정 생략.
- 코드 간소화.
[ Current Weather() 와 ForecastWeather fetch() ]
// 서버에서 현재 날씨 데이터를 불러오는 메서드
private func fetchCurrentWeatherData() {
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")
urlComponents?.queryItems = self.urlQueryItems
guard let url = urlComponents?.url else {
print("잘못된 URL")
return
}
fetchDataByAlamofire(url: url) { [weak self] (result: Result<CurrentWeatherResult, AFError>) in
switch result {
case .success(let result):
DispatchQueue.main.async {
self?.tempLabel.text = "\(Int(result.main.temp))°C"
self?.tempMinLabel.text = "최소: \(Int(result.main.temp_min))°C"
self?.tempMaxLabel.text = "최고: \(Int(result.main.temp_max))°C"
}
guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else {
return
}
// Alamofire 를 사용한 이미지 로드
AF.request(imageUrl).responseData { response in
if let data = response.data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.imageView.image = image
}
}
}
case .failure(let error):
print("데이터 로드 실패: \(error)")
}
}
}
// 서버에서 5일 간 날씨 예보 데이터를 불러오는 메서드
private func fetchForecastData() {
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/forecast")
urlComponents?.queryItems = self.urlQueryItems
guard let url = urlComponents?.url else {
print("잘못된 URL")
return
}
fetchDataByAlamofire(url: url) { [weak self] (result: Result<ForecastWeatherResult, AFError>) in
guard let self else { return }
switch result {
case .success(let result):
DispatchQueue.main.async {
self.dataSource = result.list
self.tableView.reloadData()
}
case .failure(let error):
print("데이터 로드 실패: \(error)")
}
}
}
switch문을 사용하여 성공하였을 때와 실패하였을 때로 분기
switch result {
case .success(let result): // 성공하였을 때
DispatchQueue.main.async {
self?.tempLabel.text = "\(Int(result.main.temp))°C"
self?.tempMinLabel.text = "최소: \(Int(result.main.temp_min))°C"
self?.tempMaxLabel.text = "최고: \(Int(result.main.temp_max))°C"
}
guard let imageUrl = URL(string: "https://openweathermap.org/img/wn/\(result.weather[0].icon)@2x.png") else {
return
}
// Alamofire 를 사용한 이미지 로드
AF.request(imageUrl).responseData { response in
if let data = response.data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.imageView.image = image
}
}
}
case .failure(let error): // 실패하였을 때
print("데이터 로드 실패: \(error)")
}
URLSession보다 코드가 간결하며 쉬운 느낌
마찬가지로 UI 작업은 메인쓰레드에서 작업하기 위해
DispatchQueue.main.async 안에서 코드 작성
✓ TIL
URLSession과 Alamofire를 둘 다 사용해보았을 때
조금더 간단하게 코드를 작성할 수 있는 것은 Alamofire같다.
아마도 과제를 실습할 때는 Alamofire로 과제를 제작해 볼 예정
아직 코드의 의미들이 익숙하지 않지만 여러번 사용해봐야 이해가 될 거 같다.
일단 고!!!!