"기술적 개선은 기본이다. 중요한 건 사용자가 실제로 느끼는 제품의 '밀도'를 높이는 것이다."
안녕하세요. 1인 개발사 Brewstar를 운영하며, 사이드 프로젝트로 **[머랭트립]**을 개발하고 있습니다.
머랭트립은 **'도보 반경'**을 기준으로 카페, 숙소, 술집 등 여러 장소를 한 번에 검색하여 최적의 동선을 찾아주는 앱입니다. 최근 이 앱을 대대적으로 리뉴얼하면서, 기존의 기술 스택을 Flutter로 전환했습니다.
하지만 이번 글에서는 단순히 *"Flutter로 바꾸니 빨라졌어요"*라는 뻔한 이야기를 하려는 것은 아닙니다. 이번 개선의 핵심 목표였던 **'기능의 나열이 아닌, 경험의 완성도'**를 어떻게 기술적으로, 그리고 기획적으로 풀어냈는지 공유해 보고자 합니다.
0. 왜 다시 만들었나? (Flutter 전환 배경)
기존 버전도 기능상으로는 문제가 없었습니다. 하지만 개발자로서, 그리고 프로덕트 오너로서 항상 아쉬웠던 점은 **'앱이 주는 손맛(Feel)'**이었습니다.
이번 리뉴얼에서 Flutter를 선택한 기술적인 이유는 명확했습니다.
렌더링 특성: Flutter의 렌더링 구조를 활용해 일관된 화면 동작을 만들고자 했습니다. 특정 프레임률을 보장하는 것은 아니며 기기와 구현에 따른 측정이 필요합니다.
크로스 플랫폼 대응: iOS/Android UI의 통일성 유지
생산성: Hot Reload를 통한 빠른 UI 이터레이션
하지만 사용자에게 *"이거 Flutter로 다시 짰어요"*라고 말하는 건 아무런 의미가 없습니다. 사용자가 느껴야 할 것은 **'기술'이 아니라 '쾌적함'**이어야 합니다.
그래서 이번 개선의 슬로건을 다음과 같이 정했습니다.
기능을 더하지 말고, 경험을 다듬자.
1. 검색 경험의 개선: 키워드의 모호함을 넘어서
🔴 기존의 문제점: "나는 카페를 찾고 싶다고!"
기존 머랭트립의 검색 로직은 단순한 Keyword Matching 방식이었습니다. 이는 개발 구현상으로는 매우 간단하지만, 치명적인 UX 결함을 가지고 있었습니다.
예를 들어, 사용자가 분위기 좋은 카페를 찾기 위해 **카페**를 검색했다고 가정해 봅시다.
기대 결과: 스타벅스, 블루보틀, 감성 카페 리스트
실제 결과: 스타벅스, **'카페'**같은 치킨집, **'카페'**테리아가 있는 김밥천국
장소 이름에 '카페'라는 단어만 들어가면 무조건 결과에 노출되는 **노이즈(Noise)**가 발생했습니다. 이는 사용자의 **검색 의도(Intent)**를 무시하는 결과였습니다.
🟢 개선된 로직: 의도에 따른 검색 분기 처리
이를 해결하기 위해 검색 로직을 두 가지 트랙으로 분리했습니다.
카테고리 검색 (Category Filter): 명확한 목적(카페, 숙소 등)이 있을 때
커스텀 키워드 검색 (Keyword Search): 특정 상호명이나 특수 키워드를 찾을 때
코드 레벨에서는 아래와 같이 검색 타입에 따라 쿼리 전략을 다르게 가져가는 구조로 변경했습니다.
// SearchType Enum 정의
enum SearchType { category, keyword }
Future<List<Place>> searchPlaces(String input, SearchType type) async {
if (type == SearchType.category) {
// 1. 카테고리 검색: 정확한 분류 코드(Code) 기반 필터링
return await repository.fetchByCategory(categoryCode: input);
} else {
// 2. 키워드 검색: 기존의 텍스트 매칭 방식 유지 (특정 상호명 검색 니즈)
return await repository.fetchByKeyword(query: input);
}
}
✨ 결과
이제 사용자가 '카페' 버튼을 누르면, 이름에 '카페'가 들어간 식당이 아니라 진짜 카페 카테고리에 속한 장소만 필터링되어 노출됩니다. 기술적으로는 단순한 분기 처리일 수 있지만, 선택한 카테고리에 맞춰 결과를 제한하는 동작을 명확히 했습니다. 신뢰도나 전환율의 개선 폭을 측정한 수치는 이 글에서 제시하지 않습니다.
2. 데이터 흐름의 확장: 저장은 끝이 아니라 시작이다
🔴 기존의 문제점: 고립된 데이터
이전 버전에서 '저장(Bookmark)' 기능은 단순히 Local Storage나 개인 DB에 박제되는 행위로 끝났습니다. *"나 여기 가고 싶어!"*라고 저장해 둬봤자, 친구에게 공유하려면 스크린샷을 찍거나 카톡으로 텍스트를 복사해야 하는 불편함이 있었죠.
개선된 구조: 저장한 목록을 공유하는 흐름
앱의 생명력을 길게 가져가기 위해서는 데이터가 흘러야 한다고 판단했습니다. 그래서 **[검색 → 저장 → 공유 → 재저장]**으로 이어지는 흐름을 설계했습니다.
Shareable Object: 저장된 리스트를 고유 링크(Deep Link)나 공유 가능한 객체로 변환
Social interaction: 친구가 공유한 리스트를 보고 '좋아요'를 누르거나, 내 리스트로 '가져오기(Fork)' 기능 구현
graph LR
A[사용자 A] -->|장소 검색| B(저장 리스트 생성)
B -->|공유| C{다른 사용자들}
C -->|확인| D[참고 및 방문]
C -->|Fork| E[내 리스트에 재저장]
(개념적 흐름도)
✨ 결과
이 흐름을 통해 검색 결과를 저장한 뒤 다른 사람에게 전달할 수 있도록 범위를 넓혔습니다. 1인 개발 앱이지만, 사용자들이 자발적으로 콘텐츠를 확산시킬 수 있는 구조를 마련한 것입니다.
3. 마무리: 결국 '디테일'이 전부다
개발자로서 새로운 기술 스택(Flutter)을 도입하고, 아키텍처를 설계하는 것은 언제나 즐거운 일입니다. 하지만 이번 리뉴얼 과정에서 가장 공들인 부분은 코드의 우아함보다 사용자가 마주할 화면의 완성도였습니다.
로딩 시 스켈레톤 UI의 자연스러운 애니메이션
검색 결과가 없을 때 보여주는 친절한 엠티 스테이트(Empty State)
버튼을 눌렀을 때의 미세한 햅틱 피드백
이런 사소한 디테일들이 모여 **"이 앱, 꽤 잘 만들었는데?"**라는 인상을 줍니다.
앞으로도 머랭트립은 기능의 가짓수를 늘리기보다, 단 하나의 기능을 제공하더라도 사용자가 '대접받는 느낌'을 받을 수 있는 밀도 높은 소프트웨어를 지향하려 합니다.
Flutter 전환기와 관련해 더 깊은 기술적 내용(상태 관리, 아키텍처 등)은 다음 포스팅에서 다루도록 하겠습니다.
Flutter의 렌더링 성능은 프레임 작업량과 실제 기기에서 확인해야 합니다. Flutter 성능 가이드
긴 글 읽어주셔서 감사합니다.
Assisted by AI