[Swift] weatherAPI JSON값 다루기, Closure

정환우·2021년 6월 18일
0

iOS

목록 보기
21/24
post-thumbnail

어제 하던거 계속 이어서 배우는 중. 이거 되게 신기하긴 한데 코드를 따라하다가 몇가지 의문점이 생겼다.


func fetchWeather(cityName: String){
        let url = "\(weatherURL)&q=\(cityName)"
        performRequest(urlString: url)
    }

func performRequest(urlString: String){
        if let url = URL(string: urlString){
            // URL Session 만들기
            let session = URLSession(configuration: .default)
            
            let task = session.dataTask(with: url, completionHandler: handler(data: urlresponse: error:))
            
            task.resume()
        }
    }
    
    func handler(data: Data?, urlresponse: URLResponse?, error:Error?)->Void
    {
        if error != nil{
            print(error)
            return
        }
        
        if let safedata = data{
            let dataString = String(data: safedata, encoding: .utf8)
            print(dataString)
        }
    }

이게 오늘 배운 코드의 전부라고 생각하면 되는데, 하나하나씩 해부해볼 예정.

먼저, URL이라는 구조체가 존재한다. url이라는 객체의 속성을 다루기 위해서 애플에서 정의해 놓은 구조체라고 생각하면 될 것 같다.

URLSession 이라는 클래스는 URL을 이용해서 데이터를 다운로드하고 업로드할때 사용하는 가장 기본적인 클래스 인 것 같다. 이 클래스에 있는 하위 클래스나 서브 인스턴스를 이용하는 것 같고, url을 다루기 위해 사용하는 가장 기초적인 클래스 정도로 이해하면 될 듯 하다. 그리고 얘도 delegate, protocol이 사용 가능함.

구글링을 해보니까 http를 포함한 몇 가지의 프로토콜을 지원하고, 인증, 쿠키 관리, 캐시 관리 등을 지원한다고 한다.

기본컨셉은

Session Configuration 정하고 생성 → 통신할 url, request 객체 설정 → 사용할 task를 결정하고 그에 맞는 completion handler나 delegate method 작성 → task 실행 → completion handler 실행

이라고 한다.

그렇다면 여기서 task가 뭐냐, session이 서버로 요청을 보낸 후 응답을 받을 때 URL 기반의 내용들을 받는 역할을 하는 것이다.

그리고 애플 공식문서에서 말하기를

After you create the task, you must start it by calling its resume() method.

task는 resume으로 시작해주는 게 국룰이라고 합니다.

completionhandler의 파라미터를 살펴 보면, data는 서버로 부터 받는 데이터, response는 metadata로, http 헤더나 status code같은 것들을 말한다고 한다. error는 그냥 에러인지 아닌지, 에러가 아니면 nil 값을 반환한다.

구글링을 하다보니 urlsession에 대해서 보기 좋게 정리해놓은 블로그가 있었다.

출처 : https://devmjun.github.io/archive/URLsession

URLSession HTTP 요청을 보내고 받는 핵심 객체 입니다. 제공되는 URLSessionConfiguration을 통해 다음 세가지 유형의 URL을 생성합니다.

  • .default: 디스크 지속(disk-persisted) 전역 캐시, 자격 증명(credential) 과 쿠기 저장 객체를 사용하는 기본 구성 객체(default configuration object)를 생성합니다.
  • .ephemeral: 모든 세션관련 데이터가 메모리에 저장된다는 점을 제외하고는 기본 구성(default configuration)과 다릅니다. 비공개(private) 새션이라고 생각하세요.
  • .background: 새션은 업로드와 다운로드를 백그라운드에서 이행합니다. 앱 그 자체가 일시중지(suspended) 되거나 시스템에 의해 종료되는(terminated by the system) 경우에도 전송이 계속됩니다.

URLSessionTask는 작업 객체(task object)를 나타내는 추상 클래스 입니다. 세션은 데이터를 가져오거나 파일을 업로드, 다운로드 하는 실제 작업을 수행하는 하나 이상의 테스크를 생성합니다.

세가지 유형의 구체적인 session tasks가 있습니다.

  • URLSessionDataTask: 서버에서 메모리로 데이터를 검색하는 HTTP GET요청에 이 테스크를 사용합니다.
  • URLSessionUploadTask: 전형적으로 HTTP POST, PUT 매소드를 통해서 디스크에서 웹서버로 파일을 전송할때 이 테스크를 사용하세요.
  • URLSessionDownloadTask: 임시의 파일 위치로 원격 서버에서 파일을 다운로드할때 이 테스크를 사용하세요

Closure

파이썬의 람다랑 비슷한건가?

클로저의 정의는 어떤 상수나 변수의 참조를 캡쳐해 저장할 수 있다는 것에 있다.

클로저의 표현 문법은 일반적으로

{ (parameters) -> return type in
	statements
}

이런식의 형태를 띤다고 한다.

여기까지는 뭔가 함수를 인자로 넣거나 간소화하는 듯한 느낌이든다.

var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})
var reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

이게 클로저를 사용한 얘인데, 두 코드의 의미가 같다.

그러니까 메소드에서 어떤 타입의 인자가 들어와야 하는지 알기 때문에 타입의 생략이 가능하다. 가독성과 코드의 모호성이 발생할 수 있는 경우에는 생략을 지양해야 할 것.

여기서 더 줄일 수 있다는데, 나는 안줄일래..

그래도 알아보면

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
// 단일 표현 클로저이므로 반환 키워드 생략 가능
reversedNames = names.sorted(by: { $0 > $1 }
// 인자 이름도 축약 가능.
// 제공하는 축약 인자 이름. $0부터 시작해서 순서대로.

reversedNames = names.sorted(by: >)
// Swift의 String 타입 연산자에는 String 끼리 비교 가능한 비교 연산자를
// 구현해두었기 때문에 줄일 수 있다고 한다. 아 어지러워

후위 클로저

함수에서 클로저를 사용할 때, 클로저의 바디가 너무 길고 복잡하다면 함수의 마지막 인자로 클로저를 사용하고, 후위 클로저를 사용하여 가독성을 높일 수 있다.

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

이렇게 말이다

값 캡쳐

그렇다면 이건 대체 뭘까?

원본 값이 사라져도 클로져의 body에서 그 값을 활용하는 것을 값 캡쳐라고 한다. 근데 이게 대체 뭘까..왜 쓰는 걸까..? 알아보자.

Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수(nested function) 이라고 한다.

예제를 보면

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

일단 이 함수의 반환 값은 클로저인데, (forIncrement amount: Int) 이게 인자 값이고, () -> Int 이게 반환 값인 함수이다.

incrementer 함수에 runningTotal, amount 값은 정의가 되어 있지 않지만 실행이 된다. 왜냐하면 캡쳐링이 되었기 때문.

아니 뭔가 함수안에 함수가 실행되는게 자연스럽다. 이게 스위프트인가..? 혼란이 온다 혼란이 와..

근데 또 중요한게 클로저랑 함수는 참조 타입이다. 그니까 c로 따지만 함수 포인터를 저장하는? 느낌이라고 생각하면 된다.

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 값으로 10을 반환합니다.
incrementByTen()
// 값으로 20을 반환합니다.
incrementByTen()
// 값으로 30을 반환합니다.

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

incrementByTen()
// 값으로 40을 반환합니다.

함수는 각기 실행 되지만 실제로는 캡처링 되어서 계산이 누적된 결과를 갖는다. 이게 캡처링의 중요성..?

그리고 클로저는 각기 다른 저장소를 갖기 때문에 다른 클로저는 중첩되지 않는다.

만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그게 인스턴스를 캡처링하면 강한 참조 순환에 빠지게 된다. 이 문제는 스위프트에서 캡쳐 리스트를 사용하여 해결한다고 하는데, 이건 좀 나중에 알아보자..

JSON

json이란 무엇일까?

json이란 JSON은 속성-값 쌍 또는 "키-값 쌍"으로 이루어진 데이터 오브젝트를 전달하기 위해 인간이 읽을 수 있는 텍스트를 사용하는 개방형 표준 포맷이다.

예전에 게임 동아리에서 게임을 만들때 사용하던 걸 본적이 있다. 그러니까 API로 데이터를 주고 받기 위해서는 json 인코딩, 디코딩이 필수적이라는 뜻.

여기서 weather에 관한 정보를 json으로 받을 것이기 때문에 json을 swift에서 해석하는 방법을 알아야한다.

JSON 파일 변환하기

애플 공식문서를 참고해보자. 예시가 나와있다.

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let json = """
{
    "name": "Durian",
    "points": 600,
    "description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)

print(product.name) // Prints "Durian"

코드를 하나하나씩 분석해보자. 오늘 살펴볼 중요한 키워드는 Codable, JSONDecoder(), try 이 3가지이다.

먼저 Codable 이 무엇이냐 하면, Encodable, Decodable 프로토콜을 준수하는 타입 프로토콜이라고 할 수 있다. 저 두 녀석들도 프로토콜이다.

아마도 저걸 채택하는 클래스, 구조체 등은 json으로 변환하거나 json을 객체로 디코딩하거나 할 수 있을 것 같다.

그렇다면 JSONDecoder()은 무엇일까.

json을 decode해주는 클래스라고 생각하면 될 것 같다. 저 클래스 자체는 어려운게 아닌데 사용법이 중요하다.

저기서 사용한 decode 인스턴스의 원본을 살펴보면

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable

이렇게 되어있다. 오랜만에 보는 템플릿 형식인데, 해석하자만 DataT.Type 으로 변환해 주는 것 같다. 그런데 이 과정에서 오류가 발생할 수 있으므로, 기본적으로 인스턴스에 throws가 선언되어 있고, 그렇기 때문에 throw catch문을 이용해서 오류 제어를 해주어야 한다.

왜 이게 오류가 발생할 수 있는가 생각을 해보면, 극단적으로 예시를 들어서 json 데이터는 우리가 서버와 통신을 하기 위해 사용하는데, 서버에서 이름-나이 쌍의 데이터를 포함한 json을 받아오다가 어쩌다보니 나이가 빠져서 통신이 됐다. 그러면 나이 값을 갖고 오지 못했으므로 오류가 발생한다.

뭐 이것을 옵셔널을 이용하면 예방할 수 있긴한데, 아무튼 단적인 예로 이러한 오류들이 발생할 수 있기 때문에 throw catch를 사용한다고 생각하면 편할듯.

저기서 Type값에 .self가 뒤에 붙는데 클래스나 구조체 자체의 타입을 사용한다고 표현을 하긴 하는건데, 왜 .self 까지 붙어야하는지는 모르겠다. 그냥 Type:GroceryProduct 이렇게 사용하면 안되는건가? 이건 좀 의문이다.

엄청 복잡해지기 시작했는데, 본게임이 시작된 것 같다. 오늘 내용은 복습이 꼭 필요할듯.

0개의 댓글