grep

Engineering

리뷰 통합 여행 어때? — 하편

김지영Tori(토리) / iOS 개발팀여기어때

2023년 6월 2일

원문에서 보기 ↗

이전 글에서는 통합리뷰 프로젝트가 어떤 것인지, 어떤 고민을 해서 통합리뷰프로젝트를 개발했는지 살펴봤어요.

리뷰 통합 여행 어때? — 상편

안녕하세요 👋🏻

여기어때 모바일 앱 개발을 담당하는 iOS 개발자 토리, Android 개발자 그루입니다. 오늘은 작년 하반기부터 진행되고 있는 통합리뷰 프로젝트에 대해서 여러분께 소개해 드리려 해요.techblog.gccompany.co.kr

이번 글에서는 어떤 여정을 통해 처음 기획의 비즈니스 로직이 조금씩 변화해 갔는지 이야기해 볼게요.

앱 개발팀에서는 코드 리뷰나 프로젝트 중간중간 협의를 통해 한 번 더 비즈니스 로직을 점검해 보고 추가로 필요한 기능이나 누락된 기능이 있는지 검토하는 과정을 거쳐요. 개발자는 코드만 짜면 되는 거 아니야? 라고 생각할 수도 있겠지만, 앱을 사용하는 사용자와 가까이서 만나는 앱개발팀에서는 이런 협의까지 꼼꼼히 이루어져야 해요.

4. 탐험을 떠나요

초기 기획이나 디자인 가이드를 요청사항이라고 할 때, 어느 책의 한 구절을 인용하면

“요청 사항은 사실 함께 탐험을 떠나자는 초대장이다.”

라고 할 수 있어요. 요청사항 은 꼭 수행해 내야만 하는 절대적인 룰이라는 생각이 들 수도 있겠지만, 기획과 디자인 가이드가 나오면 개발자는 기획대로 개발 전/후에, 리뷰를 통해 기획 조율하는 과정을 거치는 탐험을 시작해요. 이때 기획 수정을 제안해 볼 수도 있고, 누락된 사항을 발견하기도 해요.

이번 챕터에서는 이 초대장을 받고 어떤 모험을 떠났는지 몇 개의 여정을 보여드릴까 해요.

4–1. API 동기 이슈

앱개발팀에서는 코드 리뷰를 진행하고 있어요. 코드 리뷰에서는 소스코드뿐 아니라, 비즈니스 로직, 플로우 등을 다시 한 번 검토해 보는 시간을 가져요. 이번 여정은 좋아요 버튼을 개발하고 난 후 코드 리뷰에서부터 시작되었어요.

리뷰 좋아요 버튼과 리뷰 숨김 버튼

좋아요 버튼은 리뷰셀 하단에 있어요. 이를 통해 사용자들은 마음에 드는 리뷰에 공감을 표현할 수 있어요. 리뷰 숨김 기능도 마찬가지로 사용자가 원하는 리뷰를 숨길 수 있는 버튼이에요.

현재 여기어때의 이 두 버튼은 누를 때마다 API 통신이 이루어지고 있는데요. 이 때 응답을 받아야지만 UI 처리가 완료되고 있어요. 이 부분을 개발 완료하고 Android 파트에서 코드 리뷰 중 리뷰 숨김/해제, 좋아요 설정/해제 같은 API는 서버와 실시간 소통이 필수적이지 않다는 피드백이 있었어요. 저희는 이런 피드백을 받고 과연 이 것이 여기어때에 들어가면 좋을까? 현재 여기어때 플로우에 알맞을까를 한 번 더 고민해 보았어요.

여기어때 현 좋아요 버튼 액션 플로우 차트

여기어때에서 좋아요 버튼은 API 동기 처리를 하고 있어요. 동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 보장된다는 장점이 있지만, 앞선 태스크가 종료할 때까지 이후 태스크들이 블로킹 된다는 단점이 있죠. 그래서 네트워크 지연이 발생했을 때 사용자는 추가적인 동작을 하지 못하고 계속 대기하게 돼요. 이 피드백을 적용하여 비동기로 API 처리를 하게 되면 신규로 적용할 수 있는 기능은 아래와 같은 액션 플로우 차트로 나타낼 수 있어요.

여기어때 신규 좋아요 버튼 액션 플로우 차트

리뷰 숨김/해제, 꿀 정보 설정/해제 같은 API는 서버와 실시간 소통이 필수적이지 않다고 생각했어요. 그래서 UI 처리와 API 처리를 서로 분리해서 하도록 플로우 차트를 만들었어요. 플로우 차트에서 알 수 있듯이, API 응답 결과에 무관하게 UI를 처리하고 있어요. 에러가 발생했을 때는 기존대로 에러 다이얼로그를 노출하고, 미리 변경한 UI는 롤백하지 않아, 사용자의 동작이 반영되지 않았음을 보여줘요. 이렇게 플로우를 변경하면, 앱 체감 속도에 대한 사용자 경험을 향상할 수 있어요. 좋아요 버튼을 누를 때마다 돌아가는 네트워크 로딩 창을 보며 아무 입력도 되지 않는 화면을 기다리고 있지 않아도 되니까요.

여기서 고려해야 할 상황은 다음과 같이 API 응답 여부와 무관하게 UI를 수정하므로 사용자에게 혼란이 생길 수 있다는 점이 있었어요.

이때의 플로우를 고민해 보면서 타사는 어떻게 해결했는지 확인해 보았어요.

타사 플로우 비교

좋아요 처리가 안 된 버튼을 탭 했을 때 타사들은 각기 다른 API/ UI 처리를 하고 있었어요. 현재 여기어때와 같은 방식의 처리를 하는 플랫폼도 상당수 있었고요. 그 외 각양각색의 방식으로 처리하고 있었는데, 그중에서 저희가 생각한 플로우와 유사한 플로우를 사용하는 플랫폼들은 아래와 같은 동작을 하고 있었어요.

타사의 좋아요 탭시 플로우를 참고해 봤어요.

A, B사의 경우 저희가 바꾸고자 한 플로우와 동일하게 API를 비동기 처리하고 있었어요. 즉, 서버 응답을 받지 않아도 좋아요 버튼 동작을 변경해 주고 있었어요. 그러면 사용자는 좋아요 버튼을 눌렀는데 서버에서는 좋아요가 반영되지 않은 상황(네트워크 에러 후 페이지 이동 후 해당 페이지로 다시 돌아왔을 때)을 어떻게 해결했는지 볼까요?

A사는 좋아요를 눌렀을 때 에러가 난다면, UI는 좋아요를 선택한 상태로 변하지만, API 응답을 받게 되면 에러 팝업이 노출돼요. 이렇게 에러 팝업을 노출해, 사용자에게 좋아요가 반영되지 않은 것을 알리고, 후에 해당 페이지로 돌아왔을 때 좋아요가 반영되지 않을 수 있음을 알려주는 방식으로 이런 상황을 해결하고 있어요.

B사는 좋아요를 눌렀을 때 에러가 난다면, UI는 좋아요를 선택한 상태로 변하지만, API 응답을 받게 되면 에러 팝업이 노출되며, UI가 탭 하기 전 상태로 돌아가도록 처리했어요. 이렇게 API를 비동기 처리하면 사용자의 앱 속도 경험을 향상함과 동시에, 네트워크 에러가 발생했을 때 에러 팝업을 통해 좋아요가 서버에 반영되지 않았음을 인지할 수 있어요.

이렇게 사전 조사를 마치고 기획, API 수정이 필요하여 회의를 요청했어요.

그래서 결과는?

결론부터 말하자면, 이번 프로젝트에는 신규 플로우를 적용하지 못했어요.

꿀 정보나, 리뷰 숨김에 대해서 비동기 형태로 리팩토링하려면, API 수정도 같이 필요해요. 여기어때에서는 통일된 정책에 따라 에러 처리 하고 있어요. 그래서 이 부분만 정책을 다르게 바꾸는 것은 통일성을 해칠 수 있다는 결론에 이르렀어요. API 비동기 처리를 이용한 신규 좋아요 플로우 반영하면 좋을 것 같다는 의견이 많았지만, 아직 명확히 모든 케이스를 정의하지 못해 이번에는 적용하기가 어려웠어요. 지금 플로우도 사용성에 크게 해가 되지 않기 때문에, 후에 좀 더 명확한 정책 정리가 된다면 도입해 볼 수도 있겠지요.

여러 케이스를 고려해서 서버 쪽과 정책을 정리한 다음에 리뷰부터 순차적으로 먼저 적용하도록 하는 것이 필요해 다음을 기약했어요. 비록, 피드백을 바로 반영해 보지는 못했지만, 피드백을 받고, 다른 앱들의 플로우를 확인하고 장단점을 비교해 보는 과정이 재미있었어요. 후에 있을 모험도 기대가 되네요.

4–2. 리뷰 동작 에러 핸들링

여기어때 앱을 사용하다 보면 나오는 에러 팝업을 보신 적이 있나요?

앱을 사용하다 보면 API 통신 지연이나 잘못된 접근 등이 발생할 수 있어요. 이때 사용자들에게 어떤 일이 일어났는지 알려주고, 때로는 사용자에게 해결책도 알려줄 수 있는 것이 에러 팝업이에요. 이런 팝업들은 에러 동작을 정의하고 개발하는 과정을 통해 여러분들이 볼 수 있게 만들어져요.

먼저, 여기서 말하는 에러 동작이라는 것은 어떤 것들이 있을까요?

에러를 토스트 메시지로도 보여줄 수도 있고, 다이얼로그 팝업을 통해 보여줄 수도 있어요. 다른 팝업을 보여주지 않고, 빈 페이지 화면을 띄워줄 수도 있지요. 이렇게 에러 동작을 다양하게 정의할 수 있기 때문에 통합리뷰 프로젝트를 진행하면서 에러들의 동작을 각 화면에 적합하게 맞출 필요성이 있었어요. 그래서 저희는 기존 에러들을 포함해서 에러 항목들을 정리하고, 그에 따른 에러 액션을 정의했어요. 이때, API 호출 시에 로딩바가 돌아가게 해서 사용자 입력을 막을지 여부도 같이 정의했어요.

통합리뷰 신고 및 숨기기 API 오류 케이스

API별로 토스트 메시지로 예외 처리를 할지 공통 팝업으로 처리할지 한 가지로만 정하도록 했어요. 토스트 메시지가 나오는 곳은 전부 토스트 메시지로, 다이얼로그 메시지 팝업을 띄우는 API는 전부 다이얼로그 팝업을 띄울 수 있도록 맞추는 작업을 한 것이에요.

Android

// 토스트 에러처리
viewModel.uiFlow.collect { info ->
    when (info) {
        is UiFlow.ShowToast -> {
            toastManager.show(info.message)
        }
    }
}

// 다이얼로그 에러처리
onError = { info ->
    showErrorDialog(info, onClickConfirm = {
        _uiFlow.emit(UiFlow.FinishActivity)
    })
}

...

iOS

// 토스트 에러처리
reactor.pulse(\.$toastError)
       .filterNil()
       .bind(with: self) { owner, error in
          Toaster.makeToast("인터넷 연결이 불안정해요.\n연결 상태를 확인해주세요.", position: .center)
       }
       .disposed(by: disposeBag)

// 다이얼로그 에러처리
reactor.pulse(\.$dialogError)
        .filterNil()
        .bind(with: self) { owner, error in
            owner.showAPIErrorAlert(error)
        }
        .disposed(by: disposeBag)
        
...

기존부터 네트워크 모듈화가 잘 되어 있었기 때문에 이번 에러처리를 정의하고 구현하는 것도 어렵지 않게 할 수 있었어요.

이번 챕터 4에서는 코드 리뷰에서의 피드백이나, 개발 도중에 필요한 사항들을 캐치해서 진행한 협의과정들에 대해 이야기해 봤어요. 이렇게 일부 작업은 사용성에 대한 협의를 통해 이후에 개발할 피쳐들에서 도입할지 여부를 생각해 보기도 하고, 좀 더 통일성 있는 로직을 만들기 위해서 같이 고민해 보고 논의하는 과정을 가져요. 이렇게 기능들을 조율해 가는 과정이 이번 프로젝트를 더욱 뜻깊은 기억으로 남게 했어요.

5. 가장 기억에 남는 작업이 있나요?

다양한 작업이 있었지만, 그중에서 한 가지를 뽑는다면 아무래도 리뷰 작성화면에서 사용자들의 본문 입력 난이도를 조금 높였던 작업일 것 같아요.

여기어때뿐만 아니라 리뷰 시스템이 존재하는 다른 서비스들에서도 사용자가 리뷰를 작성하면 소정의 포인트를 제공하고 있는데요. 하지만 몇몇 사용자들은 이런 포인트를 얻기 위해서 특정 문구를 반복해서 입력한다든지 또는 리뷰 작성과 무관한 문구를 붙여넣기 하여 서비스의 리뷰 시스템에 대한 신뢰를 떨어트리는 문제가 많았어요.

이런 문제를 조금이라도 해결하기 위해 저희는 리뷰를 작성할 때 아래처럼 본문 입력 난이도를 조금 높이기로 했어요.

하지만 작업을 진행하면서 몇 가지 어려움을 마주했는데요.

5–1. 처음부터 정책이 간단하지 않았어

본문 입력 난이도를 높이는 정책은 두 번의 Phase를 거침으로써 정리가 되었는데요. 첫 번째 Phase에서는 위 같은 정책을 처음 적용하려다 보니 기획이 복잡했고 예외 케이스도 많아져서 정규식 방식을 활용하여 작업을 처리하기가 어려웠어요. 그래서 Android, iOS 모두 문자열을 배열과 함께 다루면서 문자 탐색 로직을 직접 구현하여 작업을 진행했어요.

Android

// 커서 위치값을 활용하여 커서 바로 앞에 위치한 글자의 idx 조회
fun List<String>.findTargetIdxBySelection(currentSelection: Int): Int {}

// 타깃 글자를 기준으로 앞, 뒤의 값을 비교하며 동일한 글자를 분석
fun List<String>.isFiveIterationsErrorOccured(targetIdx: Int): List<Int> {}

...

iOS

//같은 글자가 있는지를 담는 구조체
struct CheckSameResult {}

// 동일 글자를 5회 이상 연속으로 입력했는지 확인, CheckSameResult 반환
func checkSameCharacter(
    cursorPosition: Int,
    originalString: String,
    isDelete: Bool
) -> CheckSameResult {}

// checkSameCharacter를 사용하여 반복되는 글자 index를 가져온 후, 반복 글자 제거, 입력할 수 있는 최대 글자 수를 넘긴 글자를 모두 제거
func checkCharacters(
    text: String, 
    cursorPosition: Int? = nil, 
    cursorDistance: Int? = nil, 
    duplicateCheck: Bool = true, 
    initProcess: Bool = false, 
    isInputPossible: Bool = true
) -> Observable<Mutation> {}

그러다보니 복잡한 로직을 처리하는 메서드가 많아졌고 다음에 다른 개발자분께서 이어서 작업을 하신다면 히스토리를 파악하실 때 어려운 측면이 있을 것 같았어요. (저희가 작성했던 코드임에도 불구하고 시간이 조금 지나니 기억이 벌써 가물가물한 걸 보면요 🫠)

하지만 다행히 PO 분께서도 위 문제를 중요하게 생각해 주셨고 Phase 3에서 정규식 방식을 활용할 수 있는 방향으로 이전의 정책을 함께 간소화해 보기로 먼저 제안을 해주셨습니다.

리뷰 작성시 본문 입력 플로우 차트

저희는 Phase 3를 시작하면서 예외 케이스가 많았던 기존 정책을 간소화하고, 복잡했던 로직을 정규식 방식으로 전환하여 위의 플로우 차트처럼 본문 입력 시나리오를 훨씬 간단하게 만들었어요. 기존 대비 코드의 양은 4분의 1로 줄었고 가독성도 훨씬 좋아졌죠. 또한, 동일한 정규식을 Android, iOS에서 함께 사용하니 플랫폼별 로직도 통일이 되어서 나중에 시간이 지나서 다시 보았을 때에도 히스토리를 파악하기가 수월했어요.

개발자는 코드 구현 스킬도 중요하지만 “시간이 지나더라도 이 코드를 관리할 수 있을까?”, “히스토리 파악에는 문제가 없을까?” 등 개발 전 다양한 방면에서 정책에 대한 검토와 피드백을 할 수 있어야 한다는 것을 이번 계기를 통해 배울 수 있었어요.

5–2. 이모지는 일반 텍스트와 형식이 달라

예를 들어 사용자에게는 1글자로 보이는 ‘👨‍👩‍👧‍👦’ 이모지가 Kotlin에서 size나 length 함수를 통해 글자 수를 계산했을 때는 4글자로 나왔어요. 해당 이모지가 내부적으로 ‘👨’, ‘👩’, ‘👧’, ‘👦’ 이모지 4개로 조합되어 있었기 때문이죠. 저희는 앱에서 사용자 기준으로 글자 수를 보여줘야 하므로 위 문제를 해결해야 했어요. 해결책을 찾던 중 Grapheme 방식을 활용하는 것이 적합할 것 같다고 판단했어요. Grapheme 방식은 사용자가 인식하는 하나의 글자로, 1개의 Grahpeme가 여러 개의 Code Point로 처리되기 때문에 이모지같이 내부적으로 바이트 크기가 각각 다른 문자에 대응할 수 있었어요.

Kotlin에서는 Grapheme 방식을 아래처럼 구현할 수 있었어요.

fun String.graphemeLength(): Int {
    var count = 0
    return BreakIterator.getCharacterInstance()
        .apply { setText(this@graphemeLength) }
        .let {
            while (it.next() != BreakIterator.DONE) {
                count++
            }
            count
        }
}

반대로 Swift에서는 기본적으로 String이 내부적으로 Grapheme의 컬렉션으로 구현되어 있어서 추가적인 로직 구현 없이 count 프로퍼티를 사용하면 되었어요.

6. 파트별 담당자들의 생각은 어때요?

이렇게 여러 고민과 협의를 통해서 통합리뷰 프로젝트가 Phase 3까지 성공적으로 출시되었어요. 다양한 파트의 팀원들의 고민과 노력이 있었기에 통합리뷰 프로젝트가 진행될 수 있었어요. 이번 챕터에서는 이런 고민을 엿볼 수 있도록, 여러 파트의 팀원분들께 질문을 드리고 답변을 들어보는 시간을 가져 봤어요.

PO 제임스

서버/API 개발자 버나드

QA 그루트

UX 디자이너 우주

UX 라이터 제티

이렇게 약 10개월 가량 진행했던 통합리뷰 프로젝트에 대해서 두 편의 글로 이야기해 보았어요. 앱 개발 관점에서 글을 작성하다 보니 빠트린 부분도 분명히 있을 것 같아요. 그리고 DA, MKT, 보안 등 다른 많은 팀원의 노력을 글에 모두 담아내지 못해서 아쉬움을 가지고 있어요.

통합리뷰 프로젝트는 다양한 파트에서의 고민과 노력이 있었기에 지금까지 진행될 수 있었고 앞으로도 계속해서 이어 나갈 예정이에요. 여러분께 지금보다 더 나은 리뷰 시스템을 제공하여 더욱 행복한 숙소 경험을 할 수 있도록 항상 노력하겠습니다.

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