앱 프로젝트를 진행하면서 API 통신을 할 일이 정말 많다. 아니, API 통신을 안 하는 부분이 거의 없는 것 같다. 백엔드 담당하시는 팀원이 API 설계를 하시고, Swagger 를 통해 테스트를 할 수 있게 만드셨는데, 처음보는 Content-Type 가 있었다.
multipart/form-data
항상 x-www-form-urlencoded 방식으로 HTTP Body 를 구성하다가 다른 타입이 있어 iOS 에서 어떻게 모델을 구성해 요청을 보내야할지 몰랐다. Query 타입이 아니다 보니 아래와 같은 형식으로 URL을 구성하는 것도 안 된다.
https://dapi.kakao.com/v2/local/search/keyword.json?y=37.514322572335935&x=127.06283102249932&radius=20000&query=스타벅스
현재 만들고 있는 화면들이다. 서버에 매장을 신규 등록하는 로직인데, 첫 번째 화면에서 특정 매장을 검색해서 (카카오 지도 API 사용) 다음 화면으로 넘어간다. 이때 넘어가는 정보들은 매장 이름 (mall_name), 매장 연락처 (contact), 카테고리명 (category_name), 주소 (address), 위도 (latitude), 경도 (longitude), 그리고 선택사항으로 매장 관련 사진 (thumbnail) 이다. 대략 7개의 정보들이다.
앱에서 이를 구현하기 전에 POSTMAN 으로 실험해 보고 싶었다.
Body 카테고리에 들어가서 반드시 "form-data"를 선택해야 한다. Query 타입으로 할 경우에는 x-www-form-urlencoded 으로 사용했는데, multipart formdata 로 통신 시 반드시 이걸 선택해야한다.
Request 를 보내고 Response Body 가 제대로 오는 모습을 확인할 수 있다.
우선 Alamofire 를 사용해야 좀 편한 것 같다. 내가 만들고 있는 앱에서는 "신규 매장"을 등록해야 하는데, 이때 Model 로 아래를 사용할 것이다.
class NewRestaurantModel {
/// 매장명
let name: String
/// 매장 연락처
let contact: String?
/// 매장 위치
let address: String
/// 카테고리 이름 ( i.e 음식점 > 카페 > 커피전문점 > 스타벅스 )
let categoryName: String
/// Y 좌표값, 경위도인 경우 latitude(위도)
let latitude: Double
/// X 좌표값, 경위도인 경우 longitude (경도)
let longitude: Double
/// 매장 관련 이미지
let images: [Data]?
}
위에서 설명한 것처럼 7개의 필요한 정보가 저장되어 있다.
아래는 Alamofire 를 이용하여 통신을 하는 코드다.
//MARK: - 신규 매장 등록
func uploadNewRestaurant(with model: NewRestaurantModel,
completion: @escaping ((Bool) -> Void)){
AF.upload(multipartFormData: { (multipartFormData) in
multipartFormData.append(Data(model.name.utf8),
withName: "mall_name")
multipartFormData.append(Data(model.categoryName.utf8),
withName: "category_name")
multipartFormData.append(Data(model.address.utf8),
withName: "address")
multipartFormData.append(Data(String(model.latitude).utf8),
withName: "latitude")
multipartFormData.append(Data(String(model.longitude).utf8),
withName: "longitude")
if let imageArray = model.images {
for images in imageArray {
multipartFormData.append(images,
withName: "thumbnail",
fileName: "mall_image",
mimeType: "image/jpeg")
}
}
}, to: uploadNewRestaurantRequestURL,
headers: model.headers)
.responseJSON { (response) in
guard let statusCode = response.response?.statusCode else { return }
switch statusCode {
case 200:
print("매장 등록 성공")
completion(true)
default:
if let responseJSON = try! response.result.get() as? [String : String] {
if let error = responseJSON["error"] {
if let errorMessage = NewRestaurantUploadError(rawValue: error)?.returnErrorMessage() {
print(errorMessage)
} else {
print(error)
print("알 수 없는 오류가 발생했습니다.")
}
completion(false)
}
}
}
}
}
코드가 좀 길지만, multipart 통신을 하려면 아래 부분이 핵심이다.
multipartFormData.append(Data(model.name.utf8),
withName: "mall_name")
multipartFormData.append(Data(model.categoryName.utf8),
withName: "category_name")
multipartFormData.append(Data(model.address.utf8),
withName: "address")
multipartFormData.append(Data(String(model.latitude).utf8),
withName: "latitude")
multipartFormData.append(Data(String(model.longitude).utf8),
withName: "longitude")
if let imageArray = model.images {
for images in imageArray {
multipartFormData.append(images,
withName: "thumbnail",
fileName: "mall_image",
mimeType: "image/jpeg")
}
}
multipart 통신에서는 일반적으로 String, Int, Double, 이런 식으로 바로 데이터를 보내는 것이 아니라, byte buffer 형식으로 보내야한다. 그렇게 때문에 보내고자 하는 값 각각을 최종적으로 Data() 를 이용하여 형변환을 해 준 다음에 append 해줘야 한다. 각 값에 utf8 을 적용한 이유는 인코딩을 위해서다.
Alamofire 를 이제 막 사용하기 시작해서 아직 모든 부분이 이해가 잘 되지는 않는데, 인자로 클로져가 들어가는 것이 신기했다. 아무래도 Swift 가 functional programming 을 지원하기 때문에 이런 방식이 가능한 것 같은데, 아직은 헷갈린다,,