생성한 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 가능 )

Concurrent (동시성) 이란 무엇인가?

  • 앱의 로직 내에서 특정 부분이 동시에 또는 임의 순서대로 실행되기 위해 필요한 개념
  • 앱의 전반적인 효율성 측면에서도 필요하지만, 앱을 사용하는 유저에게도 일관적인 UI를 그리기 위해서도 필요한 개념
  • ⇒ 네트워크 통신을 통해 어떠한 리소스를 다운받는 동안 동시성을 적용하지 않아 유저가 비어있는 화면만 바라보게 된다면 지루함을 느끼고, 짜증을 유발할 수 있다, 이때 동시성을 적용하여 네트워킹을 통해 리소스를 다운받는 동안 유저에게 특정 뷰를 그려주면 유저 입장에서도 매우 좋다
  • 데이터 흐름을 정확히 유지하는 것이 중요하기 때문에 동시에 이루어지는 두 작업이 하나의 데이터를 조작하면 안됨

GCD (Grand Central Dispatch)

  • DispatchQueue 에 등록된 작업들을 사용 가능한 스레드에 스케쥴링 API
  • Serial DispatchQueue 와 Concurrent DispatchQueue 로 나뉨
  • Serial queue ⇒ queue의 작업을 오직 하나의 스레드에서만 수행
  • Concurrent queue ⇒ 여러 스레드의 작업을 동시에 수행

동기와 비동기

  • Sync 와 Async 로 동기 및 비동기를 설정할 수 있다.
  • 동기적으로 설정된 작업은 해당 작업이 반환(종료)되기 전까지 다른 작업을 수행하지 않는다
  • 비동기적으로 설정된 작업은 해당 작업이 수행되는 동안 다른 작업을 동시에 수행하며, 작업이 종료되는 즉시 반환한다
  • 의문점.. → Serial queue / Sync 와 Concurrent queue / Async 의 차이가 뭐지?...

 https://cskime.tistory.com/18

Seiral/Concurrent는 요청된 여러 개의 작업들을 순차적으로 또는 동시에 처리 하는 경우
Sync/Async는 어떤 작업에 대한 요청과 응답이 한 번에 또는 따로 발생 하는 경우
Serial/Concurrent는 '다수의 대상에 대한 '순서'가 주된 개념
Sync/Async는 단일 작업에 대한 요청과 응답(결과)의 발생 시점이 주된 개념

 

DispatchQueue 의 종류

Main Queue

  • 앱의 시작과 동시에 생성되며 Serial Queue로 존재한다 메인큐는 반드시 UI를 위해서만 사용해야 하며 언제나 비동기적으로 작업을 수행한다
  • DispatchQueue.main.async { task }

Global Queue

  • Concurrent Queue 로 존재
  • QoS 파라미터를 가지며, 해당 파라미터에 따라 중요 순서도가 결정됨
    • QoS 종류
      • userInteractive : 사용자화 직접 상호작용 하는것들 ( UI 업데이트 , 애니메이션 등등 )
      • userInitated : 이벤트 발생 시 즉각적인 결과가 필요한 작업
      • Default : 일반적인 작업 ( global() 은 QoS 파라미터를 생략할수 있기 때문에 굳이 디폴트를 사용하진 않음 )
      • utility : progress bar 와 같이 길~게 싱행되는 작업들 ( 데이터 다운로드 등등 )
      • background : 유저가 인지할 필요가 없는 시간과 관계없는 작업들 ( 동기화 , 백업 등,, 악용하면 이걸로 정보탈취..? )
      • unspecified : 거의 사용되지 않는 파라미터로, QoS 정보가 없을때 사용
  • DispatchQueue.global(qos: 1~6의 qos 또는 생략 가능).sync/async { task }

Custom Queue

  1. 커스텀으로 큐를 구성할 때 사용
  2. 기본적으로 Serial 특성을 갖긴 하지만, 설정을 통해 Concurrent 로 사용 가능
// serial 커스텀 큐 ( 기본값 )
let customSerialQueue = DispatchQueue(label: "customSerial")

// concurrent 커스텀 큐 ( 애트리뷰트 설정을 해야함 ) , qos 알아서 추론
let customConcurrentQueue = DispatchQueue(label: "customConcurrent", attributes: .concurrent)

// concurrent 커스텀 큐에 qos 설정
let customConcurrentQueue = DispatchQueue(label: "customConcurrent", qos: .background, attributes: .concurrent)

 

참고 링크

https://hyunsikwon.github.io/ios/iOS-Concurrency-01/#gcd

https://engineering.linecorp.com/ko/blog/about-swift-concurrency/

https://sujinnaljin.medium.com/ios-차근차근-시작하는-gcd-5-c8e6eee3327b

패스트 캠퍼스 All in one_iOS 강의

DTO 와 VO 의 차이

API를 호출하고자 하는데 데이터 정의를 어떻게 해야 할지 고민이 되어 찾아보다가 관련 개념을 정리

우선, DTO 와 VO 둘다, 데이터를 정의하는데 의미가 있다 그렇다면 차이가 뭘까?

간단히 설명하면 getter 와 setter를 모두 포함하는지, getter만 포함하는지에 대한 차이가 있다.

즉, 읽기 전용 데이터인지? 적절한 수정을 통해 가공해야 하는 데이터인지에 대한 차이이다.

DTO : Data Transfer Object

⇒ getter 와 setter를 모두 가지고 있는 객체를 의미

Swift의 문법중 ‘연산 프로퍼티' 의 개념과 동일하다, 데이터를 가져와 적절히 가공하고 사용해야 할때 사용되는 개념

  • 데이터 자체를 사용하는것이 아닌, 가공 또는 한곳에 모아 사용해야 할때 사용
  • 데이터를 어딘가에 ( 외부로 ) 전송해야 할때 유용한 객체
class Greeting {
    private var korean = "안녕하세요"
    private var english = "Hello"
    
    func getKorean() -> String {
        return korean
    }
    
    func setKorean(_ korean: String) {
        self.korean = korean
    }
    
    func getEnglish() -> String {
        return english
    }
    
    func setEnglish(_ english: String) {
        self.english = english
    }
}

VO : Value Object

  • 데이터 그 자체로 의미 있는 값을 가지는것
  • DTO와 달리, getter만 가질수 있으므로 읽기 전용 객체가 된다
open class UIColor : NSObject, NSSecureCoding, NSCopying {

    //...
    open class var black: UIColor { get } // 0.0 white
    open class var darkGray: UIColor { get } // 0.333 white
    open class var lightGray: UIColor { get } // 0.667 white
    open class var white: UIColor { get } // 1.0 white
    open class var gray: UIColor { get } // 0.5 white
    open class var red: UIColor { get } // 1.0, 0.0, 0.0 RGB
    open class var green: UIColor { get } // 0.0, 1.0, 0.0 RGB
    open class var blue: UIColor { get } // 0.0, 0.0, 1.0 RGB
    open class var cyan: UIColor { get } // 0.0, 1.0, 1.0 RGB
    open class var yellow: UIColor { get } // 1.0, 1.0, 0.0 RGB
    open class var magenta: UIColor { get } // 1.0, 0.0, 1.0 RGB
    open class var orange: UIColor { get } // 1.0, 0.5, 0.0 RGB
    open class var purple: UIColor { get } // 0.5, 0.0, 0.5 RGB
    open class var brown: UIColor { get } // 0.6, 0.4, 0.2 RGB
    open class var clear: UIColor { get } // 0.0 white, 0.0 alpha
    //...
}
  • 대표적으로 UIColor 가 VO 이다.
  • color 를 사용할때 .green 을 사용하면, green 색상을 바로 읽어 올수 있다.

참고 : 링크

+ Recent posts