멈춰!!! 선행 학습이 필요한 글이니 앞선 글들을 읽고 학습해보자!

URLSession 이란?

URL 생성하기

URLSession 적용하기

Codable 개념 파악하기

선행학습을 끝냈으니 드디어 API를 통해 원하는 데이터를 가져와보자잇!

Codable 프로토콜을 채택한 구조체 선언

  • 우선 구조체를 정의하기 전에 API를 호출했을때 데이터가 어떻게 날아오는지 먼저 확인하자
  • 주소창에 API 호출 주소를 입력하고 표시된 내용을 복사하여 https://jsonlint.com 이곳에서 확인해보자
  • 내용을 확인해보니 위와 같은 데이터로 Json 형태가 호출된다
  • 여기서 [ 평점, 제목, 줄거리, 포스터 경로 ] 네개만 일단 받아와 보자!
struct Response: Codable {
    let page: Int?
    let result: [MovieInfo]
    
    enum CodingKeys: String, CodingKey {
        case page
        case result = "results"
    }
}

struct MovieInfo: Codable {
    let title: String?
    let rating: Double?
    let summary: String?
    let post: String?
    
    enum CodingKeys: String, CodingKey {
        case title
        case rating = "vote_average"
        case summary = "overview"
        case post = "poster_path"
    }
}
  • Response 구조체를 Page 정보와, 정보를 배열로 담고있는 results를 정의한다
  • MovieInfo 구조체에는 [ 영화제목, 평점, 개요, 영화 포스터 ] 의 정보를 담는다
  • 구조체의 형태와 CodingKey 가 뭔지 이해하려면 본 글 제일 위에있는 선행학습을 하자! → Codable

Json 을 Model 로 Decode

// 데이터 파싱하기
    do {
        let decoder = JSONDecoder()
        let respons =  try decoder.decode(Response.self, from: resultData)
        let searchMovie = respons.result
        
        print("영화 제목 : \(searchMovie[0].title ?? "")")
        print("영화 평점 : \(searchMovie[0].rating ?? 0)")
        print("영화 줄거리 : \(searchMovie[0].summary ?? "")")
        print("포스터 경로 : \(searchMovie[0].post ?? "")")
    } catch let error {
        print(error.localizedDescription)
    }
  • JSONDecoder 를 통해 파싱을 시도하고, 이때 Error가 발생하면 Catch로 이동 할 것이다
  • 파싱이 완료 된후, 첫번째 인덱스의 정보들을 출력해 본다
/*
 영화 제목 : 어벤져스 오브 저스티스
 영화 평점 : 4.1
 영화 줄거리 : 은하계 최강의 빌런 조크스터는 태양을 없애 지구에 새로운 빙하기를 불러일으키고자 한다. 슈퍼히어로 슈퍼배트는 히어로 동료들과 함께 조크스터에 맞서 지구를 지켜내야만 한다. 이제 전 인류의 운명이 걸린 최후의 대결이 시작된다.
 포스터 경로 : /yymsCwKPbJIF1xcl2ih8fl7OxAa.jpg
 */
  • 정보를 잘 가져온 것을 확인할수 있다!
  • print 문에서 nil 병합 연산자 ( \\(searchMovie[0].title ?? "" ) 를 사용한 이유는 해당 데이터가 Optional로 오기 때문에 사용하였다, 실제 iOS 앱에서는 적절한 바인딩을 해야한다!
  • nil 병합 연산자는 해당 변수 및 상수가 nil이 아니면 원래 값을 반환하고, nil 일 경우 ?? 뒤에 오는 Default 값을 반환한다

⇒ 포스터 경로의 경우 TMDB의 API 문서를 보면 되는데

https://www.themoviedb.org/t/p/[포스터 사이즈]/[포스터 경로]

형태로 들어가면 img 파일을 볼수 있다

 

예시 아래 주소를 들어가보자!

https://www.themoviedb.org/t/p/w220_and_h330_face/yymsCwKPbJIF1xcl2ih8fl7OxAa.jpg

전체코드

// 구조체 선언
struct Response: Codable {
    let page: Int?
    let result: [MovieInfo]
    
    enum CodingKeys: String, CodingKey {
        case page
        case result = "results"
    }
}

struct MovieInfo: Codable {
    let title: String?
    let rating: Double?
    let summary: String?
    let post: String?
    
    enum CodingKeys: String, CodingKey {
        case title
        case rating = "vote_average"
        case summary = "overview"
        case post = "poster_path"
    }
}

let API_KEY = "발급받은 API KEY"
var movieSearchURL = URLComponents(string: "https://api.themoviedb.org/3/search/movie?")

// 쿼리 아이템 정의
let apiQuery = URLQueryItem(name: "api_key", value: API_KEY)
let languageQuery = URLQueryItem(name: "language", value: "ko-KR")
let searchQuery = URLQueryItem(name: "query", value: "어벤져스")

// URLComponents 에 쿼리 아이템 추가
movieSearchURL?.queryItems?.append(apiQuery)
movieSearchURL?.queryItems?.append(languageQuery)
movieSearchURL?.queryItems?.append(searchQuery)

// 옵셔널 바인딩
guard let requestMovieSearchURL = movieSearchURL?.url else { throw NSError() }

// configuration 설정 -> default
let config = URLSessionConfiguration.default

// session 설정
let session = URLSession(configuration: config)

// dataTask 설정
let dataTask = session.dataTask(with: requestMovieSearchURL) { (data, response, error) in
    guard error == nil else { return }
    
    guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
    let successRange = 200..<300
    guard successRange.contains(statusCode) else { return }
    
    guard let resultData = data else { return }

    // 데이터 파싱하기
    do {
        let decoder = JSONDecoder()
        let respons =  try decoder.decode(Response.self, from: resultData)
        let searchMovie = respons.result
        
        print("영화 제목 : \(searchMovie[0].title ?? "")")
        print("영화 평점 : \(searchMovie[0].rating ?? 0)")
        print("영화 줄거리 : \(searchMovie[0].summary ?? "")")
        print("포스터 경로 : \(searchMovie[0].post ?? "")")
    } catch let error {
        print(error.localizedDescription)
    }
}

dataTask.resume()

공식문서 정의

개요

A type that can convert itself into and out of an external representation.

→ 외부 표현으로 전환할 수 있는 유형

정의

typealias Codable = Decodable & Encodable

내용

Codable is a type alias for the Encodable and Decodable protocols. When you use Codable as a type or a generic constraint, it matches any type that conforms to both protocols.

→ Codable은 Encodable 및 Decodable 프로토콜의 typealias입니다. Codable을 사용하면 두 프로토콜을 모두 준수해야 한다.

  • Encodable -> data를 Encoder에서 변환해주려는 프로토콜로 바꿔주는 것 ( 모델 → Json )
  • Decodable -> data를 원하는 모델로 Decode 해주는 것 ( Json → 모델 )

⇒ Swift4 버전부터 추가되었으며 기존 Decodable 과 Encodable 을 하나로 합친 프로토콜

⇒ Type 이 모두 지정되어 있는경우 Decoable과 Encodable을 별도 채택 안해도 Codable만으로 사용

⇒ Class , Struct, Enum 상관 없이 모두 사용 가능한 프로토콜!!!

 

코드로 확인

Codable 채택

  • Codable 을 채택한 Struct 생성
struct User: Codable {
    let age: Int
    let name: String
    let phone: String
    let address: String
    
    // Json 정보와 struct의 정보를 파싱하기 위해 사용 CodingKey
    // 이름이 같으면 원형 그대로 사용하고, 다른경우 별도 지정해주면 된다
    enum CodingKeys: String, CodingKey {
        case age
        case name
        case phone = "phone_number"
        case address
    }
}
  • Codable 프로토콜을 User 구조체에 채택
  • 서버로부터 받아오는 데이터의 이름과 구조체에서 정의한 이름이 다를 수 있기 때문에 CodingKey를 이용하여 적용해준다 , 이때 서버와 구조체의 이름이 동일하다면 그대로 사용하면 되며 다를경우 파싱해준다

⇒ 예제 코드에서 Json 데이터의 phone 이름이 phone_number 로 오기 때문에 파싱을 해주었다.

Decodable ( 외부 데이터 → 모델 ) 적용

// Decode
extension User {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        age = try container.decode(Int.self, forKey: .age)
        name = try container.decode(String.self, forKey: .name)
        phone = try container.decode(String.self, forKey: .phone)
        address = try container.decode(String.self, forKey: .address)
    }
}

func decode() {
    let jsonString = """
                            [
                                {
                                    "age": 18,
                                    "name": "홍길동",
                                    "phone_number": "010-1234-5678",
                                    "address": "인천"
                                },
                                {
                                    "age": 19,
                                    "name": "이길동",
                                    "phone_number": "010-1234-5678",
                                    "address": "서울"
                                },
                                {
                                    "age": 23,
                                    "name": "김동길",
                                    "phone_number": "010-1234-5678",
                                    "address": "강원"
                                }
                            ]
                          """
    let jsonData = jsonString.data(using: .utf8)
    do {
        guard let jsonData = jsonData else { return }
        let dataModel = try JSONDecoder().decode([User].self, from: jsonData)
        print(dataModel)
				/*
					 [
							{age 18, name "홍길동", phone "010-1234-5678", address "인천"}, 
							{age 19, name "이길동", phone "010-1234-5678", address "서울"}, 
							{age 23, name "김동길", phone "010-1234-5678", address "강원"}
						]
				*/
    } catch let error {
        print(error)
    }
}

decode()
  • extension 을 이용해 User 모델에 Decode를 적용시킨다
  • Decode의 경우 초기화를 우선 해주어야 하기 때문에 init을 통해 이를 수행한다
  • container 상수에 CodingKey를 적용시킨 Decode 를 수행 각 모델 내의 상수또한 해당 이름으로 파싱한다
  • jsonString 변수에 Json 형태의 데이터가 들어있으므로 이를 우리가 원하는 User 모델로 파싱
  • let dataModel = try JSONDecoder().decode([User].self, from: jsonData) 해당 구문을 통해 Json 형태의 데이터가 User 모델로 파싱된다.

⇒ 해당 구조는 공식문서에도 나와있는 형태이기 때문에 잘 기억하자!

Encode ( 모델 → 외부 데이터 ) 적용

// Encode
extension User {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(age, forKey: .age)
        try container.encode(name, forKey: .name)
        try container.encode(phone, forKey: .phone)
        try container.encode(address, forKey: .address)
    }
}

func encode() {
     let userObject = [
        User(age: 18, name: "홍길동", phone: "010-1234-5678", address: "인천"),
        User(age: 24, name: "김재우", phone: "010-1234-5678", address: "서울"),
        ]
        
     do {
         let jsonData = try JSONEncoder().encode(userObject)
         let jsonString = String(data: jsonData, encoding: .utf8)
         guard let printJsonString = jsonString else { return }
         print(printJsonString)
				/*
					 "[
							{"age":18,"name":"홍길동","phone_number":"010-1234-5678","address":"인천"},
							{"age":24,"name":"김재우","phone_number":"010-1234-5678","address":"서울"}
						]"
				*/
     } catch let error {
         print(error)
     }
}

encode()
  • 이번엔 Json 형태의 데이터를 우리가 만든 User 모델로 파싱하기 위해 Encodable을 사용해 보자
  • Decode와 과정이 비슷하지만 그 순서를 역순으로 하면 된다!
  1. Encoder 를 통해 Json 파일을 인코딩 하고
  2. 인코딩한 데이터를 String 형태로 변환해주고 ( 한글이 포함될 수 있으니 utf8 로! )
  3. 인코딩한 데이터가 nil 인지 파악한후! 사용하면 된다 ( 와! 정말 대단해! )

⇒ Encodable 과 Decodable 을 따로 쓰면 다소 복잡할 수 있지만, Codable 프로토콜을 채택 함 으로써 한번에 적용 시킬수 있다!!!!!

이제 우리가 하려고 하는 API 호출을 통해 원하는 데이터를 받아와 보자 ( 후.. 드디어!! )

생성한 URL을 통하여 URLSession 을 통해 데이터를 받아와보자

이전 내용 반드시 보고오기!

URLSession 의 개념

URL 생성 하는 방법

API

  • 실습을 하기에 앞서 실습하고 싶은 API를 구해야 한다
  • 본 글에서는 TMDB 를 사용하여 영화를 검색한 결과를 불러온다
  • 사용하고자 하는 API의 호출 구조를 잘 파악하고, Query 내용도 파악해야 한다

URL 생성하기

let API_KEY = "발급받은 API Key 정보 입력"
var movieSearchURL = URLComponents(string: "https://api.themoviedb.org/3/search/movie?")

// 쿼리 아이템 정의
let apiQuery = URLQueryItem(name: "api_key", value: API_KEY)
let languageQuery = URLQueryItem(name: "language", value: "ko-KR")
let searchQuery = URLQueryItem(name: "query", value: "어벤져스")

// URLComponents 에 쿼리 아이템 추가
movieSearchURL?.queryItems?.append(apiQuery)
movieSearchURL?.queryItems?.append(languageQuery)
movieSearchURL?.queryItems?.append(searchQuery)
  • 해당 API의 BaseURL은 movieSearchURL 변수에 URLComponents로 저장한다
  • 쿼리는 3가지로 구분된다
    • apiQuery → API Key
    • languageQuery → 언어 설정 ( 한국어로 검색하기 위해 ko-KR 설정 )
    • searchQuery → 검색하고자 하는 검색어 입력 ( 예제에서는 어벤져스 )
  • QueryItem을 설정했으면 Components에 추가시켜 준다

URL 얻어오기

  • URL을 생성했으면 URL 정보를 얻어와야 하는데 Optional로 정의되어 있기 때문에 옵셔널 바인딩을 이용해 그 값을 가져오도록 한다
guard let requestMovieSearchURL = movieSearchURL?.url else { throw NSError() }
  • 일반적으로 else { return } 의 형태지만, 예제에서는 함수내의 구현이 아니기 때문에 Error정보를 던져주기 위하여 throw NSError() 를 사용했다
  • URL 정보가 nil 이 아니라면 requestMovieSearchURL 상수에 URL 정보가 저장된다

URLSession Configuration 설정

// configuration 설정 -> default
let config = URLSessionConfiguration.default

// session 설정
let session = URLSession(configuration: config)
  • URLSession Configuration을 설정하기 위해 config 상수에 default 로 설정
  • session 상수에 해당 configuration을 적용한 URLSession 을 할당한다

URLSessionTask 설정

  • 해당 예제에서는 Task 목록중, DataTask를 적용
// dataTask 설정
let dataTask = session.dataTask(with: requestMovieSearchURL) { (data, response, error) in
    guard error == nil else { return }
    
    guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
    let successRange = 200..<300
    guard successRange.contains(statusCode) else { return }
    
    guard let resultData = data else { return }
    let resultString = String(data: resultData, encoding: .utf8)
    print("resultData -->\(resultData)")
    print("resultString -->\(resultString)")
}

dataTask.resume()
  • dataTask 상수에 session.dataTask를 적용
  • dataTask에는 일반적으로 data 정보, response 정보, error정보 가 들어온다
guard error == nil else { return }
  • error 가 nil 일경우 즉, error 상황이 아닐경우에만 다음 코드를 실행하고 error일경우 return으로 종료
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
    let successRange = 200..<300
    guard successRange.contains(statusCode) else { return }
  • HTTP StateCode 중, 200번대의 코드가 정상적인 상황이기 때문에 200번대의 상황일때만 다음 코드 진행
  • response 를 HTTPURLResponse 로 다운캐스팅(as?) 하여 statusCode로 상태코드를 확인할 수 있다
  • response의 상태코드가 200번대 일때 다음 코드를 진행하고 그렇지 않으면 return으로 종료

→ 해당 코드는 간단한 예시이지만 각 상태코드 마다 적절한 View를 보여주면 좋을것 같다

guard let resultData = data else { return }
    let resultString = String(data: resultData, encoding: .utf8)

    print("resultData -->\(resultData)")
		// resultData -->8482 bytes
 
    print("resultString -->\(resultString)")
		// 실행 결과가 너무 길기 때문에 직접 확인해보는걸 추천!
  • data가 nil인지 아닌지 확인하기 위해 바인딩을 통하여 nil 이 아닌경우 다음 코드를 진행
  • resultData 상수에는 data에 대한 길이정보가 출력
  • resultString 상수에는 해당 데이터를 utf8로 인코딩하여 그 내용을 보여준다
dataTask.resume()
  • 위에서 dataTask를 정의했어도 즉각 실행되진 않는다 resume() 메서드를 사용하면 통신을 시작한다

전체코드

let API_KEY = "발급받은 API Key 정보 입력"
var movieSearchURL = URLComponents(string: "https://api.themoviedb.org/3/search/movie?")

// 쿼리 아이템 정의
let apiQuery = URLQueryItem(name: "api_key", value: API_KEY)
let languageQuery = URLQueryItem(name: "language", value: "ko-KR")
let searchQuery = URLQueryItem(name: "query", value: "어벤져스")

// URLComponents 에 쿼리 아이템 추가
movieSearchURL?.queryItems?.append(apiQuery)
movieSearchURL?.queryItems?.append(languageQuery)
movieSearchURL?.queryItems?.append(searchQuery)

// 옵셔널 바인딩
guard let requestMovieSearchURL = movieSearchURL?.url else { throw NSError() }

// configuration 설정 -> default
let config = URLSessionConfiguration.default

// session 설정
let session = URLSession(configuration: config)

// dataTask 설정
let dataTask = session.dataTask(with: requestMovieSearchURL) { (data, response, error) in
    guard error == nil else { return }
    
    guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
    let successRange = 200..<300
    guard successRange.contains(statusCode) else { return }
    
    guard let resultData = data else { return }
    let resultString = String(data: resultData, encoding: .utf8)
    print("resultData -->\(resultData)")
    print("resultString -->\(resultString)")
}

dataTask.resume()

URL 생성

let urlString = "https://ai-hong.tistory.com/213?category=911246"
let url = URL(string: urlString)

// URL의 String 값 "https://ai-hong.tistory.com/213?category=911246"
url?.absoluteString

// URL의 네트워킹 방식 "https"
url?.scheme

// 기반이 되는 주소  "ai-hong.tistory.com"
url?.host

// URL의 디렉토리  "/213"
url?.path

// URL의 쿼리값  "category=911246"
url?.query

// 기반이 되는 URL  nil
url?.baseURL
  • 다음과 같은 프로퍼티들을 이용하여 URL의 정보에 대해 파악할 수 있다.
  • BaseURL 의 경우 별도로 설정하지 않으면 nil 로 뜨기 때문에 BaseURL에 접근 하기 위해서는 relativeTo 프로퍼티를 별도로 baseURL을 지정해주어야 한다
// BaseURL을 설정하고 접근하기 위한 방법
let baseURL = URL(string: "https://ai-hong.tistory.com")
let relationURL = URL(string: "213?category=911246", relativeTo: baseURL)

relationURL?.absoluteString
relationURL?.scheme
relationURL?.host
relationURL?.path
relationURL?.query
relationURL?.baseURL "Optional(https://ai-hong.tistory.com)"
  • BaseURL이 Optional이기 때문에 적절한 바인딩이 필요함

URLComponents

  • URL 만 사용해서 구성하게 되면 쿼리 내용중 혹은 URL 내용중 한글이나 특수문자 등과 같은 것이 들어가면 서버쪽에서 Error를 내뿜게 된다, 이러한 상황을 방지하고자 Components를 사용할 수 있다
let baseURL = URL(string: "https://ai-hong.tistory.com")
let relationURL = URL(string: "213?category=한글", relativeTo: baseURL)

// 통신이 제대로 작동하지 않기 때문에 nil 출력
relationURL?.absoluteString
relationURL?.scheme
relationURL?.host
relationURL?.path
relationURL?.query
relationURL?.baseURL

→ URL 의 쿼리에 한글이 포함되어 정상적인 통신을 못하고 nil을 출력

  • URLComponents 를 생성한후, URLQueryItem을 설정해 추가하여 인코딩이 되어 통신 정상적으로 수행
// URLComponents는 Query 직전까지의 주소를 할당
var urlComponents = URLComponents(string: "https://ai-hong.tistory.com/213?")
// 추가되는 쿼리들은 다음과 같이 변수로 할당
let categoryQuery = URLQueryItem(name: "category", value: "한글")

// 추가한 쿼리 Item을 urlComponents에 추가
urlComponents?.queryItems?.append(categoryQuery)

// "category=%ED%95%9C%EA%B8%80" 출력 -> 한글이 통신 가능한 상태로 인코딩된다
urlComponents?.url?.query

// "https://ai-hong.tistory.com/213?category=%ED%95%9C%EA%B8%80" 출력
urlComponents?.url?.absoluteString

 

URLSession 에 대한 개념 파악하기

API를 호출하기 위한 방법을 찾던 중, Alamofire 라이브러리가 있지만 라이브러리 말고 스스로 적용해서 불러오는 방법에 익숙해진 후 라이브러리를 사용하는게 좋을거 같다고 판단하여 관련 내용을 정리

URLSession

  • URLSession 은 위 사진과 같은 구조를 갖는다
    • URLSessionConfiguration 을 통해 URLSession을 결정한후, Session을 생성
    • Delegate는 URLSession 통신의 중간 과정이나 통신 종료 후 같은 이벤트를 확인할 때 주로 사용
    • URLSessionConfiguration을 통해 Session 을 설정하면 URLSessionTask 로 보내 통신을 시작

URLSessionConfiguration

  • URLSessionConfiguration의 종류에 대해 알아보자
    • Default : 기본적인 세션으로 쿠키나 캐시를 저장할때 사용 ( 가장 기본적인 통신 )
    • Ephemeral : 쿠키나 캐시를 저장하지 않는 통신을 할때 사용 ( Private 모드 )
    • Background : 앱이 Background 상태일 때 사용하는 통신
  • Configuration 을 적용하지 않고 shared 를 사용할 수 있다
    • shared를 사용하면 싱글톤 객체를 제공하는데 편하게 사용할 수 있는 장점이 있지만, delegate도 없고, configuration 도 없기 때문에 일부 제한이 있음 이러한 사항을 고려하지 않을때는 간편하게 사용 가능

URLSessionTask

  • Session 작업 하나를 나타내는 추상 클래스
  • Task의 종류
    • DataTask : 서버로부터 응답 데이터를 받아 Data객체를 가져오는 작업 수행
    • UploadTask : 서버에 Data 객체나 File 데이터를 올리는(Upload) 작업 수행
    • DownloadTask : 서버로부터 Data를 다운받아 File 형태로 저장하는 작업 수행 ( Background 가능 )

+ Recent posts