해설강의 내용에서 api를 다루는 부분을 정리해보려한다.
우선 Repository라는 새로운 폴더를 생성하고 그 안에 ImageRepository라는 swift 파일을 생성했다.
안에 내용은 우선 아래처럼 작성했다.
class ImageRepository {
func fetchRandomPokemonImage(completion: @escaping (UIImage?) -> Void) {
let randomId = Int.random(in: 1...1000)
let urlString = "https://pokeapi.co/api/v2/pokemon/\(randomId) "
guard let url = URL(string: urlString) else {
completion(nil)
return
}
알라모파이어를 사용하는 대신 정석방법으로 우선 해보자.
위 코드에서는 우선 1부터 1000 사이
의 랜덤 숫자를 생성하고 그 숫자는 포켓몬의 ID를 의미하기 때문에 해당 ID로 특정 포켓몬을 가져올 수 있다.
이게 왜 필요하냐면 포켓몬API는 /pokemon/{ id }
형식의 URL
을 사용해서 데이터를 제공하기 때문인데 이걸 랜덤으로 생성하게 되면 매번 다른 포켓몬 데이터를 요청할 수 있기 때문이다.
urlString
의 경우 문자열 안에 randomId
를 넣어 특정 포켓몬 데이터를 요청할 URL
을 만들어 주는 것이다.
그리고 이 urlString
을 이용해 URL 객체를 만든 뒤 만약 올바른 형태의 URL이 아니라면 nil
이 반환된다.
만약 정말 nil
값이 나왔따면 더이상 작업이 진행되지 않기에 completion(nil)
을 호출해서 실패했다고 알리며 메서드를 끝내는 것이다.
이건 혹시라도 주소가 잘못 된 경우에는 네트워크 요청에 실패할 것이기 때문에 그런 상황을 방지하려고 하는 것이다.
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
그리고 나서 이 부분이 중요한데, 네트워크 통신에 시작점이자 핵심 메서드이기 때문에 이 부분이 중요하다.
URLSession
을 사용해서 url
로 서버에 요청을 먼저 보낸 뒤에 결과 (데이터, 응답정보, 에러)를 비동기적으로 받아오는 부분으로 시작하는데, 실제로 네트워크 요청이 발생하는 부분이기도 하고, 앱이 API와 통신해서 데이터를 가져오는 기능의 기반이다.
그 다음 guard
문을 이용해 네트워크 요청의 결과 데이터를 검증해보고 올바르게 처리해야한다.
그래서 먼저 data
를 확인하는데 만약 데이터가 없으면 작업이 실패한 것이기에 이후 로직은 실행되지 않는다.
만약 데이터가 있다면 그 다음인 error
확인을 통해서 네트워크 요청 중 발생한 에러를 확인하고 에러가 있다면 작업에 실패한 것으로 간주 된다.
josn
부분에서는 반환된 데이터를 이제 json
형식으로 변환해야하기 때문에 변환된 json 객체를 또 딕셔너리로 캐스팅을 해주었다.
이때 try?
의 사용으로 혹시라도 변환에 실패하게 될 때 에러를 던지지말고 nil
을 반환하도록 한 것이다.
에러는 앱이 중단 될 수 있어서 그래도 흐름을 계속 이어갈 수 있도록 해야하기 때문이다.
let sprites = json["sprites"] as? [String: Any],
let imageUrlString = sprites["front_default"] as? String,
let imageUrl = URL(string: imageUrlString) else {
completion(nil)
return
}
}
그리고 나서 json에 "sprites"
키에 해당하는 값을 추출해 sprites
라는 딕셔너리에 저장을 한다.
sprites
는 이미지와 관련된 정보를 담고 있는데, 여기서 "sprites"
는 또 다른 딕셔너리일 수 있기 때문에 이걸 안전하게 캐스팅하고 있다.
그 다음 sprites 딕셔너리에서 "front_default"
키에 해당하는 값인 이미지 URL을 추출한다.
"front_default"
가 포켓몬 이미지 주소가 저장된 위치라서 이걸 String
타입으로 변환 후 imageUrlString
에 저장을 한다.
마지막으로 imageUrlString
을 URL
객체로 변환하여 유효한 URL을 만든다.
imageUrlString
이 유효한 URL이라면 imageUrl
은 URL 객체
가 되고, 그렇지 않으면 nil
이 된다.
이 모든게 성공적으로 통과했다면 imageUrl
이 유효한URL 객체로 변환되었음을 의미할 수 있다.
.
.
.
// 포켓몬 데이터 모델
struct PokemonResult: Codable {
let id: Int
let name: String
let height: Int
let weight: Int
let sprites: Sprites
}
struct Sprites: Codable {
let frontDefault: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
}
}
나는 Codable
방식을 사용했었다.
Codable을 사용하면 자동으로 JSON 데이터와 Swift 객체 간의 변환을 쉽게 할 수 있고, 타입 안전성을 유지하면서 JSON 파싱을 효율적으로 할 수 있기 때문이다.
그렇게 타입이 정확한 데이터를 다룰 수 있으면서 수동으로 JSON을 파싱하지 않아도 되므로 코드가 훨씬 깔끔해진다.
* 파싱(parsing)
은 컴퓨터 프로그래밍에서 데이터를 해석하거나 변환하는 과정인데, 주로 문자열이나 데이터를 다른 형식으로 변환하는 작업을 의미한다.
Codable을 사용할 때는 기본적으로 객체 구조를 만든 후 변환하는 방식이 필요하지만, 지금처럼 단순히 몇 가지 값만 파싱할 때 JSONSerialization
을 사용하면 코드가 간단할 수 있기에 이 방식으로 한 듯 하다.
대체로 권장하는 방법은 Codable이라고 한다.
self.downloadImage(from: imageUrl, completion: completion)
}
task.resume()
}
네트워크 요청이 완료되어 imageUrl이 준비가 됐다면 이제 URL로부터 이미지를 다운로드 해야한다.
downloadImage함수는 비동기적으로 이미지를 다운로드 하는 역할을 하는데 아래에 나머지 코드도 작성할 예정이다.
아무튼 task.resume()은 URLSession.shared.dataTask(with: url) 메서드를 호출한 후에 task 객체를 실행하려면 resume() 메서드를 호출해야 가능하다.
이 메서드는 비동기 작업을 시작하게 만드는 메서드다.
private func downloadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global().async {
if let imageData = try? Data(contentsOf: url),
let image = UIImage(data: imageData) {
DispatchQueue.main.async {
completion(image)
}
} else {
DispatchQueue.main.async {
completion(nil)
}
}
}
}
DispatchQueue.global().async
는 백그라운드 스레드에서 작업을 실행하게 하는 코드인데, 이미지 다운로드 하는 작업이 UI스레드를 차단하면 안되기 때문에 백그라운드 스레드에서 데이터를 다운로드를 해야한다.
그렇게 비동기적으로 작업을 수행해주고 완료가 되면 지정된 클로저 내에 if문 코드가 실행하게 된다.
Data(contentsOf: url)
는 주어진 URL에서 데이터를 불러오는 코드다. 이미지 데이터를 다운로드하는 것이다.
이미지 데이터를 다운로드 하고 만약 다운로드 한 데이터가 유효한 이미지 형식이라면 UIImage(data:)
를 사용해서 UIImage 객체를 생성하게 된다.
DispatchQueue.main.async
코드는 메인 스레드에서 UI작업을 수행하기위해 메인스레드에서 comlaetion
클로저를 호출하게 된다.
여기서 DispatchQueue.main.async
가 왜 중요한지 이해하려면, 메인 스레드와 백그라운드 스레드의 차이를 알아야한다.
메인 스레드(Main Thread)
백그라운드 스레드(Background Thread)
DispatchQueue.main.async
를 사용하는가?DispatchQueue.main.async
는 메인 스레드에서 작업을 실행하도록 하는데, 백그라운드 스레드에서 작업이 끝난 후, 그 결과를 UI 업데이트를 위한 작업으로 메인 스레드에 전달할 수 있기 때문이다.
downloadImage
함수에서는 이미지를 백그라운드 스레드에서 다운로드해야한다. 하지만 이미지를 다운로드한 후, 이를 UI에 표시하려면 메인 스레드에서 그 작업을 해야하는 부분이 필요하다.
DispatchQueue.main.async
를 사용해 메인 스레드에서 UI 작업을 수행하도록 하는 것이다.DispatchQueue.main.async {
completion(image) // 이미지 다운로드가 완료되면 메인 스레드에서 UI 업데이트
}
iOS에서는 UI 요소(이미지, 레이블 등)에 대한 변경 작업이 메인 스레드에서만 안전하게 이루어진다.
백그라운드 스레드에서 직접 UI를 변경하면 앱이 충돌할 수 있기에 이미지 다운로드나 다른 시간이 걸리는 작업은 백그라운드에서 처리하고, 작업이 끝나면 메인 스레드로 돌아와서 UI를 안전하게 업데이트해야 한다.
오늘의 공부를 통해 API 요청, JSON 파싱, 비동기적인 이미지 다운로드 처리, 메인 스레드에서 UI 업데이트를 하는 방식에 공부를 했다.
DispatchQueue.main.async
와 같은 비동기적인 작업 처리를 통해 UI 업데이트를 안전하게 하는 방법을 좀 더 이해하게 됐는데, 이런 원칙들을 실제 프로젝트에서 활용하면 더 효율적이고 안정적인 iOS 앱을 개발할 수 있겠다 싶었다.
비동기 작업을 잘 활용하고, 네트워크 통신에서 발생할 수 있는 다양한 상황을 안전하게 처리하는 방법을 익힌 만큼 다음 단계에서는 Alamofire와 같은 라이브러리를 사용해 더 복잡한 API 요청을 처리하는 방법도 다시 한번 학습해보려고 한다.
완벽한 정리..