아삭아삭 iOS 개발

[SLP] SeSAC Study Post-Mortem 본문

1인 앱개발

[SLP] SeSAC Study Post-Mortem

바닐라머스크 2022. 12. 25. 22:27

새싹 프로그램의 마지막 프로젝트였던 SLP(Service Level Project)가 드디어 마무리되어 회고를 해보려고 합니다!

 

마지막 프로젝트 였기도 하고 서비스레벨의 작업을 해본다는 점이 매우 설레고 의미가 있었던 것 같아요.

이전에 모든 것을 혼자 작업했던 출시 프로젝트와 달리

전문가의 손길이 느껴지는 디자인 리소스(Figma 사용), 서버(Swagger로 확인), 그리고 기획안(Confluence로 협업)을 갖고 작업하니 보다 체계적인 진행을 할 수 있어 좋았습니다.

(정성스럽게 작성해주신 디자인 리소스, 기획안, 그리고 바쁘신 와중에 서버관리까지 모두 감사합니다 멘토님들!!👍🏻)

 

Figma에서는 화면 구성을 참고하고, Confluence 기획안에서는 작동 로직을 참고하여

어떤 UI객체로 화면을 구성할지 그리고 어떤 방식으로 로직을 짜야할지 나름의 가설들을 세우고 시행착오를 거치며 코드로 짜보는 과정들이 정말정말 재밌었습니다.😌

또 같은 디자인 리소스를 참고했는데도 다른 새싹분들과 서로 구현방안이 한 명 한 명 모두 달랐던 점이 신기해서 기억에 남네요.

 

이제 새싹에서의 프로젝트는 끝이라는 사실이 너무 아쉽지만,,

마지막까지 많은 것을 배우고 느꼈던 slp 프로젝트였던만큼 오래오래 기억에 남을 것 같아요 :)

 

Data Points

  • 개발자: 저요!👩🏼‍💻
  • 개발언어: Swift
  • 개발기간: 2022.11.08 ~ 2022.12.18
  • 프로젝트 소개:
    - 위치 기반 스터디(모각코) 서비스
    - 스터디를 원하는 상대에게 스터디 요청 할 수 있고, 상대방이 수락하면 채팅 가능
  • 본 프로젝트 개인적 목표:
    - 실무 환경과 동일한 개발 프로세스로 진행해보자(Figma, Swagger, Confluence)
    - 서버없이 개인 프로젝트로는 구현이 어려웠던 기술들을 써보자 (Auth, RESTful API, Socket, IAP(인앱결제), Remote Push 등)
    - 서비스 레벨 프로젝트이니 실제 사용자들의 다양한 유저케이스가 있을 것을 염두하면서 작업하자

[👉(GitHub)프로젝트 전체코드 보러가기]

 

Good Points & Bad Points

Keywords : 데이터 일관성, 구조화, 재사용성 , 실무 환경, commit, 서비스 레벨 고려사항 등

 

Good Point1) 이미지, 색상, 폰트 등 에셋요소들을 Enum으로 관리하여 데이터 일관성 부여

프로젝트 시작 후 둘째 날까지는 프로젝트 셋팅과 에셋정리하는 데에만 시간을 할애했다.

개인출시때와 다르게 프로젝트내 사용될 메인색상, 폰트종류, 이미지, 워딩 등이 모두 명확하게 셋업 완료된 상태에서 시작했기에 나중에 깔끔하게 가져다가 사용할 수 있도록 구조를 만들어두고자 했다.

 

이렇게 초반에 정리를 해두었더니, 확실히 UI나 로직을 구성하는 코드에 raw한 값들이 들어가는 빈도가 줄었다.

이번 플젝 진행중에는 급작스러운 에셋요소의 변경사항은 없었지만, 추후 타 플젝 진행시 갑자기 메인색상이나 워딩, 혹은 버튼의 이미지 값 등이 바뀌었을 경우 코드를 전체적으로 재확인하지 않고도 빠르게 수정할 수 있을 것이다.

즉, 변경사항에 대한 대응성이 좋아지고 여기에 드는 공수도 줄어들고, 휴먼에러도 줄일 수 있는 등 사전의 에셋 구조화에 대한 장점을 알 수 있었다.

 

다만 데이터의 일관성을 위한 처리 중, 내가 누락한 부분이 있어  아쉬웠다.

대표적으로, 이번 프로젝트에서 남녀선택 구분값은 각각 여자는 0, 남자는 1 이었는데 이를 기준으로 버튼 색상이나 불러오는 데이터값을 구분해주고 있었다. 만약 여자를 나타내는 값이 앞으로 항상 0이 아닐 수도 있다는 점을 감안한다면, 추후 데이터가 바뀌더라도 코드 수정이 없을 수 있도록 추가 개선이 필요하다.

프로젝트 초반에 정리한 색상, 폰트, 이미지, 워딩

 

Good Point2) API통신 코드들을 API Router로 관리하여 간편하게 사용

이번 프로젝트에서는 내정보, 채팅, 샵 등과 관련해서 연결할 API 통신 코드들이 많았다.

그래서 지난번 Trady 출시플젝때 아쉽게 시도하지 못했던 router를 생성하여 적용해보았다.

 

아래 화면에서 보는 것과 같이 각 통신의 종류별로 파일을 나누고, enum으로 API Router를 생성 후 그 내부에서 통신 case별로 구분하여 작성하였다.

우선 router로 정리하니 router 내부에서 네트워크 케이스별 구분이 한 눈에 보여서 가독성이 향상되었고,

타 뷰컨에서 통신코드 작성시 편리하게 사용할 수 있었다.

그리고 네트워크들을 종류별로 구분해두었더니 1개의 네트워크 소스만 사용할 때 모든 네트워크 관련 코드가 메모리에 올라가지 않아도 된다는 장점이 있었다.

왼쪽부터 순서대로 채팅, 스터디, queue, 샵 관련 네트워크 통신 API router

또한 팀별 코드리뷰시간에 네트워크 관련 아래 2가지 사항에 대해서도 개선할 수 있다고 새롭게 알게 되었다.

다음번에는 아래 방안도 적용해서 통신코드를 더 효율적이고 안정적이도록 개선해보고 싶다.

  • 사용할 데이터를 구조체로 만들어두고, 네트워크에서 따로 파라미터 적용
  • 통신 분야별로 Router분리 시 버전을 관리

 

Good Point3) 반복되는 UI에 대해 UITableViewHeaderFooterView, UITableViewCell, 그리고 UIViewController를 재사용하여 코드의 재사용성 향상

Figma로 전체적인 구성 화면들을 죽 훑어보았을 때 반복적으로 사용되는 화면들이 있었다.

대표적으로 UserCard, 팝업화면, 새싹찾기 화면의 탭2개 내부화면들 이렇게 세 부분에서 재사용이 가능해 보였고 각각 아래와 같이 구현했다.

 

(1) UserCard

UserCard가 사용된 곳은 총 3곳(내정보 관리, 새싹찾기, 새싹샵) 이었는데 이 중 두 곳에서는 UserCard가 상단 고정이었고, 나머지 한 곳에서는 UserCard가 리스트도 반복되어 보여지고 있었다.

그래서 해당 3곳에서 UserCard를 포함한 계층은 TableView로 구현하고, UserCard의 이미지와 이름은 UITableViewHeaderFooterView로,  접혔다 펼쳐질 때 보여지는 곳은 UITableViewCell로 구성하려고 결정했다.

정보관리, 새싹찾기, 새싹샵에 재사용된 UserCard

UserCard가 적용된 각 화면에서 재사용된 UI가 조금씩 다르고 ([요청하기] 버튼 유무, 이름라벨 및 접었다폈다 유무)

보여지는 데이터도 다르고 (내정보, 다른 새싹의 정보, 실시간 선택된 새싹&배경이미지)

User Interaction에 따른 이벤트도 상이했다.(계정 정보 펼치기, 스터디 요청하기, 스터디 수락하기, 프로필 저장하기 등)

 

그래서 화면별로 상이한 요소들을 구분 적용하기 위해 UITableViewHeaderFooterView, UITableViewCell 클래스 내부에 메서드들을 생성하여 구현하였다.

 

// UserCard의 이미지와 이름 표기 View
class CollapsibleTableViewHeader: UITableViewHeaderFooterView {
    ...
    // my info
    func setData(bgNum: Int, fcNum: Int, name: String) {
        backgroundImage.image = UIImage(named: "sesac_background_\(bgNum + 1)")
        sesacImage.image = UIImage(named: "sesac_face_\(fcNum + 1)")
        nameLabel.text = name
    }
    
    // 새싹 찾기
    func setSesacData(data: [FromQueueDB], section: Int) {
        let row = data[section]
        backgroundImage.image = UIImage(named: "sesac_background_\(row.background + 1)")
        sesacImage.image = UIImage(named: "sesac_face_\(row.sesac + 1)")
        nameLabel.text = row.nick
    }
    
    // 샵
    func setPreviewData(bg: Int, sprout: Int) {
        nameView.isHidden = true
        askAcceptbtn.setTitle("저장하기", for: .normal)
        backgroundImage.image = UIImage(named: "sesac_background_\(bg + 1)")
        sesacImage.image = UIImage(named: "sesac_face_\(sprout + 1)")
    }
}
// UserCard의 접었다 펼칠 때 보여지는 profile cell

final class ProfileCell: BaseTableViewCell {

	....

    // [my info]
    func setData(reputation: [Int], comment: [String]) {
        title2.isHidden = true
        wantedStudy.isHidden = true
        setProfileData(reputation: reputation, comment: comment)
    }
    
    // [새싹 찾기]
    func setSesacData(data: [FromQueueDB], section: Int) {
        title2.isHidden = false
        wantedStudy.isHidden = false
        setProfileData(reputation: data[section].reputation, comment: data[section].reviews)

        let study = data[section].studylist.isEmpty ? "아무거나" : data[section].studylist[0]
        wantedStudy.setTitle(study, for: .normal)
    }
    
    override func prepareForReuse() {
        titleButton1.setImage(nil, for: .normal)
        titleButton2.setImage(nil, for: .normal)
        titleButton3.setImage(nil, for: .normal)
        titleButton4.setImage(nil, for: .normal)
        titleButton5.setImage(nil, for: .normal)
        titleButton6.setImage(nil, for: .normal)
        wantedStudy.setTitle(nil, for: .normal)
        reviewTextField.text = nil
    }
    
}

 

(2) 팝업화면

[취소/확인] 팝업이 쓰이는 곳은 총 4곳으로 탈퇴/스터디요청/스터디수락/스터디취소 가 있다.

페이지 모드 구분을 위해 PageMode라는 Enum을 생성하여 4개의 case를 생성했다.

긜고 케이스별로 팝업화면에서 달라져야 할 요소는 '팝업문구'와 '[확인]클릭시 실행될 액션'이였기에 이를 구분해주었다.

 

팝업문구는 enum내부에 프로퍼티를 생성하여 정리했고,

네트워크는 [확인]버튼 클릭시 실행될 함수 내에서 switch문으로 분기처리했다.

 

(3) 새싹찾기 화면의 탭2개 내부화면들

[새싹찾기] 화면에서는 아래 표처럼 4가지 케이스의 화면이 나올 수 있었다.

케이스가 나뉘는 기준은 , 그리고 나타낼 sesac데이터 유무 이렇게 2가지였는데

두 탭 모두 케이스별로 보여지는 화면의 UI가 동일했기 때문에 하나의 뷰컨으로 구성하기로 결정했다.

 

< 내주변새싹 or 받은요청 >

탭 구분은 Tabman이라는 라이브러리로 간단히 구현하고 넘어왔기에,

사용자가 특정 탭 클릭시 어느 화면에 있는지 인식할 수 있도록 구분해줄 값이 필요했다.

 

구분해줄 기준값을 찾던 중, pageboyPageIndex를 찾게되어 해당 값으로 탭 구분을 주었다.

 

pageboyPageIndex 정의

 

< 나타낼 Sesac 데이터 있음 or 나타낼 Sesac 데이터 없음 >

아래와 같은 계층구조를 토대로 나타낼 Sesac 데이터 유무에 따라  tableView 혹은 emptyView를 isHidden 하려고 했다.

계층구조 : SearchResultViewController > ListViewController > ListView > tableView or emptyView

- SearchResultViewController : TabmanViewController로 하단에 깔린 뷰컨
- ListViewController : Tabman 내부의 PageboyViewController가 될 뷰컨. 여기서 탭 구분값으로 구분해야 함!

 

위 구분값들을 사용하여 뷰컨트롤러 1개로 4가지 케이스의 화면표기를 나타낼 수 있었고

결론적으로 화면의 재사용성을 높일 수 있었다.

 

 

Good Point4) 다양한 고려사항에 대해 서비스적인 고민을 해보고 기획자와 소통하며 작업함

 

(1) 기획안에 명시된 사항은 아니지만, 사용자의 입장을 고려하여 검색기능에 불편함이 덜하도록 로직을 추가함

사용자가 [새싹찾기]에서 스터디원을 찾다가 다른 스터디 키워드로 다시조회하고자 [스터디 입력]화면으로 돌아올 경우,

기존에 입력했었던 '내가 하고 싶은' 키워드가 남아있도록 로직을 추가하고 싶었다. 사소한 사항이긴 하지만 그 이유는

 

- 타서비스에서의 검색입력 기능을 떠올려보았을 때 기존에 입력했던 키워드들은 남아있어서 편리하게 기존 기록 확인이 가능했고

- 내가 SeSAC Study 사용자라면 키워드 몇 개만 수정해서 재조회하고자 할 때 기존에 입력사항이 날라가 있다면 '내가 전에 어떤 키워드로 검색했더라? 새롭게 추가하거나 제외하려고 했던 키워드는 뭐였지..?' 하는 생각과 함께 키워드를 다시 다 추가해야하는 불편함이 있을 것으로 예상되었기 때문이다.

 

(왼) waiker 입력란, 쿠팡이츠 입력란, SLP SeSAC 입력란

final class MainViewController: BaseViewController {
    ....
    // 플로팅 버튼 클릭
    @objc func floatingButtonTapped() {
  
        switch matchingMode {
               case .normal: // 일반 상태일 경우 (즉, 새롭게 스터디를 조회할 때)
                   let authorizationStatus = locationManager.authorizationStatus
                   if authorizationStatus == .denied || authorizationStatus == .restricted {
                       showRequestLocationServiceAlert()
                   } else {
                       UserDefaultsManager.mywishTagList = [] // '내가 하고 싶은' 리스트 초기화
                       let vc = SearchViewController()
                       transition(vc, transitionStyle: .push)
                   }
                   return
    ....
                   
}

final class SearchViewController: BaseViewController {
	var mywishTagList: [String] = []

	override func viewWillAppear(_ animated: Bool) {
	  super.viewWillAppear(animated)
    mywishTagList = UserDefaultsManager.mywishTagList as! [String] // 저장된 정보를 화면 표기하기 위해 배열에 할당
  }

  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
		switch indexPath.section {
    case 0:
	    let selectStudy = aroundTagList[indexPath.row]
			if mywishTagList.contains(selectStudy) {
				...
       } else {
	       mywishTagList.append(selectStudy)
         UserDefaultsManager.mywishTagList = mywishTagList // '내가 하고 싶은' 리스트에 추가
				...
       }
        case 1:
            let selectStudy = mywishTagList[indexPath.row]
            mywishTagList = mywishTagList.filter { $0 != selectStudy}
            UserDefaultsManager.mywishTagList = mywishTagList // 변경된 '내가 하고 싶은' 리스트 저장
				...
        }
    }
}

// 텍스트필드에 입력한 사항으로 mywishTagList에 추가사항이 생길 때
extension SearchViewController: UITextFieldDelegate {
        
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        ...
        guard let text = textField.text else { return true }
        var inputStudy = text.components(separatedBy: " ").filter { $0.count > 0 }
        let inputStudyLength = inputStudy.map { $0.count }.filter { $0 != 0 }

        if inputStudyLength.min()! < 1 || inputStudyLength.max()! > 8  {
						...
        } else if (inputStudy.count + mywishTagList.count) > 8 {
            let to = 8 - mywishTagList.count
            let slicedArr = Array(inputStudy[0...(to - 1)])
            mywishTagList.append(contentsOf: slicedArr)
            
            UserDefaultsManager.mywishTagList = mywishTagList // 변경된 '내가 하고 싶은' 리스트 저장
						...
            return true
            
        } else {
            mywishTagList.append(contentsOf: inputStudy)
            UserDefaultsManager.mywishTagList = mywishTagList // 변경된 '내가 하고 싶은' 리스트 저장
						...
            return true
        }
    }
    
}

그래서 [홈]->[스터디 입력]으로 갈 땐 초기화하고, [새싹찾기]->[스터디 입력]으로 갈 땐 기록 남아있도록 했다.

기능구현은 성공해서 좋았지만 if문이 너무 많아서 개선하고 싶다..

 

(2) 기획안과 디자인 리소스로 유추하기에 한계가 있는 질문사항에 대해 기획자와 소통하였고, (아직 기획안에 없는 내용이지만) 추후 적용예정인 기능에 대한 추가정보를 알게 되어 앞으로의 진행방향을 고려하며 구현할 수 있었다.

 

<궁금했던 사항>

[새싹찾기] 화면 말고도 어느 화면에 있든지 사용자의 상태를(myQueueState API) 확인해야 하지 않을까?

왜냐하면 내가 요청을 보내두고 다른 화면에 머무르는 도중에 상대방이 요청을 수락한다는 가정을 할 경우,

case1) [새싹찾기]에서만 상태확인을 한다면

           : 사용자는 매칭완료 여부도 모를 수 있고, 상대방 새싹은 무한정 채팅방에서 기다리게 될 수 있음

case2) 모든 화면에서 실시간으로 상태확인을 한다면

           : 사용자는 아직 채팅을 할 마음의 준비(?)가 안되었는데, 다른 화면에 있다가 갑자기 채팅화면으로 끌려갈 수도 있음

 

<기획자 답변>

- 5초마다 사용자 상태 확인하는 것은 [새싹찾기]에서만 진행

- 왜냐하면 (Confluence에 기획안 아직 명시는 안했지만) 추후 새싹찾기 외 화면(그리고 background에서도)에서는 push알림 구현 예정

- 푸시 알림으로 사용자는 매칭여부를 알 수 있고 채팅방에 입장하는 시기를 선택할 수 있음

- 홈화면에서 플로팅버튼을 클릭하는 순간에도 매칭여부가 변할 수 있으므로, 해당 액션 실행시에도 상태확인 필요함

 

이렇게 기획안에 명시되어 있지 않지만 궁금한 부분에 대해 기획자에게 문의해보며
명확한 로직, 그에 대한 이유, 추후 진행예정 사항, 예외케이스에 대한 고려사항까지 알 수 있었다.
덕분에 남은 로직에 대해서는 추후 진행 방향성을 고려할 수 있었고, 특히나 기획자와의 소통 중요성을 느꼈다.

 

 

Bad Point1) RX, MVVM사용이 적절한 곳에 충분히 활용하지 못함

회원가입, 로그인 파트에서 간단하게 적용해본 것 외에 프로젝트 전체적으로 RxSwift와 MVVM을 충분히 적용해보지 못한 점이 아쉬웠다.

특히 TableView에 데이터 소스를 채우거나, 네트워크 결과를 비동기적으로 처리하거나, 실시간으로 사용자 액션에 반응하거나, 화면 구조가 복잡한 곳에서 데이터를 전달하는 등의 케이스를 구현할 때 Imperative Programming으로 작업을 했더니 코드가 매우 복잡해졌다.

 

예를 들어, [새싹샵]화면이 그 중 대표적인 케이스였는데

 

(왼)새싹샵 파일구조와 (오)구현 화면

 

(1) 복잡한 구조에서 비동기 처리를 작업할 때 Callback Depth(즉, 콜백지옥)이 발생했다.

사용자가 새싹 이미지 제품의 가격버튼을 클릭하는 User Interaction 처리 시

→ 몇 번째 제품인지 데이터를 전달하고

 전달받은 데이터를 인앱결제 구매로직을 태우고

 그 결과를 네트워크 통신으로 2번 연속적으로 거쳐서

 UI를 업데이트 해야했다...

 

아래 코드는 인앱결제를 위해 단순히 사용자가 클릭한 sprout 제품정보 데이터를 전달하는데 관련된 코드들이다.

정말..너무 복잡하다... 

심지어 아래 코드 이후에도 결제, 네트워크, UI 변경까지 비동기적인 작업들이 연속적으로 발생했기에 관찰방식의 필요성을 느꼈다.

사용자와의 User Interaction에 따라 화면이 실시간으로 변하며 데이터 Stream이 처리되도록 Reactive Programming으로 리팩토링 해야겠다.

 

class PriceButton: UIButton { // step1
    var row: Int?
}

final class ShopSesacCollectionViewCell: BaseCollectionViewCell {
    
    let priceButton: PriceButton = { // step2
        let btn = PriceButton()
        return btn
    }()
}

final class ShopSesacView: BaseView {

    var ssPriceButtonActionHandler: ((Int) -> ())? // step3
    var row = 0 // 값 전달용

    let cellRegistration = UICollectionView.CellRegistration<ShopSesacCollectionViewCell, SesacController.SesacItem> { (cell, indexPath, faceItem) in
        cell.priceButton.addTarget(self, action: #selector(self.priceBtnTappedClicked), for: .touchUpInside) // step4
        cell.priceButton.row = indexPath.row // 값 전달용
    }

    @objc func priceBtnTappedClicked(sender: PriceButton) {
        guard let row = sender.row else { return }  // 값 전달용
        ssPriceButtonActionHandler?(row) // step5
    }
}

final class ShopView: BaseView {
    
    func setPriceButtonBuyAction() {
        vc1.mainView.ssPriceButtonActionHandler = { row in // step6
            // 구매로직으로 연결
        }
    }
}

 

(2) UI 컴포넌트에 데이터를 채우기 위해 여러개의 Delegate 프로토콜을 채택하여 코드가 길고 복잡해짐

상단 프로필의 새싹와 배경 이미지(UITableViewHeaderFooterView)에 네트워크 통신결과를 채우거나, 실시간으로 사용자가 선택한 데이터를 채우기 위해 UITableViewDelegate이 필요했다.

그리고 새싹과 배경 이미지 제품(UICollectionViewCell)을 나타내거나 사용자의 제품구매로 UI 업데이트 등을 구현하기 위해 UICollectionViewDelegate이 필요했다.

 

만약 동일한 기능을 구현하는데 RxSwift, RxCocoa를 도입하면 delegate를 통한 과정이 아닌 매우 간결한 코드로 UI요소와 데이터 소스를 바인딩해서 기능을 구현할 수 있었을 것이다.

리팩토링시 꼭 체크하자!!

final class ShopView: BaseView {

    func setDelegate() {
        pageViewController.dataSource = self
        pageViewController.delegate = self
        tableView.delegate = self
        vc1.mainView.collectionView.delegate = self
        vc2.mainView.collectionView.delegate = self
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {...}

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {...}

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int. {...}
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {...}
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {...}

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {...}

}

 

(3) MVVM 아키텍처 패턴 적용을 좀 더 명확하게 했어야 했다.

매우 무거워진 새싹샵 뷰컨(ShopViewController)을 비우고자 UI 업데이트 관련 코드들을 뷰(ShopView)로 최대한 옮겼다.

하지만 상태변화를 감지하는 코드를 ViewModel로 분리하지 못하고 이마저도 모두 뷰에 몰아서 넣게 되었다.

그래서 결과적으로 뷰컨은 가벼워졌지만, 오히려 뷰가 똑같이 무거워지고 데이터 처리코드까지 포함한 이상한 구조가 되었다..

 

리팩토링시에는 View는 ViewModel의 상태변화에 따라 UI를 업데이트만 하도록 하고,

ViewController에서는 RxSwift를 통해서 ViewModel의 상태와 View를 binding하는 방향으로 개선해야겠다!

 

 

 

Bad Point2) API별 Error관리시 여러개가 아닌 하나의 Enum으로 관리할 수 있도록 통합적인 구조개선 필요

계정/스터디/샵/queue/채팅 등 종류별로 에러 enum을 생성하여 각각 관리하고 있었다.

하지만 공통적인 5개의 common case(statuscode 200, 401, 406, 500, 501)를 제외하고는 각 에러별로 1~3개 정도만 다를 뿐 케이스들이 거의 유사했다.

기존에는 에러 enum을 무식하게 종류별로 다 생성해서 사용했다면, 공통사항에 대해서는 enum안에 enum을 넣어서 작업해봐야겠다. 

개선방안에 대해 조사하던 중 아래 글을 발견했는데,

- MainError를 Enum 하나를 생성해서 통합관리하고, 그 내부에서 공통케이스와 추가적인 케이스들까지 모두 다루는 방안

- 추후 발생할 수 있는 future error에 대한 문구 상세처리까지 대응

할 수 있는 방안을 소개하고 있어서 흥미로웠다.

 

리팩토링시 참고해서 시도해봐야겠다.

https://blog.appcircle.io/article/error-handling-in-swift

 

Error Handling in Swift - Appcircle Blog

Error handling in Swift is a tricky subject. The core principle is to separate error types with associated values in a main error enum.

blog.appcircle.io

 

Bad Point3) 관련 메서드끼리 Singleton으로 정리하여 구조화 개선 필요 (locationManager, Socket, 인앱결제)

 

(1) 지도화면에서의 locationManager

(2) 채팅에서의 Socket관련 메서드들

(3) 인앱결제에서의 결제로직 IAPHelper정리

 

위 기능관련 메서드들을 아직 별도로 빼지않고 뷰컨에서 처리해주고 있기에 뷰컨에서의 코드 가독성도 떨어지고 무거워진 상태다.싱글톤으로 따로 정리하여 사용할 수 있다면 뷰컨도 가벼워지고, 메모리를 보다 효율적으로 사용할 수 있으니 별도로 정리해야겠다.

 

(각 기능들이 여러 화면에서 사용될 예정은 아니지만, static으로 타입 프로퍼티 인스턴스 생성시 사용시점에 초기화되므로 효율적인 메모리 사용에도 긍정적일 것) 

 

정리

  • 프로젝트 진행시 목표사항 일부 충족
    - 실무환경과 유사한 프로세스로 진행
    - 개인으로는 어려운 기술 구현(Auth, RESTful API, Socket, IAP(인앱결제), Remote Push)
  • 정해진 시간 안에 핵심기능을 우선순위로 개발하다보니, 생각만큼 모든 페이지에서 상세한 유저케이스들을 고려하지 못한 것 같아 아쉬움
  • 에셋정리, 기획자와의 의사소통 중요성을 느낌
  • commit을 작업한 기능별로 최대한 세세하게 하고(총 약 387회), 컨벤션을 적용해보니 히스토리 찾기에 수월했음
    다음번에 보다 체계적으로 해보자
  • RxSwift와 MVVM을 사용하면 코드를 간결하게 쓸 수 있다! 꼭 더 공부해서 코드 개선해보자

※ 더 공부해볼 사항

- RxSwift, RxCocoa + MVVM

- 에셋종류별 효율적인 구조화 방안

- 에러 Enum 통합관리