[Swift] 메모 앱 만들기 심화 (3) : URL Session 으로 API 통신하여 고양이 사진 가져오기

Oni·2023년 9월 4일
0

TIL

목록 보기
34/47
post-thumbnail

원문 포스팅 🔗

URL로 되어 있는 외부 API를 사용해서 메모 앱에 추가하려고 한다.

The Cat API에서 제공하는 API를 활용하였고, 자세한 가이드는 공식 홈페이지에서 참조하였다.
시작에 앞서, 해당 API를 호출하면 다음과 같은 결과를 받을 수 있다.

[
  {
    "id": "e4f",
    "url": "https://cdn2.thecatapi.com/images/e4f.jpg",
    "width": 500,
    "height": 375
  }
]

해당 결과값은 JSON 형태로 제공되므로, 데이터를 받아올 때 변환해줘야 한다.
무료로 제공하는 갯수는 10개이며, 그 이상의 데이터 수를 얻기 위해서는 가입해서 API KEY를 받아 설정하면 된다.

그리고 고려해야 할 점은 ViewController의 Life Cycle을 고려하여 호출되는 시점을 정해야 한다.


URLSession를 통해 API 데이터 가져오기

  1. URL 생성
  2. URLSession 인스턴스 생성
  3. Data Task 생성
  4. Data Task 실행
  5. 데이터 처리
  6. 에러 처리
  7. 세션 종료

간단한 예제코드

// 1. URL 생성
let apiUrl = URL(string: "https://api.example.com/data")

// 2. URLSession 인스턴스 생성
let session = URLSession.shared

// 3. Data Task 생성
let dataTask = session.dataTask(with: apiUrl!) { (data, response, error) in
    if let error = error {
        print("Error: \(error)")
        return
    }
    
    // 4. 데이터 처리
    if let data = data {
        // 데이터 파싱 및 활용
        print("Received data: \(data)")
    }
}

// 5. Data Task 실행
dataTask.resume()

1. URL 생성

먼저 가져올 데이터를 제공하는 API의 URL을 생성한다. 이 때 필요한 파라미터나 경로 등을 포함할 수 있다.

let url = URL(string: "https://api.thecatapi.com/v1/images/search")!
// 만약 api key 나 품종 등 추가 설정이 존재할 경우 커스터마이징

guard let url = URL(String: "https://api.thecatapi.com/v1/images/search") else { return }
  • 나는 해당 url이 nil이 될 가능성이 없다고 판단하여 강제 옵셔널 해제를 하였지만, 혹시 모를 nil에 대응하기 위해 옵셔널 바인딩 하는 것을 추천
  • guard let 구문을 활용하여 nil 일 경우 에러 메세지를 보여줄 수 있음

2. URLSession 인스턴스 생성

URLSession 클래스의 인스턴스를 생성하여 네트워크 작업을 수행할 수 있다.

let session = URLSession(configuration: .default)
  • configuration 특징 : 캐시 및 쿠키 사용, 인증과 관련된 기본 설정, Connection Pooling, 백그라운드 업로드 및 다운로드, 캐시 정책 설정, 타임아웃 설정
    -> 여러 네트워크 작업 간에 연결을 재사용하여 성능을 최적화 함
  • default : 기본적인 설정을 사용하는 configuration으로 일반적인 네트워크 작업에 적합함
  • ephemeral : 일시적인 데이터 요청에 적합. 캐시 및 쿠키를 저장하지 않고 임시적인 세션을 사용하여 데이터 요청을 처리
  • background : 백그라운드에서 데이터 업로드 및 다운로드를 처리. 앱이 백그라운드로 들어갔을 때 네트워크 작업을 계속할 수 있음
  • custom : 사용자 정의 설정을 통해 네트워크 작업에 특정한 옵션을 정확하게 조정할 수 있음

(추가) 데이터 받을 객체 생성

Data Task를 생성하기 전에 해당 url의 데이터를 받을 객체를 생성해야 한다.
해당 url은 id, url, width, height 로 구성되어 있으며 나는 url만 필요해서 아래와 같이 간단하게 모델을 생성했다.

struct RandomImage: Codable {
    let url: String
}

3. Data Task 생성

URLSession 인스턴스를 사용하여 데이터를 가져오기 위한 Data Task를 생성한다. 생성된 Task는 백그라운드에서 실행되며 네트워크 요청을 처리한다.

session.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
    	return 
    }
    
    // Data Task 실행 코드
    
}
  • session.dataTask(with:completionHandler:) : URLSession에서 데이터를 가져오는 작업을 생성. with - 가져올 데이터의 URL, Handler - 네트워크 작업이 완료되었을 때 실행할 클로저를 전달
  • data : 서버로부터 받아온 데이터
  • response : 서버의 응답에 대한 정보가 담겨있는 객체
  • error : 네트워크 작업 중 발생한 에러. nil인 경우 네트워크 작업이 성공적으로 완료된 것을 의미
  • 상기 코드는 data가 nil이 아니고 error가 nil인 경우에만 실행되는 코드로 데이터가 성공적으로 받아와짐을 의미

4. Data Task 실행

생성한 Data Task를 실행하여 데이터를 가져온다. 이 때 다양한 옵션을 설정하여 요청을 커스터마이즈 할 수 있다.

session.dataTask(with: url) { data, response, error in
    guard let data = data, error == nil else {
    	return 
    }
    
   ...
    
}.resume()
  • 생성된 Data Task를 불러와 마지막에 .resume()을 붙여 실행할 수도 있고, 상기 코드처럼 바로 실행할 수도 있음
  • resume()을 호출함으로써 데이터 작업이 서버로 요청을 보내고, 서버 응답을 기다리며 데이터를 받아오는 등의 동작이 시작됨
  • resume()을 호출하면 앞서 정의한 클로저가 실행되며, 클로저 내부에서 데이터를 받아와서 처리
  • 비동기적으로 네트워크 작업을 수행하면 앱이 멈추지 않고 다른 작업을 수행할 수 있게 됨

5. 데이터 처리

Data Task가 완료되면 서버로부터 받은 데이터 또는 에러 정보를 처리한다. 성공적으로 데이터를 가져온 경우 원하는 형식으로 파싱하고 활용할 수 있다.

let decoder = JSONDecoder()     // (1)
guard let randomCatImage = try? decoder.decode([RandomImage].self, from: data) else { return } // (2)
if let imageUrl = URL(string: randomCatImage.first?.url ?? "") {     // (3)
    URLSession.shared.dataTask(with: imageUrl) { imageData, _, _ in     // (4)
        if let imageData = imageData, let image = UIImage(data: imageData) {     // (5)
            DispatchQueue.main.async {     // (6)
                self.randomImageView.image = image
            }
        }
    }.resume()
}
  • 상기 코드는 JSON 데이터를 파싱하여 이미지 url을 가져오고, 해당 url을 통해 이미지 데이터를 받아와 imageView에 나타냄
  • (1) : JSON 데이터를 디코딩하기 위한 JSONDecoder 인스턴스 생성
  • (2) : 서버에서 받아온 JSON 데이터(data)를 앞서 정의한 RandomImage 타입으로 디코딩하고 randomCatImage 변수에 저장. 만약 실패하면 함수 실행 종료
  • (3) : randomCatImage에 저장된 첫 번째 이미지 URL을 가져옴. nil이 아닐 경우에만 실행
  • (4) : (3)에서 가져온 이미지 url을 이용하여 새로운 dataTask를 생성
  • (5) : imageData가 nil이 아닐경우 UIImage로 변환
  • (6) : 이미지뷰 업데이트는 메인 스레드에서 실행

6. 에러 처리

Data Task 실행 중에 에러가 발생할 경우를 대비라여 적정한 에러 처리를 구현한다. 네트워크 연결 오류, 서버 응답 오류 등을 처리할 수 있다.

URLSession.shared.dataTask(with: imageUrl) { data, response, error in
    if let error = error {
        print("Error: \(error)")
    }
    if let httpResponse = response as? HTTPURLResponse {
        print("Status Code: \(httpResponse.statusCode)")
    }
    if let imageData = data, let image = UIImage(data: imageData) {
        DispatchQueue.main.async {
            self.randomImageView.image = image
        }
    }
}
  • error : 에러 정보 프린트
  • response : HTTP 응답에 대한 정보 프린트

7. 세션 종료

필요한 모든 데이터를 가져온 뒤에는 URLSession을 종료하여 리소스 관리와 메모리 누수를 방지한다.

session.finishTasksAndInvalidate()
  • invalidateAndCancel : 세션을 무효화하고 진행 중인 작업을 취소. 현재 세션과 해당 세션 내 모든 작업 중단
  • finishTasksAndInvalidate : 현재 진행 중인 작업을 완료한 후 세션을 무효화. 현재 작업들이 완료될 때까지 기다린 후 세션 종료
  • 아무 코드도 작성하지 않으면 앱 수명동안 유지되며, 작업이나 Task를 계속해서 추가할 수 있음

최종코드


import UIKit
import SkeletonView

class PetViewController: UIViewController {
    var imageList: [RandomImage] = []
    
    @IBOutlet weak var randomImageView: UIImageView!
    @IBOutlet weak var randomButton: UIButton!
    @IBOutlet weak var catImageView: UIImageView!
    @IBAction func randomButton(_ sender: Any) {
        getRandomCatImage()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        randomImageView.showAnimatedGradientSkeleton()
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            self?.randomImageView.hideSkeleton()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        getRandomCatImage()
    }
    
    func getRandomCatImage() {
        guard let url = URL(string: "https://api.thecatapi.com/v1/images/search") else { return }
        let session = URLSession(configuration: .ephemeral)
        let task = session.dataTask(with: url) { data, response, error in
            guard let data = data, error == nil else { return }
            let decoder = JSONDecoder()
            guard let randomCatImage = try? decoder.decode([RandomImage].self, from: data) else { return }
            if let imageUrl = URL(string: randomCatImage.first?.url ?? "") {
                let imageTask = URLSession.shared.dataTask(with: imageUrl) { data, response, error in
                    if let error = error {
                        print("Error: \(error)")
                    }
                    if let httpResponse = response as? HTTPURLResponse {
                        print("Status Code: \(httpResponse.statusCode)")
                    }
                    if let imageData = data, let image = UIImage(data: imageData) {
                        DispatchQueue.main.async {
                            self.randomImageView.image = image
                            session.finishTasksAndInvalidate()
                            print("세션 종료")
                        }
                    }
                }
                imageTask.resume()
            }
        }
        task.resume()
    }
}

🤳🏻 적용화면

세션이 종료되는 시점을 확인할 수 있으며, 해당 페이지로 접근 또는 꾹꾹이라는 버튼을 누르지 않는 이상 이미지를 로드할 일은 없으니 이미지뷰에 데이터를 불러오자마자 세션을 종료하도록 설정했다.

실행하고보니 문제아닌 문제로 느끼는건 바로 로딩 속도... 너무 느리다... 한국패치 plz 🤮

profile
하지만 나는 끝까지 살아남을 거야!

0개의 댓글