Swift로 API Request를 전송하기

NB·2021년 1월 31일
12

Swift

목록 보기
4/4
post-thumbnail

서론

서비스를 개발하는 것에 대해서 API를 호출하고, 데이터를 받는 것은 매우 중요하다. API를 사용하지 않는 Application은 한정된 정보만 가지고 있을 뿐이다. 이번 글에서는 Swift를 사용해서 HTTP 통신을 통해 서버와 통신하는 방법에 대해서 알아보겠다.

실습을 시작하기 전에 우리는 이 HTTP(Hypertext Transfer Protocol)에 대해서 짚고 넘어가야 할 필요가 있다.

이번 글을 쉽게 이해하기 위해서는 HTTP 통신에 대해서 어느 정도 알고 있으면 좋습니다. 🙂

기본적으로 서버와 통신하기 위해서 URLSessionURLRequest을 이용한 요청방식과 Alamofire 라이브러리를 이용한 요청방식, 2가지 방법을 알아보겠다.

초기 설정

기본적으로 iOS 개발을 진행하다 보면 SSL 인증이 되지 않는 사이트에 관해서 기본적으로 막고 있다는 것을 알게 된다. 그렇다고, 처음 개발을 시작할 때부터 Https로 구현할 필요는 없다. Info.plist 에 들어가서 다음과 같이 설정을 진행해주자.

Information Property List > App Transport Security Settings > Exception Domains > localhost > NSTemporaryExceptionAllowsInsecureHTTPLoadsYES로 변경해주자.

info.plist 설정

물론 이 설정은 현재 Server를 localhost에서 구축할 것이기 때문이고, 만약 API Server가 따로 구현되어 있다면 해당 Server를 추가해주자. 그리고 제일 중요한 것은 잊지 말고 Production으로 배포할 때에는 NO로 설정을 한 뒤, 배포해주자.



URLSession을 사용해보자!

URLSession이란, 기본적으로 제공되는 API로서 HTTP를 포함한 여러 가지 프로토콜을 지원하고, 인증, 쿠키, 캐시 등의 관리를 지원한다. 만약 URLSession에 대해서 자세한 정보가 필요하다면 다음 글을 읽어보자.

iOS URLSession 이해하기 - Eth Dev Post

Swift, URLSession가 무엇인지, 어떻게 사용하는지 알아봅니다. - 까칠코더(Minjun Ju 번역)

또한, URLRequest는 요청에 대한 정보를 표현하는 객체이며 , 우리는 이 객체를 URLSession을 사용하여, 서버로 요청을 보내게 될 것이다.

# 전체 구조

먼저 효율적으로 request를 사용하기 위해서, request.swift 라는 파일을 생성시켜주자. 우리는 이 파일에서 메소드별 동작을 구현하게 될 것이다. 먼저 전체 구조를 확인하고 세부적으로 진행해보자.

import UIKit

struct Response: Codable {
    let success: Bool
    let result: String
    let message: String
}

/* Body가 없는 요청 */
func requestGet(url: String, completionHandler: @escaping (Bool, Any) -> Void) {
    
    ...
    
}

/* Body가 있는 요청 */
func requestPost(url: String, method: String, param: [String: Any], completionHandler: @escaping (Bool, Any) -> Void) {
    
    ...
    
}

/* 메소드별 동작 분리 */
func request(_ url: String, _ method: String, _ param: [String: Any]? = nil, completionHandler: @escaping (Bool, Any) -> Void) {
    if method == "GET" {
        requestGet(url: url) { (success, data) in
            completionHandler(success, data)
        }
    }
    else {
        requestPost(url: url, method: method, param: param!) { (success, data) in
            completionHandler(success, data)
        }
    }
}

우리는 다른 View 파일에서 가장 아래에 있는 request 함수를 요청하게 될 것이다. 그리고 서버로부터 받은 데이터를 Escaping Closure를 통해서 View 파일에서 사용할 수 있게 할 것이다. 위 구조에서 request 함수를 사용하는 방식은 다음과 같다.

request("http://localhost:5000/test/get", "GET") { (success, data) in
  print(data)
}

// or

request("http://localhost:5000/test/post", "POST", ["key": "hello!"]) { (success, data) in
  print(data)
}

첫 번째 인자로는 URL을, 두 번째 인자는 METHOD를, 세 번째 인자로는 Optional로 Body에 들어갈 Dictionary 타입의 데이터를 넣어준다.

# GET 메소드를 사용할 때

흐름을 파악했다면 이제 세부 코드를 통해서 작동하는 방식을 이해해보자. 먼저 requestGET 함수를 살펴보자.

func requestGet(url: String, completionHandler: @escaping (Bool, Any) -> Void) {
    guard let url = URL(string: url) else {
        print("Error: cannot create URL")
        return
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        guard error == nil else {
            print("Error: error calling GET")
            print(error!)
            return
        }
        guard let data = data else {
            print("Error: Did not receive data")
            return
        }
        guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else {
            print("Error: HTTP request failed")
            return
        }
        guard let output = try? JSONDecoder().decode(Response.self, from: data) else {
            print("Error: JSON Data Parsing failed")
            return
        }
        
        completionHandler(true, output.result)
    }.resume()
}

먼저 우리는 주로 guard 문법을 사용할 것이다. 해당 문법에 대해서 더 알고싶다면 다음을 참고하도록 하자.

[Swift]guard 문 - Robin Kang

위 코드에서 순서는 다음과 같이 진행된다.

  1. URL 객체 생성
  2. Request 객체 생성 (+ 메소드 설정)
  3. URLSession을 이용해서 데이터 요청
  4. @escaping Closure을 이용한 외부 함수로 인자 전달

여기서 주목해야 할 점은 제일 마지막 부분에서 JSONDecoder()를 실행시키는 부분이다. Swift4부터 등장한 Codable를 통해서 우리는 JSON 객체를 특징 Dictionary 타입으로 만들 수 있게 되었는데, 코드를 보게 되면 상단에 이미 Codable을 선언했던 것을 확인할 수 있다.

struct Response: Codable {
    let success: Bool
    let result: String
    let message: String
}

Backend code
위처럼 서버에서 항상 Response와 같은 구조를 반환해준다는 가정하에 위와 같이 선언해준다. 그리고 JSONDecoder()를 통해서 일반 JSON에서 데이터를 파싱해준다.

# 그 외의 메소드를 사용할 때

RESTful API를 사용한다는 가정하에 GET 메소드를 제외하고 다른 메소드들은 body를 포함할 수 있다. 그렇기에 body를 넣어주는 코드만 추가하고 나머지는 동일하게 작성해주자.

func requestPost(url: String, method: String, param: [String: Any], completionHandler: @escaping (Bool, Any) -> Void) {
    let sendData = try! JSONSerialization.data(withJSONObject: param, options: [])
    
    guard let url = URL(string: url) else {
        print("Error: cannot create URL")
        return
    }
    
    var request = URLRequest(url: url)
    request.httpMethod = method
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = sendData
    
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        guard error == nil else {
            print("Error: error calling GET")
            print(error!)
            return
        }
        guard let data = data else {
            print("Error: Did not receive data")
            return
        }
        guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else {
            print("Error: HTTP request failed")
            return
        }
        guard let output = try? JSONDecoder().decode(Response.self, from: data) else {
            print("Error: JSON Data Parsing failed")
            return
        }
        
        completionHandler(true, output.result)
    }.resume()
}

서버에서 body 값을 JSON으로 받는다는 가정하에 Dictionary 타입으로 들어온 param 인자를 JSON으로 바꿔준다. 그리고서 Content-Typeapplication/json으로 바꿔주고, body에 데이터를 추가해준다. 나머지 과정은 GET을 사용할 때와 동일하다.



Alamofire 라이브러리를 사용해보자!

이제 두 번째 방법은 이미 나와 있는 라이브러리를 사용하는 것이다. 먼저 Alamofire Github에 들어가서 위 라이브러리를 프로젝트에 추가해주자. 라이브러리를 추가하는 방법은 이전 글에서도 설명한 여기를 통해서 알 수 있다. 설정이 끝났다면, API를 호출하고 싶은 파일에서 다음과 같이 작성해주자.

import Alamofire

그리고, 다음 코드는 API를 호출하는 코드이다.

AF.request("http://localhost:5000/test/get").responseJSON() { response in
  switch response.result {
  case .success:
    if let data = try! response.result.get() as? [String: Any] {
      print(data)
    }
  case .failure(let error):
    print("Error: \(error)")
    return
  }
}

역시 사람들이 많이 사용하는 라이브러리답게 한 눈에 봐도 직관적으로 구현되었다. 받을 데이터는 JSON 형태이므로 responseJSON()을 사용해준다. 위 방식은 GET 메소드를 호출할 때의 방식이고, 다음은 POST 메소드를 호출할 때의 방식이다.

AF.request("http://localhost:5000/test/post", method: .post, parameters: ["key": "hello!"], encoding: URLEncoding.httpBody).responseJSON() { response in
  switch response.result {
  case .success:
    if let data = try! response.result.get() as? [String: Any] {
      print(data)
    }
  case .failure(let error):
    print("Error: \(error)")
    return
  }
}

GET 때와는 다르게 methodparameters 라는 인자를 입력한다.



코드실행 결과

코드실행 결과는 다음과 같다.

Swift 전체 코드 확인은 여기에서 확인할 수 있습니다.



💡 중요

  • ATS(App Transport Security) 설정 잊지않기

  • URLSessionURLRequest의 이해

  • @escaping Closure에 대한 이해

  • Codalbe 프로토콜에 대한 이해

  • guard 문법에 대해서도 알아두기!
profile
𝙄 𝙖𝙢 𝙖 𝙛𝙧𝙤𝙣𝙩𝙚𝙣𝙙 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙚𝙧 𝙬𝙝𝙤 𝙚𝙣𝙟𝙤𝙮𝙨 𝙙𝙚𝙫𝙚𝙡𝙤𝙥𝙢𝙚𝙣𝙩. 👋 💻

0개의 댓글