Flutter GPS 신호에 따른 API 호출 최적화

우기·2025년 1월 19일
0

이 글은 그라운드 플립의 프론트엔드에 존재하는 과도한 API 호출을 최적화하는 글입니다.

서론

그라운드 플립에선 아래와 같이 지도 화면에 픽셀들을 표시해준다.

내가 소유하고 있는 땅의 경우 초록색, 남이 소유하고 있는 땅은 빨간색으로 표시를 해주는 방식이다.

게임이라는 도메인의 특성 상 우리 팀은 픽셀 소유의 변화를 '근실시간'으로 제공하고 싶었다.

하지만 매우 빠르게 진행되는 SW마에스트로 연수 과정 특성 상 웹소켓이나 SSE같은 복잡한 방법을 통해 구현하기엔 무리가 있었다.

따라서 우리가 선택한 방법은 Polling 방식이었다.

프론트엔드 측에서 특정 조건을 만족하게 되면 화면에 표시되고 있는 중심 위경도를 통해 반경 내 픽셀들에 대한 정보를 서버에 요청하는 방식이다.

참고로 화면에 표시되는 영역의 반경은 구글맵 API에서 제공하는 getVisibleRegion()을 사용하여 구할 수 있었다.

  Future<int> _getCurrentRadiusOfMap() async {
    LatLngBounds visibleRegion = await googleMapController!.getVisibleRegion();

    final LatLng topLeft = LatLng(
      visibleRegion.northeast.latitude,
      visibleRegion.southwest.longitude,
    );
    final LatLng bottomRight = LatLng(
      visibleRegion.southwest.latitude,
      visibleRegion.northeast.longitude,
    );

    return (_calculateDistance(topLeft, bottomRight) / 2).round();
  }

문제

API 호출이 정말 너무 매우 자주 일어났다.

출처 : 팀원(Koomin)의 블로그

사진에서 보다시피 점령된 땅을 조회하는 API가 다른 여러 API들에 비해 많은 횟수로 호출되었다.

해당 API가 호출되는 경우는 아래와 같다.

  • 사용자가 지도 화면을 움직인 후 멈춘 시점으로 0.3초 뒤
void onCameraIdle() {
    updateMap();
}
  • 30초에 한 번씩 발생하는 주기적인 호출
  void trackPixels() {
    _updatePixelTimer =
        Timer.periodic(const Duration(seconds: 10), (timer) async {
      UserManager().updateSecureStorage();
      updateMap();
    });
  }
  • 게임 모드를 전환할 때 (예: 개인전 => 그룹전)
  void changePixelMode(int type) {
    selectedMode.value = type;
    currentPixelMode.value = PixelMode.fromInt(type);
    bottomSheetController.minimize();

    isBottomSheetShowUp = false;
    lastOnTabPixel = Pixel.createEmptyPixel();
    updateMap();
    trackPixels();
  }

문제는 "사용자가 지도 화면을 움직인 후 멈춘 시점으로 0.3초 뒤"의 경우 였다.

사용자가 화면을 조금만 움직여도 API가 호출된다.

또한 아래와 같이 GPS 신호에 따라 카메라를 고정시키는 코드가 존재하는데, 가만히 있는 경우에도 GPS 신호는 미세하게 바뀌기 때문에 API 호출이 일어났다.

  void _trackUserLocation() {
    _locationService.location.onLocationChanged.listen((newLocation) async {
      _locationService.updateCurrentLocation(newLocation);
      if (isCameraTrackingUser.value) {
        setCameraOnCurrentLocation();
      }
      speed.value = _locationService.getCurrentSpeed();
      if (isPixelChanged() && isWalking()) {
        _updateLatestPixel();
        await occupyPixel();
      }
    });
  }

어떻게 개선하지?

여러 가지 방법들이 있었지만 일종의 Threshold를 사용하는 방식으로 개선했다.

보다시피 아주 조금의 좌표 변화도 API를 호출했다.

하지만 생각해보면 조금 만큼만 카메라를 움직이는 경우 굳이 새롭게 호출을 하지 않아도 된다.

따라서 위와 같이 Threshold를 만들어 이 반경 내에선 카메라를 움직여도 호출하지 않도록 개선했다.

고려해야 할 점

하지만 고려해야 할 것이 있는데, 바로 Zoom Level이다.

이 Threshold를 정적인 값으로 두게 된다면, 지도를 축소했을 땐 결국 같은 문제가 발생한다.

따라서 지도를 축소하면 임계값을 높이고, 확대하면 줄이는 방식으로 구현을 해야했다.

다행히 이에 대한 지식은 픽셀의 경계가 겹쳐보이는 문제를 해결했을 때 습득했었다.

줌 레벨이 1 변화할 때마다 지도는 약 2배 확대/축소된다.

따라서 아래와 같이 로직을 개선했다.

  void onCameraIdle() {
    if (!isBottomSheetShowUp) {
      _cameraIdleTimer = Timer(Duration(milliseconds: 300), () {
        final currentZoomLevel = currentCameraPosition.zoom;
        final calculatedThreshold = _calculateThresholdByZoom(currentZoomLevel);
        final zoomLevelChanged = _hasZoomChanged(currentZoomLevel);

        if (zoomLevelChanged || _cameraMovedOverThreshold(calculatedThreshold)) {
          updateMap();
        }

        _lastZoomLevel = currentCameraPosition.zoom;
        lastStoppedCameraPosition = currentCameraPosition;
      });
    }
  }

  bool _cameraMovedOverThreshold(double threshold) {
    final distance = _calculateDistance(currentCameraPosition.target, lastStoppedCameraPosition.target);
    return distance > threshold;
  }


  bool _hasZoomChanged(double currentZoom) {
    if (_lastZoomLevel == null) return true;
    return (currentZoom - _lastZoomLevel!).abs() > zoomChangeThreshold;
  }

도식화하면 아래와 같다.

결론

테스트 결과, 유의미하게 호출 횟수가 줄어들었지만 아직 앱마켓 상에 배포되지 않아 정확한 수치를 측정하진 못했다.

배포 이후 API 호출 수를 따져 볼 예정이다.

profile
항상 한번 더 생각하는 개발자를 지향합니다!

0개의 댓글