ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [맥주] 맥주 추천 어플 만들기(1) - API Requset
    카테고리 없음 2021. 12. 23. 22:42

    Device : 2021 M1 Macbook pro
    OS : macOS Monterey 12.1
    Xcode : Version 13.1 (13A1030d)

    결과물 예시

    이제 Alamofire와 맥주 API를 이용해서 추천 맥주의 정보를 받아오는 부분을 구현하려합니다.

    API 테스트

    맥주 API에 들어가서 문서를 보면, 추천 맥주를 받아오는 API가 굉장히 간단하게 구현되어있습니다.

    이걸 테스트 해 보려면 간단하게 curl을 제외한 주소를 복사해서 safari에서 열어주면 됩니다.

    API 호출 결과

    위 화면은 Safari Json Extension을 통해서 확인한 결과이므로, 필요하다면 링크에 들어가서 설치하시면 됩니다.

    구조체 Model 만들기

    이 JSON결과를 우리가 사용할 수 있게 디코딩하려면, JSON결과와 매칭되는 Codable(Encodable & Decodable) Model객체가 필요합니다.

    예를 들어서 위 이미지에서 id속성을 코드에서 Beer.id와 같은 형태로 접근하기 위해서는 JSON결과를 디코딩해서 구조체에 저장해야 하는 것 입니다.

    Generate Model를 이용해서 간단하게 구조체 모델을 만들 수 있습니다.

    위에서 테스트해본 response를 그대로 복사해서 위 홈페이지에 붙여넣어주면, 오른쪽에 swift 객체가 만들어지게 됩니다.

    왼쪽은 JSON, 오른쪽은 Swift Code

    일단 Copy Code를 눌러 모든 코드를 복사해옵니다.

    프로젝트 내에 Model 폴더를 만들고, 그 안에 새로운 Swift파일을 만든 뒤, 복사한 코드를 붙여넣어줍니다.

    Model 폴더 안에 BeerInfo파일

    그 후, 필요한 프로퍼티들만 남기고 모두 제거해줍니다.

    구조체의 이름도 목적에 맞게 변경 해 주었습니다.

    import Foundation
    
    typealias Beer = [BeerInfo]
    
    // MARK: - BeerInfo
    struct BeerInfo: Codable {
      let id: Int
      let name, tagline: String
      let beerDescription: String
      let imageURL: String
      let foodPairing: [String]
      let brewersTips: String
    
      enum CodingKeys: String, CodingKey {
        case id
        case name, tagline
        case beerDescription = "description"
        case imageURL = "image_url"
        case foodPairing = "food_pairing"
        case brewersTips = "brewers_tips"
      }
    }

    맥주 API에 Request해보기

    이제, 모든 준비는 끝났고 원하는 정보를 요청해볼 차례입니다.

    Alamofire을 import한 뒤에, ViewController파일에 다음과 같은 함수를 만들어봅시다.

    func requestRandomBeer() {
      let baseURL = URL(string: "https://api.punkapi.com/v2/beers/random")!
      // 1
      AF.request(baseURL).response { response in
      // 2
        if let error = response.error {
          print("Request Error : ", error)
          return
        }
        guard let data = response.data else {
          print("Response Data Error")
          return
        }
      // 3
        guard let decodedData = try? JSONDecoder().decode(Beer.self, from: data) else {
          print("Decode Error")
          return
        }
      // 4
        guard let beer = decodedData.first else { return }
        print(beer)
      }
    }

    간단하게 viewDidLoad()에서 함수를 실행 해보면, 아래와 같은 결과를 얻을 수 있습니다.

    잘 나온다!

    간단하게 코드에 대해서 설명 해 보자면 다음과 같습니다.

    • 1 에서는 Alamofire의 기본적으로 사용하는 Session(AF == URLSession.default)을 통해서 baseURLrequest를 보내고, 그 응답을 response를 통해 받아옵니다.
      그 뒤의 closure은 비동기적인 request요청이 끝나면 실행되는 compeletion hander입니다.
    • 2 에서는 응답으로 들어온 response.error가 nil이 아닐경우, error가 발생 한 것이므로 그 경우에 대해 처리해줍니다.
      또한 data가 nil일 경우에 대해서도 처리해줍니다.
    • 3 에서는 response를 디코딩 해주는 과정입니다. 앞서 만들어 놓은 Model class(Beer)의 메타타입(Beer.self)과 디코딩할 데이터를 파라미터로 넣어줍니다.
    • 4 에서는 배열의 첫번째 원소를 추출하는 과정입니다. 앞서 API를 테스트 했을 때의 response를 보면, Array의 형태인 것을 알 수 있습니다. 그렇기 때문에 첫번째 원소만 추출해서 사용할 것입니다.

    사실 response는 원소가 1개인 Array

    response를 통해 다시 맥주 image 받아오기

    위에서 얻은 response를 잘 살펴보면 image주소를 찾을 수 있습니다.

    잘 안 보인다면 print(beer) 대신 print(beer.imageURL)을 해 보면 한눈에 알 수 있습니다.

    response에 있는 imageURL

    이제 다시 이 imageURL을 통해서 image를 받아올 수 있습니다.

    Decoding Model 디버깅

    그런데 여러번 request를 돌리는 와중에 종종 Decode Error가 발생합니다.

    이것은 Model이 디코딩할 JSON과 형식이 맞지 않기 때문에 디코딩을 실패한 것입니다.

    어느 요소가 형식과 일치하지 않는지 살펴보기 위해서 의심이 가는 Modle 요소에 Optional을 추가한 뒤 테스트 해 보았습니다.

    imageURL에서 nil을 만났다

    가끔씩 imageURL이 없는 경우가 있기 때문에 imageURL 요소를 Optional로 만들어 주어야 합니다.

    물음표가 붙었다

    다시 request

    Image를 request하기 위해서 처음 만들었던 함수도 수정 해 줍니다.

    func requestRandomBeer() {
      let baseURL = URL(string: "https://api.punkapi.com/v2/beers/random")!
      // 1
      AF.request(baseURL).response { [weak self] response in
        if let error = response.error {
          print("Request Error : ", error)
          return
        }
        guard let data = response.data else {
          print("Response Data Error")
          return
        }
        guard let decodedData = try? JSONDecoder().decode(Beer.self, from: data) else {
          print("Decode Error")
          return
        }
        guard let beer = decodedData.first else { return }
    
        guard let imageURLString = beer.imageURL,
              let imageURL = URL(string: imageURLString) else { return }
        // 2
        self?.requestBeerImage(url: imageURL)
      }
    }

    간단하게 달라진 부분에 대해서 설명하자면

    1 weak self

    Swift ARC(Autometic Reference Counting)의 동작원리상, 참조 카운트(누군가 자신의 참조를 갖고있으면 +1)가 0이 되어야만 해당 객체의 메모리를 청소해준다.

    response의 함수 원형

    response의 함수 원형을 보면, completionHander에 해당하는 closure가 @escaping인 것을 볼 수 있습니다.

    @escaping은 함수의 실행이 종료 된 뒤에 실행되는 closure인데, 그 말은 즉 비동기적으로 동작하는 response의 동작이 완전히 끝날 때 까지 메모리상에 closure가 남아서 함수의 동작이 끝난 뒤에 실행이 된다는 뜻입니다.

    방금 한 말과, ARM의 동작 원리를 생각 해 본다면, closure가 끝날 때 까지 closure에서 캡쳐한 self는 참조카운트가 +1 되어있는 상태입니다.

    weak키워드는 해당 객체의 참조 카운트를 +1 하지 않으면서 객체를 참조하는 형태입니다.

    현재 상황을 다이어그램으로

    따라서 ViewController는 completionHander의 존재 유무에 상관없이 필요할 때 사라질 수 있습니다.

    2 에서는 weak self의 영향을 받은 것으로, self가 사라진 상황에서는 실행할 수 없기 때문에 optional chaining을 했습니다.

    간단하게 설명한다면서 너무 길어져버렸네요..

    다음, image를 받아오는 함수를 살펴보면 아래와 같습니다.

    func requestBeerImage(url: URL) {
      AF.request(url).response { response in
        if let error = response.error {
          print("Request Error : ", error)
          return
        }
        guard let data = response.data else {
          print("Data Error")
          return
        }
        guard let image = UIImage(data: data) else {
          print("Image Error")
          return
        }
        print(image)
      }
    }

    위와 거의 동일하므로 설명은 생략하겠습니다.

    그 결과 이미지 객체가 잘 생성 됐습니다!

    뭔가 잘 된 느낌

    댓글

Designed by Tistory.