본문 바로가기

내일배움캠프 안드로이드 3기

[TIL] 24.04.30 앱 개발 숙련 주차 팀 프로젝트 회고

 

 저번 주는 내내 팀 프로젝트를 진행했고, TIL을 거의 작성하지 못했다. 그래서 과정부터 소감까지 한 번에 결산 회고로 작성하기로 했다. 

 

 

📝진행과정

 

 

 이번에 구현할 앱은 연락처 앱이었고, 각 화면 별로 몇 가지 상세한 요구사항을 염두에 둔 채로 인원을 배분했다. 와이어 프레임을 대강 작성해 두고, 일단 필수 기능들부터 나누어서 작업하기로 했다.

 

 

 위의 붉은 사각형으로 하이라이트 된 부분이 처음 계획했던 팀 플랜이다. 수요일까지 필수 기능을 구현해서 PR을 진행하고, 목요일에 코드 리뷰 및 리팩터링을 마무리해 병합 후 추가할 기능을 선정해 금요일까지 마무리할 요량이었다. 물론, 계획대로 흘러가지 만은 않았는데, 이번 프로젝트는 우여곡절이 좀 있었다.

 일단 이 캠프에는 아직 숙련도가 부족해서, 본인이 어떤 부분을 개발한다고 했을 때 얼마나 걸릴지 예측하고, 진척도를 객관화시켜 파악하는 것을 힘들어하시는 분들이 있다. 그래서 플랜이 어느 정도 딜레이 되는 건 예상되는 범주였는데, 이번에는 케이스가 좀 달랐다.

 

 수요일에 팀원 중 한 분이 열이 난다며 조퇴를 하셨는데, 목요일에도 병원에 가신다고 연락이 잘 안 됐다. 그리고 잠깐 연락이 닿았을 땐, 금요일에 면접이 있어 통으로 출석하지 않는다는 통보와 함께 본인 작성한 내용을 새 브랜치에 올리겠다는 말만 하고 그 뒤엔 연락이 전혀 닿지 않았다.

 팀 작업을 위한 오픈 카톡에서 몇 번이나 언급하며 PR을 요청드렸는데도(팀 컨벤션 및 브랜치 룰로 2명 이상의 승인을 받아 merge 하도록 함), 메시지를 확인하고도 묵묵부답으로 일관하셨고, 이는 금요일까지도 계속 이어졌다. 이 때문에, 병합을 해도 기능 동작 테스트엔 어려움이 있었고, 올려두셨다는 브랜치를 확인했을 때도, Conflict가 생긴 커밋들을 전혀 해결하지 않고 아예 모든 파일을 빈 브랜치에 복사 붙여넣기 한 형태로 올라와 있었다.

 당연히 그걸 그대로 강제 병합 시도했다간 거의 코드의 모든 부분에서 Conflict가 발생할 것이었고, 코드만 재사용 하기에도 마치 여러 코드들을 짜깁기한 듯, 좋은 퀄리티의 코드가 아니었다.

 

 하는 수 없이, 금요일에 매니저님께 이 사실을 알렸고, 우선 프로젝트의 마무리를 위해서 해당 파트에 인원을 재분배해 다시 작업하기로 했다. 꽤나 어려운 상황이었지만, 다른 팀원분들이 적극적으로 참여해 주셔서 그나마 큰 결함 없이 프로젝트를 마무리할 수 있었던 것 같다. 나름 중간중간 선택 구현 기능들을 조금씩 도입해서 넣어두기도 했고, 막판에 어려울 수 있지만 다른 팀원분들도 새로운 기능 도입에 도전해 주셔서 선택 구현 기능도 웬만큼 넣을 수 있게 됐다.

 

 처음에 다양한 확장 가능성을 생각해서, 앱의 구조와 더미 데이터를 받아오는 Object의 CRUD 메소드를 디자인해뒀는데, 꼬인 일정으로 인해 이를 충분히 활용하지 못해 아쉬운 마음은 있다.

 

📝구현했던 것들

 내가 핵심적으로 구현했던 것들은 연락처 리스트를 노출하는 Fragment의 로직과, 연락처 상세 페이지의 로직 및 UI, 그리고 더미 데이터에 상호작용하는 CRUD 메소드들이었다.

 

 

 RecyclerView의 LayoutManager와 멀티 ViewType을 활용해서 리스트-그리드 형태로 각각 목록이 노출될 수 있게 했다. 그리고 상세 페이지에서 북마크 하게 되면 당연히 더미 데이터에 반영되고, 리스트를 노출하던 Fragment에도 바로 반영이 될 수 있게 했다.

 

 

 그리고 Intent를 활용해 바로 통화나 SMS로 접근할 수 있게 로직을 작성하고, CollapsingToolBar를 이용해 UI 디테일도 연습해 봤다. 예전에 MotionLayout을 이용해서 저런 UI 변화를 구현하거나, 혹은 Compose를 이용해 UI를 짰을 때 변화를 넣어본 적은 있는데, CollapsingToolBar를 써본 것은 또 처음이었다. 생각보다 xml 구조의 깊이가 깊어져서 적용할 때 유의할 필요가 있을 것 같다.

 

 

...


 fun getUserData() = userData

    fun addContactData(newData: ContactData): Boolean {
        _totalContactData = _totalContactData.toMutableList().apply {
            add(newData.copy(id = idNum, isFavorite = false))
        }.toList()
        idNum++

        return true
    }

    fun updateContactData(newData: ContactData): Boolean {
        var flag = false
        _totalContactData = _totalContactData.map {
            if(it.id == newData.id) {
                flag = true
                newData
            }else {
                it
            }
        }
        return flag
    }

    fun deleteContactData(id: Long): Boolean {
        val itemInd = _totalContactData.binarySearchBy(id){
            it.id
        }

        return if(itemInd>=0) {
            _totalContactData = _totalContactData.filter {
                it.id != id
            }
            true
        } else {
            false
        }
    }
    
    ...

 

 

 그리고 더미데이터에 접근하는 코드도 열심히 디자인했다. 비록 실제 서버나 DB에서 받아오는 데이터는 아니지만, 어느 정도 Data 레이어 정체성을 가미해서 데이터의 불변성에 신경을 많이 썼다. 또 데이터의 수가 많아졌을 때를 생각해 삭제 과정은 이진 탐색을 적용하도록 했다. 수정은 설계 상, 확실히 데이터가 존재하는 상황에서만 발생하며 그래서 한 번의 선형 탐색으로 끝나게 했지만, 삭제는 앱에 적용되진 못했지만, 로직은 이진탐색으로 지울 값이 존재하는 지를 우선적으로 검사해 불필요하게 선형 시간이 소모되는 것을 막으려고 했었다.

 

📝고민한 것

 다른 것보다도 내가 제일 고민했던 것은 Fragment 간 데이터의 전달 문제였다.

 

 

 Fragment의 데이터 전달은 여러 심플한 방법으로 해결될 수 있지만, 이번 과제에서는 몇 가지 제약사항이 있었다. 연락처 목록 및 마이페이지가 TabLayout 및 ViewPager2를 이용해 구현되어야 했고, 이 와중에 상세 연락처 페이지도 Fragment로 구현되어야 했다. 상세 연락처 페이지도 ViewPager에 넣어버리면 연락처 목록에서 받아와야 할 Bundle 데이터가 정상적으로 전달되지 않은 채 화면에 노출되는 등 여러 가지 문제가 생길 수 있었다.

 그리고 아직 MVVM 패턴 및 LiveData에 대한 학습이 진행되지 않은 팀원들도 있어, 이를 당장 도입하기도 힘들었다.

 

 그래서 위의 구조처럼 조금은 기형적인 구조로 앱을 디자인해야 했는데, MainActivity 내에서 FragmentContainerView에 MainFragment와 DetailFragment를 호스팅 하고, MainFragment 내에서 ViewPager를 이용해 ListFragment 및 MyPageFragment를 띄우는 구조였다.

 그러다 보니 DetailFragment나, 연락처 추가 다이얼로그에서 발생하는 데이터 셋의 변화 여부를 ListFragment까지 전달해 갱신하도록 할 필요가 있었고, 이를 계속 고민했던 것 같다.

 

 

 

 나는 그래서 이와 같은 구조를 제안했다.

 먼저 ListFragment에 어댑터의 아이템을 갱신할 수 있는 public 메소드 refresh()를 만든다. MainFragment에서는 ViewPager에 할당할 Fragment List를 생성해서 쥔 채로, 그 어댑터에 주입한다. 이렇게 되면 쥐고 있는 리스트를 통해서 노출되는 ListFragment 인스턴스에 접근 가능해지고, 당연히 ListFragment 내의 refresh() 메소드에도 접근할 수 있다. 이를 트리거 하는 requireRefresh() 메소드를 public으로 생성해 둔다.

 MainActivity에서는 Fragment의 id나 tag를 통해 특정 자식 Fragment에 접근 가능하므로, 당연히 MainFragment의 requireRefresh()가 호출 가능하다. 마지막으로 DetailFragment에선 호스팅하고 있는 부모 Activity를 호출 가능하므로, 해당 Activity를 MainActivity 타입으로 가져와 변화 발생 시, update()를 호출하게 한다.

 이렇게 하니, 예상한 대로 로직이 동작했고 프로젝트에 적용했다. 다이얼로그 쪽에도 비슷한 과정이 필요해 그쪽엔 콜백을 주입받게 해, 연락처 추가에 성공했을 때 트리거 되게 해 두고 MainActivity에서 해당 콜백 동작시 update()가 발생하게 해 해결했다. 지금 와서 생각하는 거지만, DetailFragment쪽도 콜백으로 주입하는 게 더 괜찮은 코드가 되었을 것 같기도 하다.

 

 

 

 이 외에도 프로젝트 내내 여러 가지 오류나 버그, 형상관리 이슈 등이 있었지만, 내가 예전에 겪어봤던 문제들이 많아서 원만하게 해결하는데 많은 도움을 드릴 수 있었던 것 같다. 많이 부딪혀보고, 경험해 보는 것의 중요성을 또 한 번 느끼게 된다.

 그리고 이번에 겪은 팀원의 무단이탈 이슈는 꽤 치명적이었고 당황스러웠는데, 대학교 때 겪었던 수많은 조별과제가 백신이 되었는지(...) 어른스럽지 못한 행동에 어른스럽게 잘 대처한 것 같다. 책임감에 대한 것도 다시금 떠올려보게 되는 계기가 아니었나 생각한다.