🛫서버 연동 공부

서버 연동 경험이 전무한 것은 아니지만, 단순히 GET을 통해 데이터를 가져오는 것에만 익숙해 걱정이 좀 있었습니다.
심지어 배포받은 API가 30개 가량 되기 때문에, 빨리 익혀서 앱잼 기간 내에 작업을 완료해야했습니다.

다행히 저희 리드개발자 태현이형이 하나하나 다 완벽하게 설명해준 덕분에 금방 이해할 수 있었습니다! 팟짱 못지않은 YB 클라스...

이번 프로젝트를 통해 처음 알게 된 서버관련 몇 가지를 기록해보겠습니다.

🛬Request-Header

두리번은 로그인 과정을 통해 유저를 구분하여 서비스를 제공하기 때문에, 대부분의 API에서 Request-Header에 토큰을 입력했어야 했습니다.
따라서 토큰을 싱글톤 변수로 저장해두고 활용했습니다.

struct APIConstants {
	static let jwtToken = "~~"
}

struct NetworkInfo {
    static let token = APIConstants.jwtToken
    static var header: HTTPHeaders {
        [NetworkHeaderKey.contentType.rawValue: APIConstants.applicationJSON]
    }
    static var headerWithToken: HTTPHeaders {
        [
            NetworkHeaderKey.contentType.rawValue: APIConstants.applicationJSON,
            NetworkHeaderKey.auth.rawValue: token
        ]
    }
}

🛬Parameters

여행 그룹에 따라 다른 정보들을 지니고 있고, 사용자별로 다른 여행 그룹을 갖기 때문에 API송수신 과정에서 해당 그룹의 고유ID를 주고 받아야했습니다.
API주소에서 ":groudID"에 해당하는 부분은 replacingOccureences 메소드를 활용했습니다.

struct EditTripService{
    static let shared = EditTripService()
    
    private func makeURL(groupID: String) -> String {
        let url = APIConstants.editTripURL.replacingOccurrences(of: ":groupId", with: groupID)
        return url
    }

🛬pathErr

POST API를 사용하는 과정에서 네트워킹 결과가 PATH ERROR가 나오는 경우가 지속됐었습니다.
처음엔 오타가 있거나 문법적으로 오류가 있는 줄 알고 한참 확인해보았지만 잘못된 것은 없었습니다.
결국 태현이형에게 확인요청을 한 결과, 데이터 모델을 잘못 설계한 탓이었습니다.
데이터 모델은 항상 Respone-Body를 바탕으로 구성해야하는데, POST API를 사용하며 Request-Body를 바탕으로 구성한 것이 문제였습니다.

import Foundation
struct EditTripResponse: Codable {
    let status: Int
    let success: Bool
    let message: String
    let data: EditTripData
}

// MARK: - DataClass
struct EditTripData: Codable {
    let travelName, destination, startDate, endDate: String
    let image: String
}

데이터 모델은 항상 Respone-Body를 바탕으로 구성해야한다는 것을 깨닫게 되었습니다.

🛬PATCH API

GET&POST 이외의 API는 접해본 적이 없어 PATCH API를 처음 보곤 많이 당혹스러웠습니다.
어떤 기능을 하는 지도 모르고, 사용법도 몰랐기 때문입니다.
구글링을 해보니 다행히(?) POST API와 사용법이 동일하여 별 탈 없이 사용할 수 있었습니다.
PATCH는 이전에 POST했던 데이터를 갱신해주는 역할을 하는 API라고 합니다.

    func patchData(groupID : String,
                   travelName : String,
                   destination : String,
                   startDate : String,
                   endDate : String,
                   imageIndex : Int,
                   completion : @escaping (NetworkResult<Any>) -> Void)
    {
        let url: String = makeURL(groupID: groupID)
        let header : HTTPHeaders = NetworkInfo.headerWithToken
        let dataRequest = AF.request(url,
                                     method: .patch,
                                     parameters: makeParameter(travelName: travelName, destination: destination, startDate: startDate, endDate: endDate, imageIndex: imageIndex),
                                     encoding: JSONEncoding.default,
                                     headers: header)
        
        dataRequest.responseData { dataResponse in
            switch dataResponse.result {
            case .success:
                guard let statusCode = dataResponse.response?.statusCode else {return}
                guard let value = dataResponse.value else {return}
                let networkResult = self.judgeStatus(by: statusCode, value)
                print(statusCode)
                print(networkResult)
                completion(networkResult)
                
            case .failure: completion(.pathErr)
                
            }
        }
        
    }

Alamofire.request 메소드를 사용할 때 method를 post에서 patch로 변경만 해주면 되었습니다.

이러한 내용들이 서버 연동을 위해 새로 공부해야 했던 내용들이었으며, 생각보다 그렇게 어렵진 않았습니다.




🛫날짜 파싱

두리번은 여행 관련 어플이라 시간과 날짜를 다룰 일이 많았습니다.
이전에 캘린더 작업을 할 때에 잠깐 다뤄보긴 했지만, 더미데이터가 아닌 실제 서버와 데이터를 송수신하려고 하니 형태가 맞지 않아 오류도 많이 나고 출력해줄 때 원하는 형태로 변환해주는 과정도 너무 어려웠습니다.

{
	"travelName": "두리번 강릉 여행",
	"destination": "강릉",
	"startDate": "2021-07-17",
	"endDate": "2021-07-18",
	"imageIndex": 1
}

API 사용시 날짜를 위와 같은 형태와 String형식으로 주고 받아야했는데, 데이터를 사용할 땐 아래와 같은 구조로 사용하기 때문에 파싱이 필요했지만 처음엔 그 과정이 어려웠습니다.

요일같은 경우에는 Calendar를 활용해 추출했으며, 날짜 형태는 separatedBy를 활용해 변형했습니다.

    func dateSet() {
        let f = DateFormatter()
        f.locale = Locale(identifier: "ko_KR")
        f.dateFormat = "yyyy.MM.dd"
        let today = f.date(from: self.startDate)
        var cal = Calendar(identifier: .gregorian)         // 그레고리 캘린더 선언
        cal.locale = Locale(identifier: "ko_KR")
        let dateComponents = cal.dateComponents([.weekday], from: today!)
        guard let weekIndex = dateComponents.weekday else { return }
        let dayOfWeek = cal.weekdaySymbols[weekIndex-1]
        let strList = startDate.components(separatedBy: "-")
        day = "\(strList[0]).\(strList[1]).\(strList[2])(\(dayOfWeek.first!))"
        startDateLabel.text = day
        endDateLabel.text = day
    }

혹은 Service 파일에서 JSONDecoder 자체를 포메팅할 수 있었습니다.
JSONDecoder 자체를 포메팅하면, 데이터 모델의 형태도 수정해주어야합니다.

 private func judgeStatus(by statusCode: Int, _ data: Data) -> NetworkResult<Any> {
        
        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd-HH:mm"
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(f)
        guard let decodedData = try? decoder.decode(MainDataModel.self, from: data)
        else { return .pathErr }
        
        switch statusCode {
        
        case 200: return .success(decodedData)
        case 400: return .pathErr
        case 500: return .serverErr
        default: return .networkFail
        }
    }
    
 struct Group: Codable {
    let _id: String
    var startDate: Date
    var endDate: Date
    var travelName: String
    var image: String
    var destination: String
    let members: [String]
    }



🛫서버 로딩

서버에서 이미지 혹은 큰 용량의 데이터를 불러오게 되면 로딩이 되는 동안의 시간이 소요되게 됩니다.
데이터가 로딩이 되는 동안에는 데이터가 들어갈 자리들이 빈 자리로 보이게 되어 이 부분은 어떻게 해야할지 팀원들과 상의해보았습니다.

  1. Placeholder를 사용해 데이터가 불러와지는 동안의 빈자리를 커버한다.
  2. 메인 뷰가 로딩되는 과정에서 모든 데이터들을 한 꺼번에 로딩해버린다.
  3. 로딩 인디케이터를 사용한다.

등의 의견이 나왔고, 결국엔 SkeletonView 라이브러리와 로딩 인디케이터를 함께 사용하기로 결정했습니다.

private func getPlanData(date: String) {
        guard let groupId = tripData?._id else { return }

        startLoading()
        TripPlanDataService.shared.getTripPlan(groupId: groupId,
                                               date: date) { [weak self] (response) in
            switch response {
            case .success(let data):
                if let schedule = data as? [Schedule] {
                    self?.endLoading()
                    self!.planData = schedule
                }
            case .requestErr(_):
                print("requestErr")
                self?.endLoading()
            case .serverErr:
                print("serverErr")
                self?.endLoading()
            case .networkFail:
                print("networkFail")
                self?.endLoading()
            case .pathErr:
                print("pathErr")
                self?.endLoading()
            }
        }
    }

서버로부터 데이터를 불러오는 과정에서 인디케이터를 활용해 뷰를 멈춰두고, 스켈레톤뷰를 활용해 Placeholder를 남기며 로딩이 끝나게 되면 인디케이터를 종료시켜 뷰가 활성화되게 하는 방식입니다.
이렇게 구성하니 깔끔하고 자연스럽게 뷰 전환이 가능했습니다.




🛫마무리

16일차부터 마지막21일차 까지 서버연동 작업을 진행하며 나름 성공적인 데모데이를 치룰 수 있었습니다.
서버연동하는 작업은 개념적으로는 딱히 어려운 내용이 없었지만, 알 수 없는 이런저런 오류들이 너무 많이 나는게 고생이었습니다 ㅠㅠ
특히 메인뷰에서는 "when"에 따라 데이터를 분류해주어야했고, 분류된 데이터들을 각각 컬렉션뷰와 테이블뷰에 넣어주는 과정이 많이 복잡했습니다.

이번 프로젝트를 통해 서버 데이터가 많아지고 복잡해지는 경우에도 잘 분류하는 법을 배울 수 있었고, 당시에는 많이 힘들었지만 지금 되돌아보면 한 번쯤 꼭 겪어봐야할 성장통이었다는 생각이 듭니다.

지금까지의 회고는 프로젝트를 진행하며 기술적으로 배웠던 내용들을 기록했지만, 다음 회고에서는 프로젝트의 경험, 추억들을 전체적으로 기록하는 글을 작성할 예정입니다!

profile
공부방

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN