새싹

새싹 - 앱 출시 프로젝트 회고

Thor_yeom 2023. 10. 25. 01:17

 

 

새싹 과정 2개월차에 앱 출시 프로젝트를 진행하게 되었습니다.  

2개월동안 정말 많은 것들을 배웠습니다.

 

네트워크(URLSesseion, Alamofire), DB(Realm), 다국어, UIButtonConfiguration, Delegate패턴, 싱글톤 패턴, AutoLayout, 값 전달 , CodeBase, ARC, MVC, MVVM, DiffableDataSource... 등등... (이왜진... 2개월 동안 정말 쉴 새없이 달려왔네요

 

2개월이란 시간내에 단순히 지식을 전달하는 형식이 아니라 멘토님께서 정말 기초 개념부터 시작해서 빌드업으로 (진짜 빌드업이... 넘사벽임...갓잭) 비전공자들도 쉽게 이해 할 수 있게 머리속에 때려 넣어주었고 데일리 과제를 통해 복습을 하며 배운 기술을 직접 적용해보며 습득 할 수 있게 해주었습니다. (수업듣고 복습하고 데일리 과제하면... 보통 마무리 되는 시간은 새벽 1 ~ 2시였음...)

 

 

 

 

 

 

 

그렇게 담궈진 지식을 이제야 뽐낼 수 있는 앱 출시 프로젝트를 한다니 설레였고 

기획을 수정하고 수정하고 기능 추가하고 수정하고 기능 추가하고... 그렇게 앱은 거대해져갔습니다. (이거... 한달만에 가능한거냐??)

 

 

 

 

 

 

막상 이터레이션(기간에 내에 구현 정도)과 공수산정(해당 기능에 대한 구현 시간)을 해보니... 한 달안에 할 수없을 정도로 기능이 많아져서

기능을 다시 빼서 공수산정을 다시 했습니다. 버전 1.0.0 에 꼭 들어가야할 핵심기능과 추후 업데이트 사항을 나누었고 우당탕탕 앱 만들기 시작하겠습니다!

 

 

 

 

앱 기획 배경

 

여러분들은 혹시 어렸을때 아파트 혹은 동네에서 아나바다 장터 열릴때면 신나서 이것저것 구경도 하고 먹을 걸 사먹었던 기억이 있으신가요? ( 피캬츄... 컵떡볶이...쥐포... 포켓몬 딱지.. ) 장터가 열리는 기간에는 동네 전체가 놀이터가 되었었죠? ㅎㅎ 이러한 장터가 열릴때면 동네의 모든 이웃들이 나와 이야기 보따리를 풀고 물건을 사고 팔면서 한국의 정과 이웃사촌의 정을 느낄 수 있었고, 동네 전체가 활기가 돌았습니다.

 

하지만 점차 개인주의로 사회가 발전해가면서 아나바다 장터도 없어지고 이웃들간의 교류가 없어지면서 이웃사촌이라는 개념도 없어지고... 이웃간의 정도 없어지면서 "1인분" 이라는 것에 중점이 되가는 사회에서 사람들간의 감정과 이야기가 오가는 곳이 어디에 있을까? 를 고민하다 

 

전통시장을 떠 올리게 되었습니다.

혹시 여러분들은 현재 거주하는 곳에 전통시장 어디에 있는지 아시나요? 더 나아가 오일장이라고 들어보셨나요?

 

또한 요즘 트렌드로 할매니얼( 할머니 + 밀레니얼 ) 이라고 불립니다. 전통 디저트에 관심이 높아면서 약과, 떡, 한과 등 많은 부분이 재조명받고 있습니다. 이렇게 다시금 전통 디저트가 재조명을 받으면서 전통시장에 대해 관심도 높아졌지만, 막상 내 근처 전통시장이 어딨는지 모르는 경우가 많고 이러한 전통시장 위치 찾기는 인터넷 홈페이지에 있어 불편한 부분이 있었습니다. 이렇게 만들게 된 앱이 

저잣거리  앱입니다.

 

 

 

핵심 기능 보고 가실게요 ~  

 

  • 전국 전통시장 위치 및 지역별 필터링 기능 
  • 클러스터 어노테이션을 사용하여 어노테이션 그룹화 구현
  • 커스텀 어노테이션 뷰 구현
  • Pin 선택했을때 해당 시장 정보 및 기록 가능 
  • 지도 축척에 따라 어노테이션 생성
  • Realm을 사용하여 데이터 저장

 

앱을 만들고 나서 느낀점  보고 가실게요~

 

본격적으로 앱을 만들기 시작하면서 공수산정을 평소 구현 시간의 2.5 ~ 3배를 정했습니다.  그 이유는 데일리 과제를 하면서 구현하는데만 집중했는데 이번에 앱을 만들면서 업데이트를 지속적으로 할 것이기 때문에 가독성과 리팩토링이 쉽게 구현하는것을 목표로 했습니다.  그렇기 때문에 한줄의 코드를 작성하더라도 "내가 한 달 후에 봐도 이해가능한 코드"를 작성하는데 힘썼습니다. ( 많이 노력했지만... 아직도 갈길이 멀다 ㅠㅠ )

그리고 이번 3기에는  오프라인과 온라인을 병행 했는데, 전 오프라인으로 매일 출근?했습니다. 오프라인에는 항상 멘토님들이 계셨고, 구현하려는 기능에 대해 5 ~ 7시간씩 붙잡고 있을때 멘토님께 피드백을 받을 수 있었고, 멘토님들이 업무를 보시다가 지나가시면서 "이렇게 해보는건 어떨까요?" 라는 소스를 던져주셔서 기능 구현하는데 정말 많은 도움이 되었습니다. Shout out Hue, Jack, Kokojong, Bran 그리고 팀원들!

( 앱의 전체적인 플로우를 짧은 시간에 캐치하시고 설명해주시는데... 저도 열심히 공부해서 그런 역할을 하고 싶었습니다 ㅎㅎ )

 

앱 출시하면서 cs수업과 git 수업도 진행했고 학생들이 과부화 걸리지 않게 짧은 시간에 때려 넣어주시고... 또한 앱 출시 기간에 수업시간을 오전 10시로 변경하여 학생들에게 조금이라도 더 쉴 수 있게 배려해주셨습니다. ( 어느새 배운 개념이 스며들어와 버렸다... )
늦은 새벽 시간까지 피드백을 주시고 누구보다 일찍 출근하시는 ( 새벽 5시 출근...Hue님, Jack님 그저 빛...)  멘토님들을 보면서... 진짜 아낌없이 주는 나무 같다고 느꼈습니다. 

 

 

아쉬웠던점 

2달간 정말 많은 기술을 짧은 시간에 배웠지만... 이번 앱 출시했을때 많이 활용하지 못한거 같아서 아쉬운점이 남았습니다. 특히 출시하기 전에 배운 DiffableDatasource와 CompositionalLayout을 시도했지만 제대로 활용하지 못해서 기존에 사용하던 UITableView와 UICollectionView를 사용했습니다. 그래서 지속적으로 부족한 부분을 공부해가면서 활용하지 못했던 기술을 업데이트 하면서 앱을 더욱 풍성하고 사용자가 사용하기 편한 앱으로 발전해나가도록 하겠습니다.

 

 

 


아래는 앱을 구현하면서 겪었던 트러블 슈팅입니다

 

 

트러블 슈팅 

전통시장 API를 Realm에 저장할때 MainThread에서 작업이 이뤄졌을때 대략 1500개 가량의 데이터를 Realm에 저장하는데 걸리는 시간은 대략 9 ~ 11초 정도 걸렸고, 그동안 사용자의 터치 이벤트를 받을 수 없었음 

  • 해결 방법 : DispathGroup DispatchQueue.global()을 사용하여 데이터를 한번에 모으고 다른 쓰레드에서 저장하도록 구현

 

     1. API 데이터를 DispathGroup으로 묶는 작업 - 공공데이터 포털에서 한번의 API 콜로 최대 100개까지밖에 얻을 수 없어서 총 16번의 반복을 거쳐 데이터 저장했음

class MarketAPIManager {
    static let shared = MarketAPIManager()
    
    private init() { }
    
    let realm = try! Realm()
    
    var pageCount = Array(1...16)
    
    let realmManager = RealmManager()
    
    var aa: [Item] = []
    
    var marketList: TraditionalMarket = TraditionalMarket(response: Response.init(body: Body.init(items: [], totalCount: "", numOfRows: "", pageNo: "")))
    
    let group = DispatchGroup()
    
    func request() {
        for page in pageCount {
            group.enter()
            NetworkManager.shared.request(api: .marketInfomation(page: "\(page)")) { [weak self] response in
                
                guard let self else { return }
                marketList.response.body.items.append(contentsOf:response)
                group.leave()
            }
        }
        
        group.notify(queue: DispatchQueue.main) { [weak self] in
            guard let self else { return }
            
            self.realmManager.addDatas(markets: self.marketList.response.body.items)
            print("마켓 이름들 : \(self.marketList.response.body.items.count)")
        }
    }
}

 

 

 

     2. Realm이 DispatchQueue.global()을 사용해서 비동기로 저장

    func addDatas(markets: [Item]) {
        let latitudeZeroFilterdMarket = markets.filter { $0.latitude != ""}
        DispatchQueue.global().async {
            let realm = try! Realm()
            let traditionalMarkets = latitudeZeroFilterdMarket.map {
                TraditionalMarketRealm(marketName: $0.marketName, marketType: $0.marketType, loadNameAddress: $0.loadNameAddress, address: $0.address, marketOpenCycle: $0.marketOpenCycle, publicToilet: $0.publicToilet, latitude: $0.latitude, longitude: $0.longitude, popularProducts: $0.popularProducts, phoneNumber: $0.phoneNumber)
                
            }
            
            let allOfTraditionalMarket = traditionalMarkets + self.userDirectAddMarket()
            
            try! realm.write {
                realm.add(allOfTraditionalMarket)
            }
        }
        
    }

 

Realm은 무결성 및 일관성을 유지하기 위해 다른쓰레드에서 이벤트가 들어오는것을 지양하고 있습니다.

공식문서를 보면  DB에 락을 사용한다고 적혀있습니다.

 

하지만 이번 프로젝트의 경우 단지 저장만 하는 경우이기 때문에 비동기로 저장이 가능 했고, 비동기로 저장되는 동안 MainThread에서는 단지 Read만 하고 있기에 충돌 없이 적용 할 수 있었습니다. 

 

 

TimeInterval을 사용해서 결과를 비교해보았습니다.

왼쪽 사진 비동기 작업 전, 오른쪽 사진 비동기 자겁 후

 

2023.10.30 수정 ---> Realm에 비동기 처리를 해주는 메서드가 있었습니다...

 

Realm 공식문서 출처

이걸 왜 이제야 알았는지... Realm에 비동기로 저장하려고 3일 꼬박 했었는데... 리팩토링한 코드입니다.

    func addDatas(markets: [Item]) {
        let latitudeZeroFilterdMarket = markets.filter { $0.latitude != ""}

        realm.writeAsync {
            let traditionalMarkets = latitudeZeroFilterdMarket.map {
                TraditionalMarketRealm(marketName: $0.marketName, marketType: $0.marketType, loadNameAddress: $0.loadNameAddress, address: $0.address, marketOpenCycle: $0.marketOpenCycle, publicToilet: $0.publicToilet, latitude: $0.latitude, longitude: $0.longitude, popularProducts: $0.popularProducts, phoneNumber: $0.phoneNumber)
                
            }
            
            let allOfTraditionalMarket = traditionalMarkets + self.userDirectAddMarket()
            
            self.realm.add(allOfTraditionalMarket)
        }
    }

 

 

MapViewController의 비대해진 코드량 -> CustomView를 만들고 Viewmodel 접목

 

  • MapViewController에 많은 기능이 있다보니 코드량이 700줄 정도였기에 리팩토링이 필요했습니다. CustomView에 CLLocation과 Mapkit를 코드 분리를 하였고, Viewmodel을 활용하여 가독성과 기능 분리에 했습니다.

  • CustomView로 리팩토링후 데이터 전달을 위해 delegate completion을 활용하여 코드의 가독성을 높였습니다.

 

 

 

Alamofire URLRequestConvertible를 활용한 네트워크 통신

  •  공공데이터 포털 전통시장 API 와 네이버 이미지 API 를 활용하기 때문에 Network파일에 따로 처리를 해주었습니다. 코드의 간결함과 기능 분리를 위해 Router 패턴을 활용하여 코드 정리 했습니다. 그렇기에 Network 파일에는 문자열이 들어가지 않게 되었고, 가독성이 증가했습니다. 

 

Router 파일의 일부분을 보여주자면 

Enum으로 전통시장 API와 네이버 이미지 API를 구분하고 EndPoint와 Header, Parameter의 방식으로 설정했습니다. 

 

 

Map의 축척에 따라 해당 영역에 포함된 Realm 데이터 필터링하기 

  • map의 축척에 따라 해당 영역의 Realm 데이터만 불러오기 위해서는 일단 map의 최소 최대 span값을 구해야 했습니다. 현재 map의 보여지는 영역에서 상,하,좌,우 span 값을 구했습니다.

  • 상, 하, 좌 , 우 값으로 나온 데이터를 기준으로 해당 값의 사이값에 있는 것들만 가져오기 위한 필터링은 Realm에서 제공해주는 filter메서드를 이용했습니다.  Realm에서는 필터링을 할때 where  filter를 제공하는데 filter가 속도 측면에서 더 빠르기 때문에 사용했습니다.

  • Realm에서 filter를 사용하면 BETWEEN을 사용하여 Latitude의 상, 하 값 사이에 있는 Realm 데이터를 필터링 적용함