[iOS] URLSession을 알아보자 - 2

Youth·2023년 7월 1일
1

TIL

목록 보기
8/21

URLSession 알아보기(실습)

저번 글에서는 URLSession의 기본개념을 알아봤고 우리가 실제로 데이터를 불러오는 부분
즉, 저번 글에서 "네이버에 들어가게되면 네이버스포츠에 들어가야지"라고 상상하는 부분을 실제로 구현을 해보려한다. 이번 글에서 설명하는 방식은 가장 기초적인 방식이라고 생각하면 될거같다

1. 기본적인 URLComponents만들기

이번실습에서는 애플의 itunes API를 사용해서 데이터를 가져와볼거다. 기본 URL은
"https://itunes.apple.com/search?media=movie&term=movie"이고 queryItems정도만 URLComponents로 구성해줬다

let term = URLQueryItem(name: "term", value: mediaType.queryValue)
let media = URLQueryItem(name: "media", value: mediaType.queryValue)
let querys = [term, media]
var components = URLComponents(string: "https://itunes.apple.com/search")
components?.queryItems = querys
guard let url = components?.url else { return }

분명히 저번엔 componetns.url이었는데 components?.url에 저 물음표는 뭔가요?

let components = URLComponents()

위와같은 코드는 객체만 만들어서 반환해주면 되기때문에 components가 URLComponents타입이지만

var components = URLComponents(string: "https://itunes.apple.com/search")

string을 가지고 URLComponents를 만들면 애초에 optional일 가능성이 있어서 이 상황에서 components변수는 URLComponets?타입이다. URLComponents객체만 만들어서 scheme, host, path를 넣어주면 옵셔널이 아닐거다

2. dataTask만들기

그러고 나서 URLSession을 만들고 dataTask를 만들어보자
근데 우리가 네트워킹을할때는 비동기처리때문에 completionHandler가 있는 dataTask메서드를 사용해야하는건 알겠는데 아래 사진 3번째와 4번쨰 사진은 with이라는 param에 어떤거는 URLRequest가 들어가고 어떤거는 URL이들어간다...

대체 이 두개가(dataTask에 URL이 들어가는거와 URLRequest가 들어가는것) 어떤 차이점이 있는지 간단한게 알아보고 넘어가자

URLRequest를 이용하면 HttpMethod와 Http헤더를 설정해줄 수 있다. 사실 HttpMethod의 기본값은 GET으로 세팅되어 있고, 필수적인 Http헤더는 이미 세팅되어 있다. 그렇기 때문에 단순히 GET요청일 경우 URL로 dataTask를 만들어 사용해도 된다.
가끔, Authorization와 같이 인증키가 GET요청을 할때 필요할 수 있는데 이때는 GET요청일때도 URLRequest를 이용해서 dataTask를 만들어야한다. POST요청의 경우 반드시 URLRequest로 Task를 만들어야한다

참고로 GET 메서드의 경우 URL만으로도 요청을 보내도 되는 이유는 아래와 같다고한다

HTTP 요청의 경우 GET방식인 경우에는 무조건 URL 끝에 쿼리스트링으로 value=text 형식으로 보내지기 때문에 Content-Type은 필요가 없다.
즉 GET방식으로 데이터를 전송 시 웹서버 입장에서는 value=text 형식 데이터라는 것을 알 수 있기 때문이다.

아무튼 우리는 단순한 GET메서드이기도하고 인증키를 보낼 필요도 없는 API이니까 이번에는 4번째 녀석인 URL가지고 dataTask를 실행해보면된다

당연한 이야기지만 resum()을 빼먹으면 안된다

dataTask의 completionHandler 구성하기

error

우선 url에는 아까 만들어놓은 url을 넣어놓고 completionHandler에 input에는 Data?타입과 URLResponse?타입과 Error?타입이 들어올텐데 각각의 이름을 단순하게 data, response, error라고 하고 시작해보자

보면 우선 우리는 네트워킹을하면 data랑 response랑 error가 들어오는구나 라는걸 알수있는데 아마 이런생각이 먼저들거다

에러가 들어오면 안되는거잖아...?

맞는말이다 그러니 에러를 먼저 처리해줘야하는데 기본적인 논리는 이렇다

error는 Error?(옵셔널)타입이네 그러면 nil이면 에러가 없다는거니까 괜찮은거네? 반대로 nil이 아니면 에러가 있다는거니까 거기서 문제가 발생한거겠네?

URLSession.shared.dataTask(with: url) { data, response, error in
    /// 여기다가 이제 개발하면됨
    guard error == nil else {
        print("request API Error: Call error")
        return
    }
}.resume()

애초에지금 쓰고있는 모든 코드는 아래함수안에 들어있는 코드다(맨 아래에 전체 코드 첨부)

static func request(mediaType: MediaType, completion: @escaping ((MovieModel) -> Void))

이 함수는 애초에 return이 없는 함수이기 때문에 return을 만나면 함수가 끝난다. 그래서 guard를 통과하지 못하면(error에 nil이 아닌 다른값이 있다 == 에러가있다) else안으로 들어가서 print를 출력하고 return을 만나 함수가 끝나게된다

이 error를 처음에 처리해주는 이유는 당연히 다른코드를 실컷 실행하고 error를 처리해버리면 어차피 error를 만나면 그 다른코드를 실행할 필요가 없는데 메모리 낭비이기 때문이다.


Response

그리고 다음은 response차례이다 response는 우리가 이걸로 statusCode를 알수있고 이를 바탕으로 에러처리를해줄수있다. StatusCode는 우리가 202 404같은것들을 이야기한다

근데 문제는 우리가 썼던 함수의 URLResponse타입에는 statusCode변수가 없다...

근데 아래로 쭉 내려보면 URLResponse를 상속받고있는(URLResponse를 superClass로하는) HTTPURLResponse라는 클래스의 init에 statusCode라는 속성이 있는걸 확인할수있다

음 자식클래스(subClass, 여기서 HTTPURLResponse)를 부모클래스(superClass, 여기서 URLResponse를)로 바꿀수있는방법...?

바로 다운캐스팅을 이용하면된다(as? <= 이렇게 생긴녀석)

URLSession.shared.dataTask(with: url) { data, response, error in
    /// 여기다가 이제 개발하면됨
    guard error == nil else {
        print("request API Error: Call error")
        return
    }
    
    guard let response = response as? HTTPURLResponse,
                         (200...300).contains(response.statusCode) else {
        print("request API Error: Status Code not included in the scope")
        return
    }
}.resume()

우선 response를 HTTPURLResponse로 다운캐스팅을 해줘야하지만 다운캐스팅을 실패할수있으므로 guard문으로 실패가능성을 처리해주고 guard문에서 ","는 &&의 의미를 가진다 즉 다운캐스팅이 성공하고 "그리고" status코드가 200이상 300미만인 범위안에 포함되면 else에 걸리지 않는다

(200...300) ~= response.statusCode 라고 쓰는것과 완전히 동일한 코드이다

만약에 다운캐스팅이 실패하거나 statusCode가 200과 300사이가 아니면 else로 들어와서 print를 하고 return을 만나서 함수가 종료된다


data

data는 크게 두가지인데 우선 옵셔널을 풀어하고 data타입의 서버에서의 데이터를 swift에서 사용할수있는 데이터형식으로 바꿔줘야한다

서버에서 넘어오는 json을 Swift에서 사용할수있게 구조체를 이용해서 바꿔놓은 모델이다

struct MovieModel: Codable {
    let resultCount: Int
    let results: [MovieResult]
}

struct MovieResult: Codable {
    let trackName: String?
    let previewUrl: String?
    let artworkUrl: String?
    let releaseDate: String?
    let shortDescription: String?
    let longDescription: String?
    
    enum CodingKeys: String, CodingKey {
        case trackName
        case previewUrl
        case artworkUrl = "artworkUrl100"
        case releaseDate
        case shortDescription
        case longDescription
    }
}

이러한 서버데이터를 swift데이터로 바꾸는 과정을 decode라고 하고 우리는 json형식의 데이터로부터 데이터를 바꿀거기때문에 JSONDecode라고한다. 그리고 swift에는 이와같은 역할을하는 JSONDecoder를 제공한다.

URLSession.shared.dataTask(with: url) { data, response, error in
    /// 여기다가 이제 개발하면됨
    guard error == nil else {
        print("request API Error: Call error")
        return
    }
    
    guard let response = response as? HTTPURLResponse, (200...300).contains(response.statusCode) else {
        print("request API Error: Status Code not included in the scope")
        return
    }
    
    guard let hasData = data else {
        print("request API Error: data is nil")
        return
    }

    guard let outputData = try? JSONDecoder().decode(MovieModel.self, from: hasData) else {
        print("request API Error: Decode Error")
        return
    }
    
}.resume()

주의할점이 있다면 당연히 JSONDecode는 swift구조체로 바꾸는걸 실패할가능성이 있고 애초에 함수자체가 throw하는(시스템 내부에서 error를 던질 수있는) 함수이다.

간단한 설명을 붙이면 throw하는(에러가 발생할수있는) 함수를 실행할때는 함수앞에 try를 붙여야하는데 try?의 의미는 에러가 발생하면 nil을 리턴하겠다는 의미이다

그렇다면 저 코드에서 outputDat는 JSONDecoder가 에러를 발생시키면 nil을 리턴시키고 그렇게되면 outputData에 nil이 들어가게되고 else로 들어와서 pirnt를 실행시키고 return으로 함수를 종료하게된다.

이러한 guard문을 다 통과하고(else로 안빠지고) 코드를 실행시키면 그때서야 outputData를 네트워크의 결과로 completionHander의 input으로 넣어줄수있다.

URLSession.shared.dataTask(with: url) { data, response, error in
    guard error == nil else {
        print("request API Error: Call error")
        return
    }
    
    guard let response = response as? HTTPURLResponse, (200...300).contains(response.statusCode) else {
        print("request API Error: Status Code not included in the scope")
        return
    }
    
    guard let hasData = data else {
        print("request API Error: data is nil")
        return
    }
    
    guard let outputData = try? JSONDecoder().decode(MovieModel.self, from: hasData) else {
        print("request API Error: Decode Error")
        return
    }
    print("✅네트워킹 완료✅")
    completion(outputData)
}.resume()

최종 코드

구글링을 해보면 이거보다 훨씬 간단한 코드도 많겠지만 기본은 이 코드라고 생각하고 공부해보고 구글링했을때 나오는 간단한 코드들을 이해하면 될거같다

class NetworkLayer {    
    static func request(mediaType: MediaType, completion: @escaping ((MovieModel) -> Void)) {
        /// 쿼리만들기(media=movie&term=movie이부분만들기)
        let term = URLQueryItem(name: "term", value: mediaType.queryValue)
        let media = URLQueryItem(name: "media", value: mediaType.queryValue)
        let querys = [term, media]
        var components = URLComponents(string: "https://itunes.apple.com/search")
        let componet = URLComponents()
        components?.queryItems = querys
        guard let url = components?.url else { return }
        URLSession.shared.dataTask(with: url) { data, response, error in
            guard error == nil else {
                print("request API Error: Call error")
                return
            }
            
            guard let response = response as? HTTPURLResponse, (200...300).contains(response.statusCode) else {
                print("request API Error: Status Code not included in the scope")
                return
            }
            
            guard let hasData = data else {
                print("request API Error: data is nil")
                return
            }
            
            guard let outputData = try? JSONDecoder().decode(MovieModel.self, from: hasData) else {
                print("request API Error: Decode Error")
                return
            }
            print("✅네트워킹 완료✅")
            completion(outputData)
        }.resume()
    }
}

실제 사용

NetworkLayer.request(mediaType: .movie) { model in
    dump(model)
    self.movieModel = model
}

출력값

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글