URL Loading System

백상휘·2020년 9월 2일
0

iOS_Programming

목록 보기
1/10
post-thumbnail

과연 요즘 프로그램들 중에 네트워크와 아무 관련이 없는 프로그램을 찾아볼 수 있을까? 없더라도 나중에는 필요할 것이다.

필자는 iOS 프로그래밍을 하기 전에 웹 프로그래머로서 활동했다. 이미 만들어진 솔루션을 커스터마이징 하거나, 이미 솔루션이 보급된 고객사에서 접수되는 오류를 해결해주는 것이 주된 업무였다.

물론 여러가지 문제들이 다양하게 발생했지만 그 중 까다로운 문제는 프로그램이 고객사 자체 서버, 혹은 고객사가 계약한 IDC 업체에 설치된 상태에서 외부 시스템과의 연결이 안되는 경우였다. 어쩔 수 없이 커넥션 오류가 나는 부분을 계속 어필하는 수 밖에 없고 고객사는 화가 난다.


(대강 이런 구조였다. 지금 와서 그려보니 감회가 새롭다.)

iOS 애플리케이션을 만드는 경우도 마찬가지라고 본다. 저 그림의 웹 브라우저만 아이폰으로 바꾸면 되는 것 아닐까?

그러므로 Apple Documentation의 'URL Loading System'을 정독하고 어떤 방식으로 사용할 수 있는지 토이 프로젝트를 만들어서 공부하고 이를 공유하고자 한다.

https://developer.apple.com/documentation/foundation/url_loading_system

URL Loading System 에 대해서 공식문서는 다음과 같이 설명하고 있다.

'인터넷 프로토콜을 통해 URL으로 상호작용하고 서버와 상호작용한다.'

즉, URL을 이용하여 특정 시스템에 엑세스 하고 결과 값을 받아오는 것이 위에 공유한 공식문서의 주된 내용이다.

URL Loading System을 사용하기 위해서는 URLSession 객체가 필요하다. 이 객체로 여러 개의 URLSessionTask를 만들어서 데이터를 내보내고, 데이터를 받고, 파일을 다운로드하고, 파일을 업로드할 수 있다. 세션이나 쿠키 등의 세션 설정은 URLSessionConfiguration 객체로 가능하다.

URL Loading System의 특징은 다음과 같다.

  • URL 요청 후 발생하는 URLSessionTask 작업은 비동기적으로 수행한다. 애플리케이션은 반환된 데이터와 에러를 다룰 수 있는 상태를 유지하고 있다.
  • 다수의 요청 작업에 하나의 URLSession을 사용하는 것이 가능하다.
  • Custom URL Session 을 만들 수 있습니다. 하지만, 이를 위해서는 URLSession, URLSessionConfiguration, URLSessionDataDelegate를 염두에 두셔야 합니다.
  • Background Task 를 지원합니다.

https://developer.apple.com/documentation/foundation/url_loading_system/fetching_website_data_into_memory

본격적으로 URL session 을 만들어서 외부와 통신해보도록 하겠습니다.

데이터 요청(Fetch & Request)

URLSession은 요청하고 반환하는 데이터에 따라 두 개의 Task Class를 만듭니다.

  • URLSessionDataTask : 메모리에 데이터를 저장하거나 메모리의 데이터를 전송할 경우
  • URLSessionDownloadTask : file system에 데이터를 저장해야할 경우

상대적으로 요청만 보내는 간단한 상황에는 URLSession 클래스의 shared 객체를 이용한 URL session 생성만으로도 충분합니다.

session이 생성되었다면, dataTask() method를 통해 외부와의 통신을 수행하고, 작업을 정의할 수 있습니다. dataTask는 정지 상태에서 만들어지고, resume() 메소드를 통해 시작할 수 있습니다.

// 간단한 요청만을 필요로 할 경우 세션을 따로 정의한 상태에서 작업.
let url = URL(string: "내가 원하는 URL")
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { data, response, error in
	.......
}

// 위의 예시보단 아래와 같이 사용하는 경우가 더 많음.
// 세션 갯수 주의.
let task = URLSession.shared.dataTask(with: url) { data, response, error in
	
}


*주의 :
dataTask() 의 completion handler에서
UI를 업데이트 하거나 새로운 창을 여는 코드가 있다면
main queue에서 작업할 수 있도록 해야 합니다. completion handler의 작업은 다른 queue 에서 수행되기 때문입니다.

Apple에서 권장하는 dataTask()의 completion handler 사용방법은 다음과 같습니다.

  • error 파라미터가 nil인지 확인하고, nil이 아닐 경우 이를 핸들링 한다.
  • response 파라미터의 상태코드와 MIME타입(파일 타입)이 예상한 대로인지 확인한다. 예상과 다를 경우는 에러를 처리한다.
  • data 객체를 이용하여 작업을 수행한다.

데이터 요청 심화(URLSession Custom Delegate)

completion handler를 사용하지 않고 직접 dataTask()를 만들 수도 있습니다.

이를 위해서는 URLSessionDataDelegate에서 제공하는 urlSession(_:dataTask:didReceive:) 메소드를 이용합니다. 이 메소드는 전송이 종료되거나 에러가 발생할 때까지 동작합니다.

위의 그림에서 나온대로 우리는 직접 만들어야 할 것이 몇 개 있습니다.

  • Custom URLSession
  • task 이후 각각의 상황에 맞는 delegate 함수(urlSession()).

아래는, Custom URLSession의 한 예시입니다. URLSession을 설정할 수 있는 URLSessionConfiguration의 waitsForConnectivity를 true로 지정해서 session의 연결이 성립될 때까지 계속 연결을 시도하도록 하는 것입니다.

이를 이용해서 커스텀 URLSession 클래스를 만듭니다. 이후 토이 프로젝트에는 다음과 같이 사용할 예정입니다.
(대부분 애플 공식문서에서 얻어 온 코드입니다.)
(토이 프로젝트 시작 전이므로 해당 코드는 다음 포스팅에서 수정하여 올리도록 하겠습니다.)
(segue Identifier 같은 값은 오타의 가능성이 있어서 Constants.swift 파일을 만들어 관리합니다.)

AppDelegate.swift

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 공통 유틸로 사용하기 위한 공통 변수
    private(set) var myURLSession: URLSession?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionKey: Any]?) -> Bool {
    	self.myURLSession = MyURLSession() // 커스텀 URLSession 객체 생성
    }
    -이하 중략-
}

MyURLSession.swift

import Foundation

// Custom Delegate
protocol MyURLSessionDelegate {
    var data: Data? {get}
    var result: String? {get set}
}

class MyURLSession: URLSession, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
    var receivedData: Data?
    var qualifiedURL: String?
    var customdelegate: MyURLSessionDelegate? // URLSessionDelegate의 delegate와 겹침.

    private lazy var session: URLSession = {
        let conf = URLSessionConfiguration.default
        conf.waitsForConnectivity = self.willWaitForSignal
        return URLSession(configuration: conf,
                delegate: self, delegateQueue: nil)
    }()

    private var willWaitForSignal: Bool = true {
        willSet {
            self.session.configuration.waitsForConnectivity = newValue
        }
    }

    // 통신 시작.
    func startLoad() {
        if let qualifiedURL = qualifiedURL, let url = URL(string: qualifiedURL) {
            let task = session.dataTask(with: url)
            receivedData = Data()
            task.resume()
        }
    }
    
//MARK: - urlSession Delegate 메소드.
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (ResponseDisposition) -> ()) {
        guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode), let mimeType = response.mimeType, mimeType == "text/html" else {
            completionHandler(.cancel)
            return
        }

        completionHandler(.allow)
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        self.receivedData?.append(contentsOf: data)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        DispatchQueue.main.async {
            if let error = error {
                fatalError()
            } else if let receivedData = self.receivedData, let string = String(data: receivedData, encoding: .utf8) {
                // customdelegate에 반영된 result에 결과 값을 반영.
                self.customdelegate?.result = string
                print("Success URLSession Request!!! \(string)")
            }
        }
    }
}

MainViewController.swift

import Foundation
import UIKit

class MainViewController: UIViewController, MyURLSessionDelegate {

    @IBOutlet var mainTableView: UITableView!
    var myURLSession: MyURLSession?
    private(set) var categoryList: [String]

    var data: Data?
    var result: String?

    init() {
        categoryList = Constants.categories
        // Custom URLSession 통신 시작 및 뷰컨트롤러의 변수 초기화.
        myURLSession = (UIApplication.shared.delegate as? AppDelegate)?.myURLSession
        myURLSession?.customdelegate = self
        myURLSession?.qualifiedURL = Constants.kosbiRESTUrl
        myURLSession?.startLoad()
    }
    
    override func viewDidLoad() {
        mainTableView.delegate = self
        // 임시 소스
        if let result = result {
            self.navigationItem.title = result
        }
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        super.prepare(for: segue, sender: sender)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

//MARK: - Table View delegate
extension MainViewController: UITableViewDelegate {
    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath)
        cell?.isSelected = true
        performSegue(withIdentifier: Constants.detailViewSegueIdentifier, sender: self)
    }
}

//MARK: - Table View data source
extension MainViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return categoryList.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath)
        cell.textLabel?.text = categoryList[indexPath.row]
        return cell
    }
    
//    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
//
//    }
}

토이 프로젝트를 만들면서 일어나는 일과 수정할 부분들을 찾아서 다시 찾아뵙겠습니다. 감사합니다.

출처 : https://developer.apple.com

.
.
.
.
.
.
.
후기
이번에 JetBrains 의 AppCode를 처음 써봤는데 XCode 쓰면서 생긴 암세포가 사라졌어요! 근데 storyboard 파일 열 수가 없음 ㅡㅡ.....

이렇게 된 이상 투배럭식 프로그래밍으로 간다.....

profile
plug-compatible programming unit

0개의 댓글