이 글에서 API URL은 전부 생략하였습니다.
라인, 헤더, 바디로 구성.
라인은 http 메시지의 최상단 한줄. 그 다음줄 부터는 헤더이며, 헤더가 끝나고 한줄 공백이 있고 그뒤에 나오는게 바디이다.
POST / userAccount/login HTTP/1.1 // 라인
Host: api.swift.co.kr:2029 // 헤더
Content-Type: application/x-www-form-urlencoded // 헤더
account=swift%40swift.com&passwd=1234&grant_type=password // 바디
Host 헤더에는 도메인과 포트번호가 적혀있다. 두개 이상의 도메인에 연결되어 있는 서버일 경우 어느 도메인으로 요청이 들어왔는지 구분하기 위함이다.
Content-Type은 메시지 본문이 어떤 형식으로 작성되어 있는지를 나타낸다. 메시지 본문(바디)에 적혀있는 자료는 Content-Type 과 일치해야 한다.
웹 콘텐츠나 데이터를 http 기반으로 간단히 주고 받기 위해 정의된 형식.
RESTful API 에서는 데이터 본문을 JSON 형식으로 구성하여 보낸다.
API 호출 형식은 표준적인게 없고, 기업 서비스마다 다르다.
현재 시간 API : 게임에서 디바이스의 시간 설정을 변경해 아이템 획득 쿨타임을 우회하려는 어뷰징 시도를 방지하는데 사용할 수 있다.
CurrentTime API 호출 형식
값을 요청할때 전달값(request, req)에는 아무것도 포함하지 않아도 된다.
API 를 호출할 Xcode 프로젝트를 하나 만든다.
프로젝트를 만든 뒤 info.plist 의 루트 항목인 Information Property 하위에 다음과 같이 항목을 추가한다.
-App Transport Security Settings
-Allow Arbitary Loads : YES
iOS 어플리케이션이 https 가 아닌 http 통신을 하기 위해선 Allow Arbitary Loads 설정이 YES로 되어 있어야 한다. (default는 NO이다)
(IP 기반의 주소를 이용하여 서버와 통신하면 ATS 설정이 필요없지만, 서버 증설이 어렵기 때문에 개발단계가 아닌이상 사용되지 않는다. 또한 애플은 IPv6를 지원하지 않는 메소드를 사용하거나 IPv4 기반의 서버와 통신하는 코드가 있을 경우 앱스토어 심사해서 리젝해버린다.)
SwiftUI 기반으로 현재시간을 호출하고 출력하는 버튼을 만들어 보자.
import SwiftUI
struct ContentView: View {
@State var currentTime : String = "what's the time?"
func callCurrentTime() {
do {
let url = URL(string: "현재시간 호출용 API URL")
let response = try String(contentsOf: url!)
self.currentTime = response
} catch let error as NSError {
print(error.localizedDescription)
}
}
var body: some View {
VStack{
Button(action: callCurrentTime) {
Text("GET current time")
}
Text(currentTime)
}
}
}
클라이언트가 보낸 요청을 그대로 응답하는 Echo API 호출하기. 개발 과정에서 서버와 클라이언트를 처음 연동할때 서버가 잘 응답하는지 확인하기 위하여 사용한다.
클라이언트의 요청내용을 JSON 형식으로 돌려주고, 거기에 더해 timestamp 와 처리결과는 result 항목을 추가하여 응답한다.
Echo API 요청 형식
GET 방식과 다르게 Content Type 이 있다. 서버에 값을 전송할때 URL 인코딩된 폼 타입을 사용하겠다는 의미. 보통 웹페이지의 form 에서 submit 눌렀을때 값이 전달되는 형식이 x-www-form-urlencoded 이다.
SwiftUI를 기반으로 한 코드는 다음과 같다.
import SwiftUI
struct ContentView: View {
@State var userId : String = ""
@State var userPassword: String = ""
@State var parsedResponse : String = ""
func post() {
// 1. 전송할 값 준비
let userId = self.userId
let userPassword = self.userPassword
let param = "userId=\(userId)&userPassword=\(userPassword)"
let paramData = param.data(using: .utf8)
// 2. URL 객체 정의
let url = URL(string: "서버 API 주소")
// 3. URLRequest 객체를 정의하고 요청 내용을 담는다.
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.httpBody = paramData
// 4. http 메시지의 헤더를 설정
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue(String(paramData!.count), forHTTPHeaderField: "Content-Length")
// 5. URLSession 객체를 통해 전송 및 응답값 처리
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// 서버가 응답 없거나 통신이 실패했을때
if let e = error {
NSLog("An error has occerred: \(e.localizedDescription)")
return
}
// 응답이 있을때 응답 처리 로직
// (1) 메인 스레드에서 비동기로 처리되도록 한다
DispatchQueue.main.async() {
do {
let object = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary
guard let jsonObject = object else { return }
// (2) JSON 결과값을 추출한다.
let result = jsonObject["result"] as? String
let timestamp = jsonObject["timestamp"] as? String
let userId = jsonObject["userId"] as? String
let userPassword = jsonObject["userPassword"] as? String
// (3) 결과가 성공일 때에만 텍스트 뷰에 출력한다.
if result == "SUCCESS" {
self.parsedResponse = "요청결과: \(result!)" + "\n"
+ "응답시간: \(timestamp!)" + "\n"
+ "요청방식: x-www-form-urlencoded" + "\n"
+ "유저 ID : \(userId!)" + "\n"
+ "유저 password: \(userPassword!)" + "\n"
}
} catch let e as NSError {
print("An error has occured while parsing JSON Obejt : \(e.localizedDescription)")
}
}
}
// 6. POST 전송
task.resume()
}
var body: some View {
VStack{
TextField("user id", text: $userId)
TextField("password", text: $userPassword)
Button(action: post) {
Text("Submit")
}
Text(parsedResponse)
}
}
}
Xcode 시뮬레이터를 통해 다음과 같이 Echo API 가 동작하는 것을 확인할 수 있다.
전송할 값 준비
값을 POST 방식으로 전송하려면 http 통신 표준에 정의된 약속에 따라, key1=value1&key2=value2&... 와 같은 방식으로 작성해 주어야 한다.
이 값을 그대로 보내면 공백이나 문장부호, 한글 같은 문자는 전송과정에서 변형되어 버릴 수 있으므로 Data 객체로 변경할때 .utf8 인코딩을 적용해줘야 한다.
URL 객체 정의
API를 호출하는 과정의 대부분은 URL 객체를 생성하는 것으로 시작한다.
URLRequest 객체를 정의하고 요청 내용을 담는다.
생성된 URL 객체를 이용해서 URLRequest 객체를 만든다. 이 객체는 API를 호출할 때 필요한 요청 내용이 모두 포함되며, http 요청 메시지 구조를 구성하는 역할을 한다.
http 메시지의 헤더를 설정한다.
헤더는 실제 내용에는 포함되지 않으면서 전송하는 콘텐츠에 대한 형식이나 특성 등 메타정보를 제공한다. request.addValue(: forHTTPHeaderField: ) 메소드와 request.setValue(: forHTTPHeaderField: ) 를 이용해 헤더를 추가할 수 있다. 추가할 수 있는 헤더의 수에는 제한이 없으므로, 필요한 만큼 위 메소드를 반복적으로 사용하면 된다.
addValue 는 헤더에 값을 추가하는 것이고, setValue 는 기존값을 replace 하는 것이다.
Content-Length 를 설정해준 이유는 다음과 같다.
서버에 전송되는 데이터는 문자열이 덩어리 단위로 끊어져서 전송되는 것이 아니라 스트림 형식으로 계속 이어져 전달되기 때문에, 서버는 http 메시지를 어디까지 끊어서 처리해야 할 지 결정할 수 있어야 한다. http 메시지 길이를 판별하기 위한 헤더.
Content-Length 헤더가 없어도 대부분의 서버는 http 메시지의 끝을 잘 판별하지만, 본문의 길이를 계산하여 넣어주는 것이 더 안전하다.
Data 객체 값의 길이는 count 속성을 통해 확인할 수 있고, 헤더에 추가할 때는 문자열 형식으로 변경해서 집어넣어야 한다.
URLSession 객체를 통해 전송 및 응답값 처리
URLRequest 객체를 전송하는데는 URLSession.shared.dataTask(with:) 구문이 사용된다. 인풋에 URLRequest 객체를 담고, 뒤에 이어질 트레일링 클로저에는 응답이 올때 처리될 로직이 들어간다.
응답값의 처리가 필요없는 요청이라면 트레일링 클로저를 생략하고 다음과 같이 간략히 적을 수 있다.
let task = URLSession.shared.dataTask(with: request)
http 통신은 비동기로 이루어지기 때문에, 응답값을 받아 처리할 내용을 클로저 형태로 미리 작성하여 인자값으로 넣어주어야 한다.
응답 클로저에는 응답 메시지 본문(Data 타입의 data), 응답 코드 및 메타정보가 저장된 응답정보(URLResponse 타입의 response), 오류정보(Error 타입의 error) 가 매개변수로 들어간다.
서버 통신에 문제가 생긴다면 세번째 매개변수에 Error 타입의 값이 대입된다. 따라서 응답 시 세번째 매개변수를 체크하면 통신이 제대로 수행되었는지를 확인할 수 있다.
Swift 에서 비동기로 실행되는 모든 구문은 서브 스레드에서 실행되고, UI 관련 구문은 메인 스레드에서 실행된다. 따라서 비동기로 수행되는 URLSession 핸들링은 서브스레드에서 실행된다.
그러나 서버 통신으로 얻은 결과물을 UI 에 반영시켜야 되기 때문에, 서브 스레드 대신 메인스레드에서 실행되게 만들어야 한다. 서브스레드 대신 메인스레드에서 실행되도록 DispatchQueue.main.async() 구문을 사용한다.
서버 통신으로 받아온 jsonObject를 NSDictionary 객체로 변환하고, 각각 값을 출력해서 텍스트에 집어넣으면 된다.
URL 객체를 만들고, URLSession 에 URL 객체를 담아 보내고, 응답을 파싱하여 원하는대로 사용하면 된다.
이번에는 Echo API 호출할때와 비슷하지만, Request 가 JSON 형식을 따르는 호출을 시도한다. JSON 방식의 API 호출은 상용 서비스에서 많이 적용하는 방식이다.
전송할 값을 =와 &로 연결하는 대신 JSON 형식으로 구성하고, Content-Type 헤더를 application/json 으로 변경한다.
Echo API 호출 코드에서 일부분만 수정하면 된다.
func json() {
// 1. 전송할 값 준비
let userId = self.userId
let userPassword = self.userPassword
let param = ["userId": self.userId, "userPassword": self.userPassword]
let paramData = try! JSONSerialization.data(withJSONObject: param, options: [])
// 2. URL 객체 정의
let url = URL(string: "JSON 용 응답 서버 URL")
...
// 4. http 메시지의 헤더를 설정
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
...
}
이전 방식과는 다르게 JSONSerialization 을 이용해서 param 객체를 시리얼라이즈된 문자열로 만들었다.
꼼꼼한 재은 씨의 Swift 실전편
(2020년 9월 개정판이 나온다.)