본문 바로가기
프로젝트

[개인 프로젝트] SpotChat 회고

by dsungc 2024. 12. 7.

⭐️  기획 단계

🤔  주제 선정

유럽 여행 경험을 바탕으로, 사람들이 함께 동행할 수 있는 플랫폼이 있으면 좋겠다는 필요성을 느꼈습니다.

특히 유럽 여행지에서 동행을 구하는 활동이 ‘유랑’과 같은 온라인 커뮤니티에서 주로 이루어진다는 점에서 착안하여, 이를 더욱 간편하게 연결할 수 있는 모바일 앱으로 구현하고자 했습니다.

또한, 이러한 활동이 주로 젊은 연령층 사이에서 이루어지고 있음을 고려했을 때, iOS 앱으로 출시가 된다면 해당 타겟층에게 높은 메리트를 제공할 것으로 판단했습니다.

 

✅  개발 방식

지난 프로젝트에서는 처음부터 모든 기능을 완벽히 설계하고 진행하는 방식으로 작업을 진행했습니다. 그러나 프로젝트 후반부에 새로운 요구사항이 생기거나 수정이 필요할 때, 앱 전반을 변경해야 하는 상황이 발생했습니다. 이러한 변경은 일정 지연으로 이어질 가능성이 높았으며, 결과적으로 일부 기능을 포기해야 하는 선택을 해야 했습니다.

 

저는 기능을 포기하는 방향으로 해결책을 선택하여 일정 지연을 방지하고, 남은 기능의 완성도를 높이는 데 집중했습니다.

또한 초기 설계의 중요성과 동시에 유연한 개발 방식의 필요성을  깨닫게 되었습니다.

 

이러한 경험을 바탕으로, 이번 프로젝트에서는 최소한의 핵심 기능만 구현하여 1차적으로 각 화면을 완성한 뒤, 필요에 따라 추가적인 기능과 성능 개선을 반복적으로 진행하는 방식을 선택했습니다.


대표적인 예시로 직접 눈으로 확인하며 작업하는 것을 선호하기 때문에, 가장 기본적인 화면인 게시물 업로드 화면부터 구현하기 시작했습니다. 이 화면을 통해 점진적으로 개선해 나가는 과정을 설명드려볼까합니다.

 

 

1. 최소한의 구현 사항만으로 시작

 

초기에는 게시물 업로드 화면에 필요한 최소한의 구현사항만 포함하여 간단하게 구성했습니다.

핵심은 복잡한 기능 구현보다는 서버와의 통신이 정상적으로 이루어지는지를 우선 확인하는 것이었습니다.

 

최소 구현사항

- 이미지 선택을 위한 이미지 피커

- 게시물의 제목과 내용을 입력하는 텍스트 필드

- 해시태그를 추가하기 위한 텍스트 필드

 

이 단계에서는 간단한 구조를 유지하면서, 빠르게 결과를 확인하고 주요 흐름을 파악하는 데 집중했습니다.


 

2. 역할에 따른 객체 분리

 

구현이 진행되면서 화면에 필요한 기능들이 점점 더 추가되었고, 코드가 복잡해지기 시작했습니다.

특히, ViewController(VC)의 역할이 지나치게 비대해지면서 코드의 유지보수성이 떨어지는 문제를 찾아볼 수 있었는데요..!

ViewModel을 활용하고 있음에도 VC가 너무 커졌다고 느껴, 이를 해결하기 위해 역할에 따라 객체를 분리하는 작업을 시작했습니다.

 

이미지 피커 객체

- 이미지를 선택하는 기능은 다른 화면에서도 재사용 가능성이 있다고 판단하여, 독립된 객체로 설계

DataSource 관리 객체

- 컬렉션뷰 또는 테이블뷰와의 연결을 담당하는 객체를 분리했습니다.

- 이 객체는 데이터 변화에 대한 정보를 받고, 변화에 따라 셀 UI를 동적으로 업데이트하도록 설계

중개 시스템(BindManager)

- ViewController와 각 요소 간의 데이터 흐름을 관리하기 위해 BindManager라는 중개 시스템을 도입

- BindManager는 ViewController와 다른 객체 간의 의존성을 낮추고, 데이터 흐름을 중앙에서 관리함으로써 VC 간소화


위의 객체들을 바탕으로 물류 센터 시스템과 유사하게 설계하였는데 흐름은 다음과 같습니다.

 

우리가 쿠팡에서 물건을 주문한다고 가정했을 때, 

 

a. 사용자가 물건을 구매 요청

- 사용자가 특정 상품을 주문

 

b. 허브에서 상품 정보 확인 및 처리

- 물류 센터의 허브(중앙 분배 센터)는 사용자 요청을 받아 상품 정보를 확인

- 허브에는 다양한 상품을 저장해두고나 외부로부터 상품을 요청하고 들여오는 작업 진행

 

c. 상품의 유무에 따라 배송 진행

- 허브가 상품의 유무를 확인한 뒤, 사용자가 원하는 물건을 준비하여 배송을 진행

 

이를 현재 프로젝트에 대입을 해보면

 

a. 사용자가 물건을 구매 요청 → VC

 

- 물류 시스템: 고객이 특정 상품을 주문하는 과정으로, 물류 시스템에서 첫 번째 요청이 시작

- 프로젝트: VC는 사용자의 입력(게시물 제목, 내용, 이미지 등)을 받아 요청을 시작

                  ex) 게시물의 제목과 내용을 입력하거나, 이미지를 선택하여 서버에 업로드 요청

 

b. 허브에서 상품 정보 확인 및 처리 → BindManager

 

- 물류 시스템: 중앙 허브에서 주문 요청을 받아 필요한 상품 정보를 확인하고, 외부에서 상품을 가져오거나 기존 데이터를 사용해 처리

- 프로젝트:

    - BindManager는 ViewController와 다른 객체들(이미지 피커, DataSource, ViewModel 등) 간의 데이터 흐름 관리

    - 데이터 흐름을 한곳에서 조율하여 중복 요청을 방지하고, ViewController의 복잡성 감소

       ex)이미지 데이터를 받아 서버로 전달하거나, 입력 데이터를 바인딩해 ViewModel로 전달

 

c. 상품의 유무에 따라 처리 → Repository와 Network

 

- 물류 시스템: 허브는 상품의 유무를 확인한 뒤, 상품이 있다면 준비된 상품 배송 준비, 없다면 외부 새송사로부터 상품 매입

- 프로젝트

    - Repository: 로컬 데이터베이스의 정보를 확인하여 데이터를 저장하거나 

                              ex) 게시물 업로드 요청을 서버로 보내고, 결과를 ViewModel로 반환

    - Realm: 데이터 저장을 담당하여, 오프라인 환경에서도 데이터를 사용할 수 있도록 관리

    - Network: ViewModel로 부터 조건을 전달받아 해당 조건에 부합하는 정보 반환 >> Post 뷰의 경우 해당

                         ex) 게시물의 해시태그 리스트를 업데이트하거나, 사용자 위치를 기반으로 한 게시물 전달

    - Server: 네트워크 요청에 따라 필요한 데이터를 제공하는 공간

 

d. 상품 전달 → DataSource 관리 객체

 

- 물류 시스템: 배송 준비된 상품은 배송 차량에 적재되어 고객에게 전달

- 프로젝트

    - DataSource 관리 객체: 컬렉션뷰 / 테이블뷰와 연결하여 데이터를 UI에 표시하며, 데이터 변화에 따라 셀 UI를 동적으로 업데이트

                                             ex) 게시물 목록 등을 컬렉션뷰에 표시하고, 데이터가 업데이트될 때 UI를 실시간으로 갱신

 

 

3. 구현한 뷰가 잘 동작한다면 다음 뷰에 집중 

 

게시물 업로드 화면(PostView)을 먼저 구현한 뒤, 이를 기반으로 지도 화면(MapView), 사용자 정보 화면(UserView)을 구현하고, 게시물의 유무에 영향을 받지 않는 채팅 화면(ChatView) 을 가장 마지막 순서로 구현했습니다.

 

이 순서는 화면 간 의존성과 기능 우선순위를 고려하여 정한 것으로, 핵심 기능부터 확장 기능까지 점진적으로 구현해 나가는 방식을 따랐습니다.


🤔 고민한 지점

1. IQKeyBoardManager 라이브러리로부터의 탈출 계획

 

처음 이 라이브러리를 사용했을 때는 키보드와 관련된 고민을 덜 수 있다는 점에서 굉장히 매력적으로 느껴졌습니다. 하지만 프로젝트를 진행하면서, 생각보다 특정 UI와의 대응이 원활하지 않은 경우를 자주 마주하게 되었고, 이러한 제약이 점점 불편하게 다가왔습니다. 결국, 이번 프로젝트에서는 IQKeyboardManager를 사용하지 않고 직접 키보드 동작을 관리해보기로 했습니다.

 

KeyBoardManager 객체 주요 기능

 

a. 키보드 이벤트 감지 및 처리

- 키보드가 화면에 나타날 때와 사라질 때 이벤트를 감지하여, 필요한 동작(예: 뷰 이동, 레이아웃 조정)을 처리

 

b. 키보드 높이 제공

키보드가 나타날 때의 키보드 높이를 계산하여, UI를 적절히 조정할 수 있도록 데이터를 제공

 

c. 탭 제스처로 키보드 숨김 처리

화면을 탭하면 키보드를 숨길 수 있는 제스처를 추가해, 사용자가 쉽게 입력 모드 종료 가능

 

d. 터치 제외 기능

- 특정 뷰( 버튼, 드롭다운 메뉴 등 )를 추가해 탭 제스처 대상에서 제외

 

e. 이벤트 관리의 중앙화

앱의 여러 화면에서 반복적으로 작성해야 하는 키보드 이벤트 처리 코드를 한곳에 모아 관리

 

2. 실시간 채팅

 

채팅 UI를 설계할 때, 송신자와 수신자에 따라 채팅 메시지의 방향을 다르게 설정할지 같은 방향으로 할지 고민을 많이 했었습니다. 실제로 두 가지 방법 모두 잔디, 카카오톡, 텔레그램, WhatsApp 현재 상용중인 어플에서 많이 사용하기 때문인데요..

그래도 제가 가장 많이 사용하는 카카오톡의 UI를 본따서 만들기로 결정했습니다.

 

UIKit + SnapKit을 활용한 채팅 UI 구현

 

3. DataBase 설계

이 부분도 프로젝트를 진행해 나가면서 꽤 많이 바꾼 부분입니다.

 

Before. 모든 채팅 관련 정보를 단일 스키마에 저장

 

final class ChatData: Object {
    @Persisted(primaryKey: true) var chatRoomID: String 
    @Persisted var senderID: String // 메시지 작성자 ID
    @Persisted var senderNickname: String // 작성자 닉네임
    @Persisted var senderProfileImage: String? // 작성자 프로필 이미지
    @Persisted var messageContent: String? // 메시지 내용
    @Persisted var files: List<String> // 첨부된 파일 URL
    @Persisted var createdAt: String // 메시지 생성 시간
}

 

문제점

 

a. 데이터 중복 문제

- ChatMessagesender 필드에서 유저 정보(UserInfo)를 중복 저장

- 동일한 유저가 여러 메시지를 보낼 경우, 같은 유저 정보가 메시지마다 반복적으로 저장

b. 데이터 크기 증가

- 메시지 수가 많아질수록 중복된 유저 정보가 누적되며

c. 관리 복잡도

- 유저 정보를 업데이트할 경우(예: 닉네임 변경), ChatMessage에 포함된 모든 유저 정보 수정

 

 

After. ChatMessagesender 필드가 중복된 유저 데이터를 저장하지 않고, UserInfo 객체를 참조하도록 변경

 

final class ChatRoom: Object {
    @Persisted(primaryKey: true) var roomID: String
    @Persisted var userList: List<UserInfo>
    @Persisted var chatList: List<ChatMessage>
}

final class UserInfo: Object {
    @Persisted(primaryKey: true) var userID: String
    @Persisted var nickname: String
    @Persisted var profileImage: String?
}

final class ChatMessage: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var chatID: String
    @Persisted(indexed: true) var createdAt: String
    @Persisted var sender: UserInfo? // 메시지 작성자 ⭐️⭐️⭐️⭐️⭐️(참조)
    @Persisted var content: String
    @Persisted var files: List<String>
}

 

a. 데이터 중복 제거

- ChatMessagesender 필드에서 유저 정보를 직접 저장하지 않고, UserInfo 객체를 참조

- 동일 유저가 여러 메시지를 보낼 때, 유저 정보는 단일 인스턴스로 저장

b. 데이터 크기 감소

- 유저 정보가 메시지별로 중복 저장되지 않아, 데이터 저장 공간이 절약됨

c. 데이터 관리 용이

- 유저 정보 업데이트 시, 단일 UserInfo 객체만 수정하면 됨

ex) 닉네임 변경 → 모든 관련 메시지가 자동 반영

d. 관계형 데이터 설계

- ChatRoomUserInfo, ChatRoomChatMessage 간의 참조 관계를 통해 데이터 구조가 더 명확하도록 설계

 

4. 이미지 처리

초기에 이미지 캐싱을 구현할 때, NSCache와 FileManager를 조합하여 메모리와 디스크 캐싱을 직접 설계하는 방식을 고려했습니다.

하지만 프로젝트 요구사항과 개발 공수를 고려했을 때, Kingfisher가 다음과 같은 이유로 더 적합하다고 판단하여 선택했습니다.

 

Kingfisher를 선택한 이유

⭐️⭐️⭐️ 핵심 기능 구현에 더 많은 시간을 할애

 

a. 효율적인 메모리 및 디스크 캐싱

- Kingfisher는 메모리 캐시와 디스크 캐시를 결합하여 이미지를 효율적으로 관리

- 메모리에서 먼저 이미지 검색 후 없을 경우 디스크에서 탐색, 마지막으로 네트워크 요청을 보내는 계층화된 캐싱 로직을 기본 제공

- 직접 NSCache와 FileManager를 구현하는 것보다 간단하고 빠르게 적용 가능

비교

- NSCache: 메모리 캐싱만 지원, 디스크 캐싱은 별도로 구현해야 함

- Kingfisher: 메모리 및 디스크 캐싱을 통합 제공하며, 캐싱 전략(기간, 크기 제한 등)을 쉽게 설정 가능

 

b. 다운샘플링 지원

- NSCache를 사용할 경우, 별도의 이미지 리사이징 및 처리 로직을 추가로 작성해야 하지만, Kingfisher는 코드 한 줄로 해결 가능

- DownsamplingImageProcessor 를 활용하여 이미지 크기 최적화 및 메모리 사용량 감소

 

c. 검증된 안정성과 커뮤니티 지원

- iOS 개발자들 사이에서 널리 사용된 라이브러리이고, 레퍼런스가 풍부

- Kingfisher에 대한 설명

 


⭐️  회고

 

좋았던 점

 

이번 프로젝트와 이전 프로젝트의 가장 큰 차이점은 개발 방식의 변화라고 생각합니다.

이전에는 “완벽히 설계한 후 구현”하는 방식을 채택했지만, 이번 프로젝트에서는 최소한의 기능부터 구현하고 점진적으로 개선하는 접근 방식을 도입했습니다.

 

객체 분리와 같은 설계적 변화도 중요한 차이였지만, 이번 프로젝트에서 가장 눈에 띄는 변화는 CombineCocoa의 채택이라고 느꼈습니다.

 

처음에는 CombineCocoa 없이 모든 기능을 직접 구현하려는 계획이 있었습니다.

텍스트 입력 필드나 버튼과 같은 단순한 기능은 어떻게든 스스로 구현했지만, 세그먼트 컨트롤러 등 복잡한 UI 컴포넌트의 반응형 처리를 구현하는 데 시간이 과도하게 소요되는 문제가 발생했습니다.

 

이 문제를 확인한 후, CombineCocoa를 도입하여 공수와 효율성의 균형을 맞추는 데 집중했습니다.

CombineCocoa를 활용함으로써, 기존에 많은 시간과 리소스를 들여야 했던 반응형 UI 처리를 더 간결하게 구현할 수 있었습니다.

이로 인해 핵심 로직에 집중할 수 있는 여유가 생겼고, 프로젝트의 진행 속도도 한층 빨라졌습니다.

 

 

 

아쉬운 점

 

프로젝트에서 가장 아쉬운 점은 CombineCocoa의 도입입니다.

물론, CombineCocoa를 통해 프로젝트의 공수와 생산성을 높이는 데 성공했지만, 복잡한 UI 컴포넌트의 반응형 처리를 직접 구현해보지 못한 점이 아쉬움으로 남습니다.

 

프로젝트가 끝난 지금, 조금씩이라도 이 부분에 대해 직접 고민하고 구현해보는 시간을 가지려 합니다.

궁극적으로는 CombineCocoa를 삭제하고, 직접 구현한 로직으로 대체하는 것을 목표로 삼아, 반응형 프로그래밍에 대한 깊은 이해를 이뤄내고자 합니다.

 

 

또 하나의 아쉬운 점은 단위 테스트를 진행하지 못한 점입니다.

아직 한 번도 테스트 코드를 작성해 본 적이 없어 이에 대한 공수를 적절히 분배하지 못했던 것 같습니다.

특히, 객체를 분리하고 역할을 명확히 정의한 만큼, 단위 테스트를 통해 각 객체의 동작을 검증하고 앱 안정성을 확보하지 못한 점이 아쉽게 느껴집니다.