grep

iOS

해외숙소 홈 개편기

Louis(장지훈) / iOS Team여기어때

2024년 11월 6일

원문에서 보기 ↗

안녕하세요. 여기어때컴퍼니 iOS개발팀 루이스입니다.

이번 글에서는 해외숙소 홈 개편을 진행하는 이유와 그 과정에서 겪은 문제들, 그리고 고민했던 부분들에 대해 이야기해보려 합니다.

기존 해외숙소 홈은 웹기반으로 운영되어 왔습니다. 화면의 장점을 살려 서비스를 제공해왔지만 앱 개발팀 입장에서는 화면 관리의 어려움, 오프라인 기능 미지원, 사파리 브라우저 엔진 의존성, 그리고 네이티브에 비해 상대적으로 느린 성능 등 여러 문제들을 안고 있었습니다.

사용성 면에서도 개선이 필요했습니다. 예를 들어 ‘최저가 보상 숙소’ 영역에는 여러 도시의 숙소가 혼재되어 있어 특정 도시의 숙소를 찾기 어려웠고, 도시 정보가 제공되지 않아 사용자들이 숙소의 위치를 바로 알기 힘들었습니다. 또한 최근 검색 기록이 있을 경우 ‘최저가 보상 숙소’ 목록이 ATF (Above The Fold의 약자로 사용자가 첫 화면에서 즉시 볼 수 있는 콘텐츠를 뜻함) 아래로 내려가 스크롤을 해야만 확인할 수 있다는 불편함이 있었습니다.

이러한 문제들을 해결하고 성능과 사용성을 개선하기 위해 해외숙소 홈 화면을 네이티브로 개편하는 작업이 진행되었습니다.

새롭게 개편된 해외숙소 홈 화면

변수 관리의 고민

이번 프로젝트에서 가장 고민했던 부분은 ReactorKit의 State(혹은 ViewModel) 내에서 변수들을 어떻게 관리를 어떻게 할지 였습니다. 사실 이 문제는 이전부터 고민해왔던 부분이기도 합니다. State 에 변수를 무분별하게 선언해서 사용하다 보니, 시간이 지나면서 각 변수의 역할이 점점 모호해져 나중에 코드를 다시 볼 때 그 의미를 기억하기 어렵게 되는 경우가 자주 발생했습니다. 결국 내가 작성한 코드임에도 불구하고 다시 파악하는 데 시간을 할애해야 했고, 이 과정이 비효율적이라는 생각을 하게 됐습니다.

2022년의 내가 작성한 코드..

이런 문제들을 예방하기 위해 저희팀에서 기존부터 사용중인 코딩 컨벤션도 있긴 했습니다.

1. 의미 있는 변수 이름 사용

2. 필요한 곳에 적절한 주석 달기

3. 중복된 역할의 변수 제거

하지만 이러한 규칙들을 준수해도 반년, 일년이 지나면 잊히기 마련이라.. 이번에는 변수의 역할에 따른 분리(Separation of Concerns)와 계층적 관리(Hierarchical Management) 규칙을 새로 추가해 보았습니다.

위와 같이 데이터의 역할에 따라 변수들을 세분화하고 화면 내 UI 관련 변수를 AppModel에서 관리하도록 설정했습니다. 또한 AppModel을 화면 자체로 보고 각 화면 요소와 1:1로 매칭되도록 구성했습니다. 이로 인해 UI와 데이터 간의 연관성을 쉽게 파악할 수 있게 되었고 나중에 코드를 다시 볼 때도 이전보다 각 코드의 역할을 더 빠르게 이해할 수 있을 것이라 기대하고 있습니다.

추가로 이런 방식으로 구성하면서 예상치 못한 장단점도 생겼습니다. 단점부터 말씀드리자면 본격적으로 UI 를 구현하기 전에 AppModel의 구조를 고민하는 시간이 필요해졌다는 점입니다. 반면 이 과정을 통해 View와 어떻게 통신 할지에 대해 한번 더 설계하고, 고민해보며 프로그래밍 할 수 있는 시간이 생겨났다는 장점도 생겼습니다.

화면 노출 방식의 변화에 따른 이슈와 해결방안

다음으로는 UIModalPresentationStyle을 pageSheet로 설정하여 ViewController를 띄웠을 때 발생한 문제점과 해결 과정을 이야기해 보려 합니다.

이번 개편 작업에서 캘린더와 키워드 검색 화면의 Present 방식을 기존 fullScreen에서 pageSheet로 변경했는데, 이로 인해 해당 화면들을 Dismiss했을 때 부모 ViewController의 viewWillAppear가 호출되지 않는 문제가 발생했습니다. 이를 해결하기 위해 Delegate, Closure, NotificationCenter를 통해 부모 ViewController를 갱신하려 했지만, 이 방식은 너무 많은 화면을 수정해야 했고 QA 검증 범위 또한 넓어질 우려가 있었습니다. 고민 끝에 저는 캘린더와 키워드 검색 화면안에서 직접 문제를 처리하는 아래와 같은 방식을 선택했습니다.

override public func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    if modalPresentationStyle == .pageSheet {
        navigationController.topViewController?.viewWillAppear(true)
        navigationController.topViewController?.viewDidAppear(true)
    }
}

캘린더와 키워드 검색화면이 사라질 때, 즉 viewDidDisappear 호출 시점에 Parent의 viewWillAppear와 viewDidAppear을 직접 호출하는 방식입니다. 이 방식으로 수정 범위와 QA 검증 범위를 줄일 수 있었지만 단점도 분명히 존재했습니다.

혹시 저와 같은 방식을 택하신다면 위의 단점들도 꼭 고려해 주시는 게 좋을 것 같습니다.

연속적인 탭 이동으로 인한 데이터 불일치 문제와 해결 방안

다음은 여러 도시(지역)를 선택할 수 있는 탭 구조의 기획전 섹션에서 발생했던 문제와 해결 방법에 대해 설명드리려 합니다. 기획전은 사용자가 도시(지역) 탭을 선택할 때마다 API 를 호출하고, 응답 결과로 해당 지역의 숙소 리스트를 표시하는 구조입니다.

문제가 발생했던 시점은 API 응답을 받기 전에 다른 탭을 클릭하거나, 다른 기획전 섹션의 도시 탭을 선택했을 때, 현재 활성화된 카테고리가 아닌 이전에 선택했던 카테고리의 상품 리스트가 노출되는 현상이 나타났습니다.

원인은 API 응답 딜레이로 인한 것이었으며, 이를 해결하기 위해 두 가지 방법을 고려했습니다. 첫 번째는 이전에 요청 중이었던 API 호출을 취소하는 것이고, 두 번째는 이전 요청의 API 응답을 무시하는 것입니다.

저는 고민 끝에 두 번째 방법을 선택했습니다. 첫 번째 방법이 가장 이상적인 방법 이었지만 프로젝트가 중후반부에 접어든 상황에서 비즈니스 로직을 크게 변경하기 어려웠기 때문에 좀 더 간단한 두 번째 방법을 선택하게 되었습니다.

두 번째 방법의 코드 구현은 아래와 같습니다.

// 섹션별 취소 Subject 관리 (기존 API 호출 취소를 위해)
private var sectionCancelSubjects: [Int: PublishSubject<Void>] = [:]
func requestPromotionData(section: Int, tabIndex: Int) -> Observable<Mutation> {
    // 이전 API 호출 취소를 위해 해당 섹션의 Subject를 새로 만듦
    if let cancelSubject = sectionCancelSubjects[section] {
        cancelSubject.onNext(()) // 기존 API 호출 취소 신호
    }
    let newCancelSubject = PublishSubject<Void>()
    sectionCancelSubjects[section] = newCancelSubject
    return requestPromotionApi(section: section, tabIndex: tabIndex).take(until: newCancelSubject) // 새로 만들어진 Subject로 취소 제어
}

핵심은 RxSwift의 takeUntil을 활용하여 이전 응답들을 받지 않도록 하는 것입니다. 지역 탭을 클릭하게 되면 requestPromotionData 함수가 호출됩니다. 여기서 각 기획전(섹션) 별로 요청 중인 API가 있는지 확인하고, 있다면 해당 섹션의 이벤트를 방출하여 (onNext) 호출 중인 API의 응답을 무시합니다. 그 이후 선택한 지역에 맞는 API를 호출하게끔 만들었습니다.

긴 글 마치며

이와 같은 다양한 최적화와 개선 작업을 통해 이번 해외숙소 홈 개편 프로젝트는 사용자 경험을 높이고, 성능을 최적화하는 방향으로 진행되었습니다. 웹에서 네이티브로 전환하면서 얻은 이점도 있었고, 동시에 데이터 구조를 계층화하고 역할별로 분리하여 가독성과 유지보수성을 높일 수 있었습니다. 물론 아직 보완할 부분도 남아 있어 앞으로도 개선이 필요하겠지만, 이번 개편을 통해 얻은 경험과 교훈이 이후 프로젝트의 기반이 되리라 확신합니다.

앞으로도 사용자 피드백과 실제 사용성을 꾸준히 모니터링하여 더 완성도 높은 기능과 경험을 제공할 수 있도록 개선해 나가겠습니다.

긴 글 읽어주셔서 감사합니다!