SniffMEET. 프로필 이미지 다운샘플링과 썸네일 분리하기

개요 사용자가 고해상도 사진을 그대로 업로드하면, 앱에서는 훨씬 작은 크기로만 이미지를 표시하는데도 원본 이미지를 계속 전송하고 저장하게 된다. SniffMeet에서도 프로필 이미지는 홈 화면의 프로필 카드나 메이트 리스트의 작은 썸네일로 표시되는 경우가 대부분이었다. 그런데 원본 이미지를 그대로 업로드하고, 목록에서도 같은 이미지를 다시 내려받고 있었다. 불필요한 네트워크 사용량과 메모리 사용을 줄이기 위해 두 가지를 적용했다. 업로드 전에 프로필 이미지를 앱에서 필요한 크기로 다운샘플링한다. 메이트 리스트에서는 원본 프로필 이미지가 아니라 별도의 썸네일 이미지를 내려받는다. 다운샘플링 기준 정하기 이미지를 어느 크기까지 줄일지 먼저 정해야 했다. 기준은 앱에서 프로필 이미지가 가장 크게 표시되는 홈 화면의 프로필 카드로 잡았다. ...

January 17, 2025

SniffMEET. OSSignposter로 앱 내부 작업 구간 측정하기

들어가며 성능 개선을 하려면 먼저 어떤 작업이 얼마나 걸리는지 볼 수 있어야 한다. CPU 사용량만 보면 어느 시점에 부하가 생겼는지는 알 수 있지만, 앱 내부의 어떤 작업이 그 구간에 실행됐는지는 바로 드러나지 않는다. 그래서 Instruments에서 앱 내부 작업 구간을 이름으로 확인할 수 있도록 OSSignposter를 래핑해보기로 했다. 어디에 붙일까? 처음에는 측정용 타입을 따로 만들지, 기존 SNMLogger에 붙일지 고민했다. private static let logger: Logger = Logger( subsystem: "SniffMeet", category: "SNMLogger" ) 별도의 SNMBenchMarker 같은 타입을 만들 수도 있었지만, 이미 프로젝트에 로그 시스템이 있었기 때문에 기존 SNMLogger에 통합하는 쪽을 선택했다. ...

January 14, 2025

SniffMEET. NI·MPC 프로필 드랍 흐름 소유권 고민하기

들어가며 SniffMeet의 프로필 드랍 기능은 주변 사용자를 찾고, 기기 간 연결을 만들고, 거리와 방향 조건을 만족했을 때 프로필을 전달하는 흐름이다. 겉으로 보면 “주변 사람 찾기"와 “프로필 보내기"를 나누면 될 것 같았다. 하지만 실제 흐름을 펼쳐보니 MPC, NI, 유즈케이스, 인터랙터가 서로 맞물려 있었고, 단순히 기능 이름으로 유즈케이스를 나누기 어려웠다. 이 글은 프로필 드랍 흐름을 유즈케이스로 나누면서 고민했던 지점을 정리한 기록이다. 기존에 생각한 흐름 당시 프로필 드랍은 대략 다음 순서로 동작한다고 봤다. ...

January 14, 2025

SniffMEET. Instruments로 성능 개선 후보 찾기

들어가며 성능 개선이 필요해 보이는 지점을 감으로 고르지 않기 위해, 먼저 SniffMeet의 주요 흐름에서 CPU와 네트워크 사용량을 훑어봤다. 이 측정만으로 병목 원인을 확정할 수는 없다. 다만 어떤 화면과 기능을 먼저 의심해야 하는지, 이후 리팩토링 후보를 정하는 기준으로는 충분했다. CPU 사용량 측정 Instruments의 Time Profiler로 주요 사용자 흐름을 따라가며 CPU 사용량이 튀는 구간을 확인했다. 측정 중 비교적 사용량이 높았던 지점은 다음과 같았다. 시점 동작 관찰 0:26 프로필 입력 뷰 로드 CPU 70% 0:53 텍스트 필드 입력 CPU 70% 1:44 사진, 닉네임 입력 뷰 로드 CPU 70% 2:08 포토피커 CPU 90% 3:00 등록 완료 버튼 터치 CPU 100%, 약 0.7초 유지 3:28 메이트 리스트 뷰 로드 CPU 50% 3:41 산책 요청 보내기 터치 CPU 80% 4:10 지도 로드 CPU 95% 5:05 요청 보내기 CPU 100%, 약 0.2초 6:36 MPC 연결 CPU 100%, 약 1.2초 특히 회원가입 완료 버튼을 눌렀을 때와 MPC 연결 시점이 눈에 띄었다. 회원가입 완료 시점에는 메인 스레드에서만 작업이 몰리는 것처럼 보였고, MPC 연결 시점에는 MultipeerConnectivity 관련 스레드가 지속적으로 동작했다. ...

January 7, 2025

SniffMEET. 여러 VIPER 구현에서 Router 구조 비교하기

VIPER에서 Router가 화면 전환을 담당한다는 원칙은 분명하다. 하지만 현재 화면을 어떻게 참조하는지, 다음 모듈은 누가 만드는지, AppRouter가 반드시 필요한지는 구현마다 달랐다. SniffMEET의 화면 구조를 정하기 전에 여러 VIPER 프로젝트를 비교하며 Router의 책임 범위를 확인했다. 무엇을 확인했나 분석 기준은 다음 세 가지였다. Router가 현재 ViewController를 어떻게 참조하는가 화면 전환 시 다음 VIPER 모듈을 누가 조립하는가 여러 모듈에 걸친 전환을 AppRouter가 담당하는가 구현 비교 구현 모듈 조립 화면 전환 AppRouter iOS-Viper-Architecture WireFrame의 정적 메서드 출발 View를 인자로 전달 없음 ios-architecture 정적 팩토리 메서드 전환 사례가 적음 없음 Viperit 프레임워크가 모듈을 관리 모듈이 직접 자신을 표시 AppModules로 모듈 관리 iOS-Architecture-Sample 모듈별 생성 앱 단위 Router가 화면 계층 관리 싱글톤 AppRouter 구현은 달랐지만 공통점도 있었다. View나 Presenter가 직접 다음 화면을 생성하지 않았고, 화면 조립과 전환을 UI 바깥의 객체로 분리하고 있었다. ...

November 12, 2024

Software Engineering. VIPER 아키텍처

VIPER란 VIPER는 화면을 View, Interactor, Presenter, Entity, Router로 나누는 아키텍처 패턴이다. 각 객체의 역할과 의존 방향을 제한해 UI, 비즈니스 로직, 화면 전환을 분리한다. 구성 요소 View 화면을 그리고 사용자 입력을 Presenter에 전달한다. Presenter가 전달한 값을 UI에 반영하며, 비즈니스 로직은 직접 처리하지 않는다. protocol ExampleViewProtocol: AnyObject { func displayMessage(_ message: String) func displayError(_ message: String) } final class ExampleViewController: UIViewController { var presenter: (any ExamplePresenterProtocol)? override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() } @IBAction private func buttonTapped(_ sender: UIButton) { presenter?.handleButtonTapped() } } extension ExampleViewController: ExampleViewProtocol { func displayMessage(_ message: String) { label.text = message } func displayError(_ message: String) { errorLabel.text = message } } Presenter View의 입력을 해석해 Interactor나 Router에 작업을 요청한다. Interactor가 반환한 Entity는 화면에 표시할 값으로 가공해 View에 전달한다. ...

November 11, 2024

SniffMEET. UITabBarController VIPER 컨테이너로 다루기

VIPER 모듈을 사용하더라도, 직접 탭 바를 구현하지 않는 이상 UITabBarController는 사용해야 한다. 일반적인 VIPER 화면처럼 View, Presenter, Interactor, Router를 모두 갖춘 독립 모듈로 보기엔 애매한 것 같다. 특히 Interactor는 여기서 아무 의미가 없어보이고, Router는 UITabBarController에 내장되어 있으니까… 모듈 빌더 router, interactor를 생성하고 이를 가지고 있는 presenter를 생성해서 뷰에 집어넣는 방식 HomeModuleBuilder는 Home 화면에 필요한 VIPER 구성 요소를 한 곳에서 조립한다. ViewController를 만들고, Router와 Interactor를 생성한 뒤 Presenter에 주입한다. 마지막으로 NavigationFactory를 통해 화면을 UINavigationController로 감싸서 반환한다. ...

November 11, 2024