지도 렌더링 및 API 호출 최적화

요약

공유 킥보드 서비스 "디어"의 프론트엔드 개발을 하며 겪었던 지도 렌더링 성능 이슈와 그 해결방안에 대한 내용입니다.

기술 스택

React Native, React Query, Geolib, react-native-naver-map

문제와 해결

서비스 초기에는 한번에 모든 킥보드 데이터를 받아와서 지도에 그려주는 방식을 택했습니다. 킥보드 대수가 얼마되지 않을 때는 별다른 성능 상의 문제가 발생하지 않았습니다. 아래 지도는 서비스 초기의 상황과 유사합니다.

문제1: 늘어난 데이터로 인한 렌더링 속도 저하

하지만 서비스가 성장하면서 운영하는 킥보드의 대수도 점점 늘어났습니다. 그에 따라 지도에서 보여줘야 할 킥보드 마커, 서비스 지역 및 반납 금지 구역 등도 함께 증가했습니다. 한 번에 많은 데이터가 그려지자 렌더링 성능이 저하되는 이슈가 발생했습니다. 아래 지도를 움직여보면 지도가 심각하게 버벅이는 현상을 경험할 수 있습니다.

문제는 서비스에 필요한 모든 정보를 한번에 불러온다는 것이었습니다. 디어 서비스는 전국 각지에서 운영되고 있지만 킥보드를 이용하는 한명의 고객은 다른 지역에는 관심이 없습니다. 관련없는 지역의 데이터를 그리느라 서비스 이용에 차질이 생기면 안될 것 같습니다. 고객이 지도에서 보고 있는 지역의 킥보드만 렌더링하는 방식으로 개선해보았습니다.

해결1: 지도 영역 내의 마커만 렌더링 하도록

네이비 지도 API는 지도 영역(카메라)을 옮길 때마다 경계선의 좌표를 알려줍니다. 이를 활용해 전체 데이터 중 현재 고객이 보고 있는 영역 내의 마커만 렌더링하도록 개선했습니다. 지도를 움직여보면 이전보다 훨씬 가볍게 움직이는 것을 확인할 수 있습니다.

문제2: 데이터 사용량 증가 & 서버/클라이언트 부하

언뜻 문제를 해결한 것처럼 보였지만 서비스가 더욱 성장하면서 다른 문제가 표면 위로 드러나기 시작했습니다. 킥보드가 10,000대를 넘어가고 운영하는 지역도 100개를 넘어가며 서버에서 받아올 데이터 용량이 심각하게 커졌습니다. 유저는 고작 5km 반경 내의 킥보드를 이용하려고 앱을 열지만, 모든 지역의 데이터를 다운로드 받아야지만 앱을 정상적으로 이용할 수 있게 된 것입니다. 이는 사용자 경험에 심각한 영향을 미칠 수 있습니다. 또한 대용량 데이터 처리로 인한 서버 부하가 증가하고, 클라이언트에서 카메라가 이동할 때마다 지도 bound 내의 데이터를 찾는 연산을 진행하기에 데이터 양에 비례하여 연산량이 많아질 수 밖에 없었습니다.

해결2-1: 지도 영역 내의 데이터만 요청하기

유저는 앱에서 보이는 부분에만 관심이 있습니다. 유저가 관심있는 부분, 즉 현재 지도 영역경계(bound)를 서버에 알려주고 서버는 해당 영역 내의 정보만 내려주는 식으로 API 설계를 변경했습니다. 유저가 지도를 움직인다면, 유저가 관심있는 부분이 변경되었다는 뜻이므로 서버에 새로운 정보를 요청합니다. 아래 지도에서 확인할 수 있습니다.

해결2-2: 캐싱으로 API 요청 최적화

한번에 받아오는 Response Size를 획기적으로 줄였지만 대신 약간의 trade-off가 발생했습니다. 2-1의 해결책에서는 유저가 지도를 움직일 때마다 서버로 요청이 나가고 있습니다. 유저가 멀리 있는 지역의 킥보드를 확인하기 위해 지도를 움직인다면 당연히 새로운 데이터를 받아오는 게 맞습니다. 하지만 약간의 움직임에도 새로운 요청이 나가는 것은 다소 비효율적인 것 같습니다.

지도의 중요한 움직임과 중요하지 않은 움직임을 구분할 수 있다면 요청을 최적화 할 수 있습니다. 중요한 움직임, 즉 유저가 기존 데이터로 확인할 수 없는 새로운 지역으로 지도를 이동한다면 데이터가 갱신되어야 합니다. 중요하지 않은 움직임, 즉 기존 데이터로 여전히 확인할 수 있는 정보라면 요청이 나가지 않아도 괜찮습니다.

위 지도를 움직이보면 refetchcached라는 두 가지 다른 상태를 확인할 수 있습니다. 약간의 지도 움직임으로는 서버로 요청을 보내지않고 클라이언트 cache에 있는 데이터를 보여줍니다. 반면 기존(캐시된) 지도의 중심 좌표로부터 일정 거리 이상 멀어진다면 서버로 재요청을 보내 데이터를 갱신합니다.지도를 갱신하는 threshold가 되는 거리는 디바이스 사이즈에 따라 유동적으로 변하도록 설정했습니다. 이를 통해 지도를 그리는 데 필요한 응답 사이즈를 획기적으로 줄임과 동시에 요청 횟수를 최적화하여 서버의 부하를 최소한으로 덜어줄 수 있게 되었습니다.

What I Learned

  • 고객 경험과 성능을 해치지 않으며 대용량 데이터를 다루는 방법을 고민했습니다. API 호출 횟수를 최소화하고 동시에 windowing, caching 등의 성능을 최적화하기 위한 기법들에 대해 공부해 볼 수 있었습니다.
  • 서비스가 성장함에 따라 필요한 기술적 해결책도 달라진다는 것을 배웠습니다. 그때 맞았던 것이 지금 맞을 거라는 보장은 없습니다. 서비스의 상황과 팀의 리소스를 적절히 고려하여 현재 적합한 솔루션을 찾을 수 있는 유연한 사고가 필요하다는 것을 느꼈습니다.