iOS & Swift 공부 - Alamofire MultiPart 통신

김영채 (Kevin)·2021년 4월 30일
5

iOS & Swift

목록 보기
103/107

Multipart 통신


앱 프로젝트를 진행하면서 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으로 실험


앱에서 이를 구현하기 전에 POSTMAN 으로 실험해 보고 싶었다.


Body 카테고리에 들어가서 반드시 "form-data"를 선택해야 한다. Query 타입으로 할 경우에는 x-www-form-urlencoded 으로 사용했는데, multipart formdata 로 통신 시 반드시 이걸 선택해야한다.

Request 를 보내고 Response Body 가 제대로 오는 모습을 확인할 수 있다.

iOS 에서는?


우선 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 을 지원하기 때문에 이런 방식이 가능한 것 같은데, 아직은 헷갈린다,,

profile
맛있는 iOS 프로그래밍

0개의 댓글