React Native에서 Google Maps 연동 및 위치 추적 기능 구현하기

Published2025.03.25
Read Time11 min read

React Native에서 Google Maps 연동 및 위치 추적 기능 구현하기

1. 개요

최근 졸업작품인 러닝 크루 앱을 개발하면서 react native를 처음 써보았다. 앱에서 사용자의 현재 위치 및 코스 표시를 위해 react-native-maps를 활용해 Google Maps를 연동하였고, 사용자의 위치는 expo-location을 활용해 실시간으로 받아왔다.

본 게시글에서는 expo-location을 통해 사용자의 위치를 받아오는 방법, react-native-maps를 통해 구글맵을 연동하는 방법 및 위치를 지도에 마커로 표시하는 과정, 그리고 러닝 중이라는 가정하에 지도의 중심이 사용자의 위치를 따라가도록 하는 것을 다뤄볼 것이다. 그리고 끝으로 지도의 초기 위치를 내위치로 설정하던 과정에서 발생했던 문제도 다뤄볼 예정이다.

2. 환경 설정

2.1. 라이브러리 설치

Google Maps를 React Native에서 사용하기 위해 react-native-maps를 설치한다.

추가로, 현재 위치를 가져오기 위해 expo-location을 설치한다.

npx expo install로 설치하는 이유: 나 같은 경우 expo를 사용하여 react native개발을 하고 있고, 이 명령어를 사용하면 expo에서 공식적으로 지원하는 패키지 버전으로 라이브러리가 설치된다고 한다. 그렇기 때문에 expo를 사용할때는 가급적 npm install 보다는 npx expo install을 권장한다고 한다.

2.2. Google Maps API 키 설정

Google Maps API를 사용하려면 Google Cloud Platform에서 API 키를 발급받아야 한다.

그러기 위해서

  1. 구글 콘솔에서 프로젝트를 만든다
  2. api 및 서비스에 들어가서 Maps for javascript sdk를 허용한다. api 및 서비스 허용
  3. 그 다음 키 및 사용자 인증정보 항목에서 api키를 만들기로 api 키를 만든다. api키 생성
  • 키의 제한은 따로 없게했다. 보안을 위해서는 android용과 ios까지 따로따로 하는게 좋지만, 우선 본 게시글에선 따로 제한은 없게 할 것이다. 키의 제한없도록 하기
  1. app.json에서 이런식으로 키를 넣어 구글맵을 설정한다.
  • android는 app.json의 "android"안에 아래 코드를 추가한다.
"config": {
	"googleMaps": {
		"apiKey": "apikey"
}
  • ios까지 설정해주려면면 이런식으로 app.json안에 설정해준다.
"ios": {
	"bundleIdentifier": "com.company.runnershigh",
	"supportsTablet": true,
	"config": {
		"googleMapsApiKey": "apikey"
	}
},

이렇게 하면 기본적인 설정은 끝이난다.

3. 구현

3.1 MapView를 사용하여 지도를 보여주기

import MapView, { PROVIDER_GOOGLE } from "react-native-maps";
      <MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: 37.78825,
          longitude: -122.4324,
          latitudeDelta: 0.02,
          longitudeDelta: 0.02,
        }}
      >
      </MapView>

PROVIDER_GOOGLE을 사용하면 MapView에 굳이 키를 직접 추가하지 않더라도 미리 키가 설정되어있다면 google map을 사용할 수 있다. initialRegion은 말그대로 맵이 로드될 때 처음 위치를 말한다.

3.2. MapView에 현재 위치 표시하기

사용자의 현재 위치를 받아오기 위해 expo-location을 사용했다.

import * as Location from "expo-location";

  const [permissionStatus, requestPermission] =
    Location.useForegroundPermissions();

Location.useForegroundPermissions()을 통해, 앱이 켜져있을 때 사용자의 위치를 받는 것의 권한을 물어볼 수 있다. 이를 포함하여 askPermission이라는 함수로

const askPermission = async () => {
    if (!permissionStatus || !permissionStatus.granted) {
      const permission = await requestPermission();
      if (!permission.granted) {
        return Alert.alert("위치 권한 필요", "위치 권한을 허용해주세요.");
      }
    }
  };

이렇게 권한을 받도록 하였다. 이 askPermission이라는 코드는

  const [myLocation, setMyLocation] =
    useState<Location.LocationObjectCoords | null>(null);
  const [subscription, setSubscription] =
    useState<Location.LocationSubscription | null>(null);

사용자의 위치를 받는 myLocation과 위치 구독정보를 받는 subscription이라는 상태를 지정해준다음,

// 위치 추적 시작
const startLocationTracking = async () => {
  const sub = await Location.watchPositionAsync(
    {
      accuracy: Location.Accuracy.High,
      timeInterval: 3000, // 3초마다 업데이트
      distanceInterval: 5, // 5m 이동마다 업데이트
    },
    (newLocation) => {
      setMyLocation(newLocation.coords);
    }
  );
  setSubscription(sub);
};

Location.watchPositionAsync() 함수는 사용자의 위치를 실시간으로 받아오게 하고, 이것을 활용하여, 사용자의 위치와 위치 구독 정보를 업데이트한다.

const stopLocationTracking = () => {
  if (subscription) {
    subscription.remove();
    setSubscription(null);
  }
};

위치 추적을 멈추게하는 코드는 다음과 같다. 위치 구독정보를 삭제해주는 코드이다.

이를 useEffect를 통해

useEffect(() => {
  startLocationTracking();
  return () => {
    stopLocationTracking();
  };
}, []);

이런식으로 지정해주어 컴포넌트가 처음 마운트 될 때에 위치추적을 시작하고, 언마운트되면 위치추적을 그만둔다.

initialRegion도 사용자의 위치를 지정해준다.

const mapRef = useRef<MapView>(null);
myLocation && (
  <MapView
    ref = {mapRef}
    style={{ flex: 1 }}
    provider={PROVIDER_GOOGLE}
    initialRegion={{
            latitude: myLocation?.latitude,
            longitude: myLocation?.longitude,
            latitudeDelta: 0.02,
            longitudeDelta: 0.02,
          }}
/>
)

3.3. 지도 중심이 사용자의 위치에 맞게 이동하게 하기

이제 러닝중이라는 것을 가정하고, 현재 위치를 기준으로 지도 중심을 이동시켜 볼 것이다. 우선 MapView의 인스턴스에 직접 접근하기 위해 ref를 사용해야 한한다.

const mapRef = useRef<MapView>(null);

<MapView
  ref = {mapRef}
  style={{ flex: 1 }}
  provider={PROVIDER_GOOGLE}
  {/*...*/}  
>

이런식으로 mapRef를 통해 MapView의 인스턴스에 직접 접근할 수 있도록 한다. 그 다음 만일 사용자의 위치를 카메라가 따라가고 싶게 만들고 싶다면,

const mapRef = useRef<MapView>(null);

useEffect(() => {
  if (myLocation) {
    mapRef.current?.animateCamera({
      center: myLocation,
      zoom: 18,
    });
  }
}, [myLocation]);

이런식으로 아까 지정해 준 myLocationuseEffect의 의존성으로 집어넣어 주고, MapView의 인스턴스중 animateCamera라는 인스턴스를 이렇게 사용하여 myLocation에 카메라가 고정이 되도록(카메라가 사용자의 위치를 따라가도록) 할 수 있다.

문제 해결 : 초기 위치의 범위가 지나치게 커지는 현상

initialRegion의 latitudeDelta와 longitudeDelta가 적용이 되지 않았고, 마커자체는 내 위치이지만 지도 범위가 초기에 대한민국 한반도 전체가 보이는 문제가 발생했다.

myLocation && (
<MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: myLocation?.latitude,
          longitude: myLocation?.longitude,
          latitudeDelta: 0.02,
          longitudeDelta: 0.02,
        }}
      >
      </MapView>
)

initialRegion은 처음에만 위치가 적용이되고, 그 이후에 위치가 바뀌는 것은 적용이 되지 않는다고 하는데, myLocation은 지속적으로 위치추적을 하는 코드이기 때문에 지속적으로 비동기적으로 업데이트가 된다. 그래서 그 부분에서 꼬이지 않았나 생각이 든다.

그래서 이부분에 대해서 고민을 해보았고, 지도의 레이아웃이 처음에 지도를 로드할 때만 바뀌고, 그 이후에는 바뀌지 않는다라는 생각에 MapViewonLayout이에 animateToRegion으로 지도의 중심 및 범위를 이동시키는 코드를 넣어봤다.

  • onLayout: MapView가 화면에 배치될 때(즉, 첫 렌더링될 때) 호출되는 이벤트(뷰의 위치와 크기를 할당하는 단계이다.)
  • animateToRegion(region, duration): 지도 영역을 애니메이션으로 이동하는 함수
<MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: myLocation?.latitude,
          longitude: myLocation?.longitude,
          latitudeDelta: 0.02,
          longitudeDelta: 0.02,
        }}
        onLayout={() => {
              if (mapRef.current) {
                mapRef.current.animateToRegion(
                  {
                    latitude: myLocation?.latitude,
                    longitude: myLocation?.longitude,
                    latitudeDelta: 0.02,
                    longitudeDelta: 0.02,
                  },
                  0
                );
              }
            }}
      />

하지만, 헤더에서 장소를 검색하여 위치를 이동시키는 기능을 구현하던 도중 문제가 발생했는데, 키보드를 열 때마다 지도가 자동으로 리사이징이 되면서 onLayout 안의 코드가 실행이 되는 것이었다.

내가 원하는건 지도가 처음 로드가 될 때 초기에 한번만 지도가 내 위치로 설정이 되는 것인데, 키보드가 열릴때마다 원치않게 내 위치로 지도가 자동 이동이 되는 문제가 발생했다.

결국 onLayout이 아닌 MapView의 또다른 콜백 메소드인 onMapReady를 통해 문제를 해결했다.

  • onMapReady: 지도가 준비되었을 때 호출되는 콜백 메서드이며, 안의 코드는 지도가 준비되었을 때 초기에 한번만 실행된다.
<MapView
        style={styles.map}
        provider={PROVIDER_GOOGLE}
        initialRegion={{
          latitude: myLocation?.latitude,
          longitude: myLocation?.longitude,
          latitudeDelta: 0.02,
          longitudeDelta: 0.02,
        }}
        onMapReady={() => {
              if (mapRef.current) {
                mapRef.current.animateToRegion(
                  {
                    latitude: myLocation?.latitude,
                    longitude: myLocation?.longitude,
                    latitudeDelta: 0.02,
                    longitudeDelta: 0.02,
                  },
                  0
                );
              }
            }}
      />

이제 키보드가 열려도 위치가 이동하지 않았다!