iOS
모바일 앱의 여기서 재탐색 기능 개선!
Draak여기어때
2025년 5월 23일
원문에서 보기 ↗안녕하세요, 여기어때 iOS 개발팀 드락입니다.
여기어때 앱은 당연! 하게도 지도 기반 탐색 기능을 제공하는데요, 오늘은 모바일 앱에서 지도 기반 기능을 구현할 때 이해하고 있으면 좋은 특성과, 최근 진행한 지도 검색 로직 개선 포인트에 대해 공유드리고자 합니다.
“같은 줌 레벨에서 지도는 디바이스마다 다른 영역을 보여줍니다.”
여기어때 앱은 국내 사용자를 위한 지도 기능 구현에 네이버 지도 엔진을 활용하고 있습니다. 우선 스크린샷을 한 장 보시죠.

좌측 아이폰 / 우측 갤럭시 폴드
두 디바이스 모두 지도의 줌 레벨은 14입니다. 소수점은 표기하지 않았지만 완전히 동일한 값으로 설정되어 있습니다. 하지만 보시는 것처럼, 같은 줌 레벨임에도 불구하고 각각 보이는 지도 영역은 확연히 다릅니다. 9시부터 6시 방향으로 한번 살펴보자면 아이폰은 ‘블루맨 뮤지컬웨딩’부터 ‘길목 신관’까지, 갤럭시 폴드는 ‘S-Tower’부터 ‘삼성동더샵’까지 지도 화면 안에 들어옵니다. 줌 레벨은 같더라도 지도에 표기되는 영역에 차이가 있죠?
극적인 비교를 위해 아이폰과 갤럭시 폴드를 예시로 삼았지만, 아이폰이라고 하더라도 모델에 따라 화면의 크기와 해상도가 다르기 때문에 동일한 줌 레벨에서 지도에 표시되는 영역에 차이가 있을 수 있습니다.

좌측 iPhone 12 Pro Max / 우측 iPhone 13 mini. 보여지는 범위가 다르죠?
지도엔진의 동작 성격 상 디바이스 화면의 크기와 관계없이 같은 줌 레벨에서는 같은 축척의 지도를 제공하는 느낌이라고 이해하면 될 것 같은데요(따라서 화면 크기가 큰 디바이스에서 지도가 더 크게 보이는게 아니라 더 넓은 영역을 화면에 표시하게 됩니다.) 근데..여기에 추가로 디바이스 스크린의 해상도에 따라 조금씩 달라지는 부분도 있어서요 그냥..
‘같은 지도 같은 줌 레벨이어도 폰이 다르면 노출 영역이 다를 수 있구나’
정도까지만 인지해 두고 넘어가면 좋을 것 같습니다:)
“레거시 지도 재탐색 방식, 그리고 고민들..”
이제 지도 화면 기준으로 제휴점을 검색해서 화면에 표시하는 기능을 이야기드려 볼게요. 먼저 레거시 지도 검색 로직을 간단하게 정리해보겠습니다.
- 앱에서 디바이스의 줌 레벨과 중앙 좌표값을 서버에 보냄
- 서버는 전달받은 줌 레벨에 보편적인 차원에서 최대한 스무스하게(?) 대응되도록 만들어둔 줌 레벨-반경 맵핑 테이블을 참조하여 이 줌 레벨에서는 이 반경값으로 검색하면 되겠다고 알아냄
- 중앙 좌표를 기준으로 반경검색을 수행하여 결과를 앱에 제공해 줌
아래 코드는 줌레벨별 반경 맵핑 테이블 예시입니다
public enum ZoomType {
...
LEVEL_21(30, 21),
LEVEL_20(30, 20),
LEVEL_19(50, 19),
LEVEL_18(100, 18),
LEVEL_17(500, 17),
LEVEL_16(1000, 16),
LEVEL_15(1000, 15),
LEVEL_14(1000, 14),
LEVEL_13(1500, 13),
LEVEL_12(3000, 12),
LEVEL_11(5000, 11),
LEVEL_10(10000, 10),
LEVEL_9(20000, 9),
LEVEL_8(30000, 8),
LEVEL_7(30000, 7),
LEVEL_6(30000, 6),
LEVEL_5(30000, 5),
...
}
앞서 보신 그림들처럼, 동일한 줌 레벨임에도 디바이스에 따라 지도 범위는 다르게 나타나며, 이렇게 사용자마다 다를 수 있는 지도 범위를 하나의 줌 레벨-반경 맵핑 테이블로 완벽하게 대응할 수는 없습니다. 핀치 제스쳐를 통해 지도의 줌 레벨은 3.55라던가 15.31처럼 소수점 단위로 미세하게 조절될 수 있습니다(하지만 1초도 안되는 짧은 시간임에도 수미터에서 수백킬로미터 단위까지의 극적인 변화가 지도 위에서 발생하고 있어요!).
미리 짜둔 맵핑 테이블이 이렇게 큰 폭으로 변화하는 줌 레벨에 충분히 대응할 수준의 해상도를 가지고 있기는 어렵습니다.

미세한 줌 레벨 변화를 따라가기 어려운 반경 테이블. 간단히 표현한 예시이며 실제 값은 아닙니다 :)
이런 테이블기반 반경검색 로직 + 앞서 말씀드렸던 디바이스 스크린마다 같은 줌 레벨임에도 다르게 보여지는 지도 표기 영역으로 인해 깔끔하게 해결하기 어려운 난제들이 발생하게 되는데요
- 화면에 알맞은 반경 검색이 되지 않아, 실제 보이는 지도 영역 내에 제휴점이 존재하지 않는다고 잘못 인식됨
- 검색된 제휴점이 지도에 보이는 화면을 벗어난 위치에 찍히는 경우가 있어, 경우에 따라 사용자는 검색이 실패한 것처럼 혼동함(앱은 검색 성공 시나리오로 UI가 동작했지만, 보고 있는 지도에는 제휴점 핀이 보이지 않아 사용자는 뭐가 어떻게 된 건지 어리둥절한 경험을 하게 됩니다)
- 1번과 2번이 동시에 발생하기도 함

고정된 반경 테이블을 사용할때 발생하게 되는 1,2,3번 사례
1번 사례는 지도 화면의 중앙으로부터 특정 반경 범위를 벗어난 바깥쪽 테두리 영역에 실제로는 제휴점이 존재함에도 검색 반경 범위에 들어오지 않았기 때문에 지도 상 해당 영역에는 제휴점이 없는 것처럼 인지되는 문제가 있습니다.
2번 사례의 경우 사용자가 우선 적당히 넓은 지도 영역에서 제휴점을 검색한 뒤, 점점 원하는 영역에 가깝게 지도를 확대해 가면서(손가락을 벌려 줌인 및 원하는 위치에 가깝게 지도를 이동하면서) 재검색을 하다 보면 발생되곤 하는 문제입니다.
UX적으로 해결 방법을 고민해보자니, 영역 내 검색이 완료될 때마다 마커가 화면에 다 보여질 수 있는 위치로 지도 포지션&줌 레벨 변경을 해줘야 하지 않겠느냐..같은 고육지책들을 논의해보지만.. 사용자가 가볍게 지도를 움직여서 탐색 위치를 확정하면 딱 그 영역 내에서 재탐색이 안정감 있게 발생해야 하면 좋은데, 그렇게 사용자의 의지가 반영된 지도 위치 및 줌 레벨 설정을 재탐색이 완료될 때마다 다시 흔든다니..글로 잠깐 상상해봐도 좀 어지럽죠?
“폴리곤&박스 검색”
위의 고민들은 UI측면에서 어떻게든 완화시켜 보는 방향으로 접근할 게 아니라 근본적인 검색 방식 변경을 폴리곤 및 박스검색으로 바꾸면 자연스레 해결될 성격의 이슈입니다.
지도 리파인 개발 초기 테크리뷰를 진행하면서 전시 및 검색 개발자분께 문의해본 결과 여기어때의 검색팀은 폴리곤&박스검색을 역시나 한참 전부터 잘 지원해주시고 있었어요, 그래서 앱에 API를 제공해주는 전시팀, 폴리곤&박스 검색을 제공해주는 검색팀과의 협의를 통해 앱에서 서버에 지도검색을 요청하는 방식과 인터페이스 역시 박스 검색 타입으로 바꾸게 됩니다.
폴리곤&박스 검색에 대해서 잠깐 설명드리겠습니다. 폴리곤 검색은 다수의 위경도 좌표로 이루어진 다각형 내에 포함되는 데이터를 검색하는 방식입니다.

가로수길 / 청계천 폴리곤 예시
행정구역, 자연지역, 명소 등의 복잡한 모양의 영역 범위를 자유롭게 설정 가능하고 그렇게 설정된 범위 내의 제휴점을 검색할 수 있게 됩니다. 이게 되면 박스 검색은 더 간단하죠? 검색하고자 하는 사각형 범위의 대각선 두 꼭지점만 있으면 해당 범위 내 검색을 수행할 수 있습니다.

사각형 검색
이렇게 사각형 검색을 통해 여기어때 앱은 어떤 디바이스에서 어떤 줌으로 지도를 보고 있든지간에, 이 영역의 모서리 위경도 값 기반으로 사용자가 보고 있는 지도와 정확하게 핏되는 검색 기능을 지원할 수 있게 개발되고 있습니다.
기존에 중앙값&줌 레벨로 인터페이스하여 검색을 수행하던 코드에서
//현위치 위경도
let location: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: mapView.cameraPosition.target.lat,
longitude: mapView.cameraPosition.target.lng)
//현재 지도 줌 레벨
let zoomLevel: Int = Int(round(mapView.zoomLevel))
//검색 수행
reactor.action.onNext(.getPOI(location: location,
zoomLevel: zoomLevel))
아래처럼 현 지도 영역의 사각형을 유추할 수 있는 두개의 위경도를 인터페이스하여 검색을 수행하도록 바뀌었습니다.
//현재 지도에 노출되고 있는 범위
let contentBounds = mapView.contentBounds
//현재 지도에 노출되고 있는 범위의 남서 위경도
let southWest = CLLocationCoordinate2D(latitude: contentBounds.southWestLat,
longitude: contentBounds.southWestLng)
//현재 지도에 노출되고 있는 범위의 북동 위경도
let northEast = CLLocationCoordinate2D(latitude: contentBounds.northEastLat,
longitude: contentBounds.northEastLng)
//검색 수행
reactor.action.onNext(.getPOI(bounds: (southWest: southWest,
northEast: northEast)))
위의 코드는 설명드린 핵심 내용만을 보기 쉽게 표현한 코드이고요, 사용자가 줌아웃을 통해 매우 넓은 영역에 대한 검색이 발생하는 경우에는 서버 부하 등을 고려해서, 필요하다면 특정 줌 레벨을 기준으로 중앙점 기준 반경 검색 방식도 혼용해서 사용할 수 있습니다.
또한, 중앙 기준 최대 몇 개까지 노출하거나 광고 상품을 우선 노출하는 등의 부가적인 비즈니스 정책들도 함께 고려할 수 있겠죠 :)

이런 케이스들에서는 필요하다면 줌 레벨 기준 반경검색 방식 혼용도 고려!
이 로직을 베이스로 여기어때 앱에 탑재된 지도 관련 기능들이 하나씩 리파인 되고 있습니다.
모바일 환경에서의 지도는 앞서도 말씀드렸듯이 손바닥보다 작은 지면 위에 약간의 손가락 제스쳐 한번으로 작은 건물 하나 스케일부터 지구레벨 스케일까지 폭력적일 정도로 다이나믹한 변화가 일어나는 영역입니다. 이 엄청난 배리에이션을 한 벌의 통일성 있는 UI 내에서 전부 소화해내야 하죠. 이를 위해 POI 클러스터링, 마커간 충돌시 노출 위계 정책, 줌 레벨별 마커 차등 표출 등등 여기서 다 말씀드리지는 못하지만..작은 지도안에 담긴 수많은 정보들을 사용자에게 잘 제공하기 위한 심오하고 재미있는 기술과 고민들이 많이 있어요 :)
지도 활용 서비스에 대한 이해와 다양한 지도 베이스 기능 개발에 대한 노하우를 가진다는 것은 디바이스의 각종 센서와 로케이션 기반 기능들을 능숙하게 다뤄야 하는 모바일 앱 개발자로서 좋은 스킬셋 하나를 장착하는 일이라 생각합니다^.^
여기까지 읽어주셔서 감사합니다!