[ swift ] 포켓몬App 과제해설을 보며 복습해보기 2 (API)

sonny·2024년 12월 15일
1

TIL

목록 보기
72/133

해설강의 내용에서 api를 다루는 부분을 정리해보려한다.

우선 Repository라는 새로운 폴더를 생성하고 그 안에 ImageRepository라는 swift 파일을 생성했다.

안에 내용은 우선 아래처럼 작성했다.

url 유효성 검사

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에 저장을 한다.

마지막으로 imageUrlStringURL 객체로 변환하여 유효한 URL을 만든다.

imageUrlString이 유효한 URL이라면 imageUrlURL 객체가 되고, 그렇지 않으면 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() 메서드를 호출해야 가능하다.

이 메서드는 비동기 작업을 시작하게 만드는 메서드다.

메인스레드에서 UI를 업데이트 하는 역할

    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가 왜 중요한지 이해하려면, 메인 스레드백그라운드 스레드의 차이를 알아야한다.

1. 메인 스레드와 백그라운드 스레드

  • 메인 스레드(Main Thread)

    • UI와 관련된 작업을 처리한다. 화면에 버튼을 추가하거나 이미지를 표시하는 등의 작업은 반드시 메인 스레드에서 해야하는 것이다.
    • iOS에서 UI 업데이트는 메인 스레드에서만 가능하다.
  • 백그라운드 스레드(Background Thread)

    • 시간이 오래 걸리는 작업(네트워크 요청, 파일 다운로드 등)을 처리한다.
    • 백그라운드 스레드에서는 UI 작업을 직접 처리할 수 없기에 UI를 변경하려면 메인 스레드로 돌아와서 UI를 업데이트해야 한다.

2. DispatchQueue.main.async를 사용하는가?

DispatchQueue.main.async메인 스레드에서 작업을 실행하도록 하는데, 백그라운드 스레드에서 작업이 끝난 후, 그 결과를 UI 업데이트를 위한 작업으로 메인 스레드에 전달할 수 있기 때문이다.

3. 실제 코드에서 왜 필요한가?

downloadImage 함수에서는 이미지를 백그라운드 스레드에서 다운로드해야한다. 하지만 이미지를 다운로드한 후, 이를 UI에 표시하려면 메인 스레드에서 그 작업을 해야하는 부분이 필요하다.

  • 백그라운드 스레드에서 이미지 다운로드가 끝났을 때, UI 업데이트는 메인 스레드에서 해야해서 DispatchQueue.main.async를 사용해 메인 스레드에서 UI 작업을 수행하도록 하는 것이다.
DispatchQueue.main.async {
    completion(image)  // 이미지 다운로드가 완료되면 메인 스레드에서 UI 업데이트
}

4. 메인 스레드에서 UI 작업을 해야 하는 이유

iOS에서는 UI 요소(이미지, 레이블 등)에 대한 변경 작업이 메인 스레드에서만 안전하게 이루어진다.

백그라운드 스레드에서 직접 UI를 변경하면 앱이 충돌할 수 있기에 이미지 다운로드나 다른 시간이 걸리는 작업은 백그라운드에서 처리하고, 작업이 끝나면 메인 스레드로 돌아와서 UI를 안전하게 업데이트해야 한다.


음...

오늘의 공부를 통해 API 요청, JSON 파싱, 비동기적인 이미지 다운로드 처리, 메인 스레드에서 UI 업데이트를 하는 방식에 공부를 했다.

DispatchQueue.main.async와 같은 비동기적인 작업 처리를 통해 UI 업데이트를 안전하게 하는 방법을 좀 더 이해하게 됐는데, 이런 원칙들을 실제 프로젝트에서 활용하면 더 효율적이고 안정적인 iOS 앱을 개발할 수 있겠다 싶었다.

비동기 작업을 잘 활용하고, 네트워크 통신에서 발생할 수 있는 다양한 상황을 안전하게 처리하는 방법을 익힌 만큼 다음 단계에서는 Alamofire와 같은 라이브러리를 사용해 더 복잡한 API 요청을 처리하는 방법도 다시 한번 학습해보려고 한다.

profile
iOS 좋아. swift 좋아.

2개의 댓글

comment-user-thumbnail
2024년 12월 15일

완벽한 정리..

1개의 답글

관련 채용 정보