[두리번] 프로젝트 16~21일차 회고

한상진·2021년 7월 25일
0

두리번

목록 보기
7/8
post-thumbnail


🛫서버 연동 공부

서버 연동 경험이 전무한 것은 아니지만, 단순히 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개의 댓글