리액트 네이티브로 만드는 초간단 지도 & 위치 기반 서비스 구현 가이드 🗺️📱

안녕, 개발자 친구들! 오늘은 모바일 앱 개발의 꽃이라고 할 수 있는 지도와 위치 기반 서비스를 리액트 네이티브로 어떻게 구현하는지 함께 알아볼 거야. 2025년 현재 가장 핫한 기술 스택과 라이브러리를 활용해서 누구나 따라할 수 있게 설명해 줄게! 🚀
📑 목차
- 리액트 네이티브와 지도 서비스의 만남
- 개발 환경 설정하기
- 지도 라이브러리 선택과 설치
- 기본 지도 구현하기
- 사용자 위치 가져오기
- 마커와 커스텀 오버레이 추가하기
- 경로 찾기 및 거리 계산
- 지오펜싱 구현하기
- 오프라인 지도 지원
- 성능 최적화 팁
- 실전 프로젝트: 내 주변 맛집 찾기 앱
- 배포 및 마무리
🌟 리액트 네이티브와 지도 서비스의 만남
모바일 앱에서 지도와 위치 기능은 이제 선택이 아닌 필수가 되었어. 배달 앱, 데이팅 앱, 여행 앱 등 거의 모든 인기 앱들이 지도 기능을 탑재하고 있지. 특히 2025년 현재는 AR(증강현실)과 결합된 위치 기반 서비스가 대세인데, 이 모든 기능의 기본은 지도 API를 잘 활용하는 거야.
리액트 네이티브는 자바스크립트로 iOS와 안드로이드 앱을 동시에 개발할 수 있는 프레임워크야. 하나의 코드베이스로 두 플랫폼을 모두 지원한다는 점이 가장 큰 장점이지. 여기에 지도 기능을 추가하면 정말 강력한 앱을 만들 수 있어!
"위치 기반 서비스는 단순한 기능이 아니라 사용자 경험의 핵심 요소입니다. 사용자가 어디에 있는지 알면 그에 맞는 최적의 서비스를 제공할 수 있죠."
- 실리콘밸리 UX 디자이너
재능넷에서도 위치 기반 서비스를 활용한 재능 거래가 활발하게 이루어지고 있어. 예를 들어, 내 주변의 사진 촬영 전문가를 찾거나, 가까운 위치의 요리 강사를 찾는 등 지역 기반 재능 매칭이 가능하지. 이런 기능을 직접 구현해보면 재능넷 같은 플랫폼에 대한 이해도 더 깊어질 거야! 😉
⚙️ 개발 환경 설정하기
자, 이제 본격적으로 개발 환경을 세팅해보자! 2025년 기준 최신 버전으로 설정할 거야.
1. Node.js 및 npm 설치
리액트 네이티브를 사용하려면 Node.js가 필요해. 터미널에서 다음 명령어로 버전을 확인해봐:
node -v
npm -v
2025년 3월 기준으로는 Node.js 20.x 이상, npm 10.x 이상을 사용하는 것이 좋아.
2. 리액트 네이티브 CLI 설치
리액트 네이티브 프로젝트를 쉽게 생성하고 관리할 수 있는 CLI를 설치하자:
npm install -g react-native-cli
3. 새 프로젝트 생성
이제 새 프로젝트를 만들어보자. 2025년에는 TypeScript 템플릿이 표준이 되었으니 TS로 시작하는 게 좋아:
npx react-native init MapApp --template react-native-template-typescript
💡 2025년부터는 리액트 네이티브가 새로운 아키텍처(Fabric)를 기본으로 사용하고 있어. 이 아키텍처는 성능이 크게 향상되었고, 특히 지도처럼 복잡한 UI를 다룰 때 더 효율적이야!
4. iOS 및 Android 설정
iOS 개발을 위해서는 Xcode가, Android 개발을 위해서는 Android Studio가 필요해. 각 플랫폼별 설정은 다음과 같아:
iOS 설정:
cd MapApp/ios
pod install
Android 설정:
Android의 경우 build.gradle 파일에서 SDK 버전을 확인하고, 필요하다면 Android Studio를 통해 필요한 SDK를 설치해야 해.
여기까지 했다면 기본적인 개발 환경 설정은 완료된 거야! 이제 지도 라이브러리를 설치해보자. 🎮
🧩 지도 라이브러리 선택과 설치
리액트 네이티브에서 지도를 구현하는 데 사용할 수 있는 라이브러리는 여러 가지가 있어. 2025년 현재 가장 인기 있는 옵션들을 살펴보자!
위 차트에서 볼 수 있듯이, react-native-maps가 가장 인기 있는 선택이야. 이 라이브러리는 Airbnb에서 시작되었고, 현재는 리액트 네이티브 커뮤니티에서 관리하고 있어. 안정적이고 문서화가 잘 되어 있어서 초보자에게도 추천해!
react-native-maps 설치하기
터미널에서 다음 명령어를 실행해 라이브러리를 설치하자:
npm install react-native-maps --save
iOS에서는 추가 설정이 필요해:
cd ios && pod install
API 키 설정
대부분의 지도 서비스는 API 키가 필요해. Google Maps를 사용한다면 다음과 같이 설정해야 해:
iOS (AppDelegate.mm):
#import <googlemaps>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[GMSServices provideAPIKey:@"YOUR_API_KEY"];
// ...
}</googlemaps>
Android (AndroidManifest.xml):
<application>
<!-- ... -->
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_API_KEY"></meta-data>
</application>
⚠️ API 키는 절대 GitHub 같은 공개 저장소에 올리면 안 돼! 환경 변수나 별도의 설정 파일을 사용해서 관리하는 것이 좋아. 2025년에는 API 키 노출로 인한 보안 사고가 더 심각해졌으니 조심하자!
이제 기본적인 라이브러리 설치는 완료됐어. 다음으로 실제 지도를 화면에 표시해보자! 🗺️
🗺️ 기본 지도 구현하기
자, 이제 앱에 지도를 표시해보자! 가장 기본적인 지도 컴포넌트부터 시작할게.
첫 번째 지도 컴포넌트
App.tsx 파일을 열고 다음과 같이 코드를 작성해봐:
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
const App = () => {
return (
<view style="{styles.container}">
<text style="{styles.title}">내 첫 지도 앱</text>
<mapview provider="{PROVIDER_GOOGLE}" style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta:></mapview>
</view>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 20,
fontWeight: 'bold',
padding: 15,
backgroundColor: '#fff',
},
map: {
flex: 1,
},
});
export default App;
이 코드는 서울 시청 부근을 중심으로 하는 기본 지도를 표시해. initialRegion 속성에서 위도(latitude), 경도(longitude), 그리고 지도의 확대/축소 수준을 설정할 수 있어.
💡 latitudeDelta와 longitudeDelta는 지도에 표시될 영역의 크기를 결정해. 값이 작을수록 더 확대된 지도가 표시돼!
지도 유형 변경하기
지도에는 여러 유형이 있어. 표준, 위성, 하이브리드 등 다양한 스타일을 적용할 수 있지:
<mapview provider="{PROVIDER_GOOGLE}" style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta: maptype="satellite"></mapview>
지도 컨트롤 추가하기
사용자가 지도를 확대/축소하거나 이동할 수 있도록 컨트롤을 추가할 수 있어:
<mapview zoomenabled="{true}" scrollenabled="{true}" rotateenabled="{true}" pitchenabled="{true}" showscompass="{true}" showsscale="{true}" showstraffic="{true}" showsbuildings="{true}" showsuserlocation="{true}"></mapview>
이런 속성들을 조합해서 사용자에게 더 나은 지도 경험을 제공할 수 있어. 특히 showsUserLocation 속성은 사용자의 현재 위치를 지도에 표시해주는 아주 유용한 기능이야!
커스텀 스타일 적용하기
Google Maps에서는 지도의 스타일을 커스터마이징할 수 있어. 2025년에는 더 다양한 스타일 옵션이 추가되었지:
const mapStyle = [
{
"elementType": "geometry",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
// 더 많은 스타일 규칙...
];
// 그리고 MapView에 적용:
<mapview custommapstyle="{mapStyle}"></mapview>
이렇게 하면 지도의 색상, 라벨, 도로 등의 스타일을 완전히 바꿀 수 있어. 다크 모드 지도나 앱의 브랜드 색상에 맞는 지도를 만들 수 있지!
이제 기본적인 지도는 구현했어. 다음으로는 사용자의 현재 위치를 가져오는 방법을 알아보자! 📍
📍 사용자 위치 가져오기
위치 기반 서비스의 핵심은 바로 사용자의 현재 위치를 정확하게 파악하는 거야. 리액트 네이티브에서는 Geolocation API를 사용해 사용자의 위치 정보를 가져올 수 있어.
위치 권한 요청하기
사용자 위치를 가져오기 전에 먼저 권한을 요청해야 해. 2025년에는 개인정보 보호가 더욱 강화되어 명시적인 권한 요청이 필수야!
먼저 필요한 패키지를 설치하자:
npm install react-native-permissions
그리고 다음과 같이 권한을 요청할 수 있어:
import React, { useEffect, useState } from 'react';
import { StyleSheet, View, Text, Alert } from 'react-native';
import MapView from 'react-native-maps';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import Geolocation from 'react-native-geolocation-service';
const App = () => {
const [location, setLocation] = useState(null);
useEffect(() => {
requestLocationPermission();
}, []);
const requestLocationPermission = async () => {
const platform = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await check(platform);
switch (result) {
case RESULTS.UNAVAILABLE:
Alert.alert('위치 서비스를 사용할 수 없습니다.');
break;
case RESULTS.DENIED:
const requestResult = await request(platform);
if (requestResult === RESULTS.GRANTED) {
getCurrentLocation();
}
break;
case RESULTS.GRANTED:
getCurrentLocation();
break;
case RESULTS.BLOCKED:
Alert.alert(
'위치 권한이 차단되었습니다.',
'설정에서 위치 권한을 허용해주세요.'
);
break;
}
};
const getCurrentLocation = () => {
Geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
},
(error) => {
console.log(error.code, error.message);
Alert.alert('위치를 가져올 수 없습니다.', error.message);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
};
return (
<view style="{styles.container}">
<text style="{styles.title}">내 위치 찾기</text>
{location ? (
<mapview style="{styles.map}" initialregion="{{" latitude: location.latitude longitude: location.longitude latitudedelta: longitudedelta: showsuserlocation="{true}"></mapview>
) : (
<view style="{styles.loading}">
<text>위치를 가져오는 중...</text>
</view>
)}
</view>
);
};
💡 enableHighAccuracy 옵션을 true로 설정하면 더 정확한 위치를 얻을 수 있지만, 배터리 소모가 증가할 수 있어. 앱의 성격에 따라 적절히 설정하는 것이 좋아!
실시간 위치 추적하기
사용자가 이동할 때마다 위치를 업데이트하고 싶다면 watchPosition 메서드를 사용할 수 있어:
useEffect(() => {
let watchId;
const startLocationTracking = () => {
watchId = Geolocation.watchPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
},
(error) => {
console.log(error.code, error.message);
},
{
enableHighAccuracy: true,
distanceFilter: 10, // 10미터마다 업데이트
interval: 5000, // 5초마다 업데이트 (Android only)
fastestInterval: 2000 // 가장 빠른 업데이트 간격 (Android only)
}
);
};
requestLocationPermission().then(() => {
startLocationTracking();
});
// 컴포넌트가 언마운트될 때 위치 추적 중지
return () => {
if (watchId) {
Geolocation.clearWatch(watchId);
}
};
}, []);
distanceFilter 옵션을 사용하면 사용자가 일정 거리 이상 이동했을 때만 위치가 업데이트돼. 이는 배터리 소모를 줄이는 데 도움이 돼!
위치 정확도 향상시키기
2025년에는 위치 정확도를 높이기 위한 여러 기술이 발전했어. 특히 GPS, 와이파이, 셀룰러 네트워크, 블루투스 비콘 등을 조합해 더 정확한 위치를 얻을 수 있지:
// 고급 위치 설정 (Android)
if (Platform.OS === 'android') {
Geolocation.setRNConfiguration({
enableHighAccuracy: true,
skipPermissionRequests: false,
authorizationLevel: 'whenInUse',
});
}
⚠️ 실시간 위치 추적은 배터리를 많이 소모할 수 있어. 꼭 필요한 경우에만 사용하고, 사용하지 않을 때는 반드시 clearWatch로 추적을 중지해야 해!
이제 사용자의 위치를 가져오는 방법을 알았으니, 다음으로는 지도에 마커와 커스텀 오버레이를 추가하는 방법을 알아보자! 📌
📌 마커와 커스텀 오버레이 추가하기
지도에 마커를 추가하면 특정 위치를 표시하고 정보를 제공할 수 있어. 리액트 네이티브 맵스에서는 Marker 컴포넌트를 사용해 이를 구현할 수 있지.
기본 마커 추가하기
가장 간단한 마커부터 시작해보자:
import React from 'react';
import { StyleSheet, View } from 'react-native';
import MapView, { Marker } from 'react-native-maps';
const App = () => {
return (
<view style="{styles.container}">
<mapview style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta:>
<marker coordinate="{{" latitude: longitude: title="서울 시청" description="서울특별시 중구 세종대로 110"></marker>
</mapview>
</view>
);
};
이렇게 하면 서울 시청 위치에 기본 마커가 표시돼. title과 description 속성을 사용하면 마커를 탭했을 때 정보 창이 표시돼!
여러 마커 표시하기
보통은 하나가 아닌 여러 개의 마커를 표시해야 할 때가 많아. 데이터 배열을 사용해 여러 마커를 효율적으로 표시할 수 있어:
const locations = [
{
id: 1,
title: '서울 시청',
description: '서울특별시 중구 세종대로 110',
coordinate: { latitude: 37.5665, longitude: 126.9780 },
},
{
id: 2,
title: '경복궁',
description: '서울특별시 종로구 사직로 161',
coordinate: { latitude: 37.5796, longitude: 126.9770 },
},
{
id: 3,
title: '남산타워',
description: '서울특별시 용산구 남산공원길 105',
coordinate: { latitude: 37.5512, longitude: 126.9882 },
},
];
// MapView 내부에서:
{locations.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}"></marker>
))}
커스텀 마커 디자인
기본 마커 대신 커스텀 이미지나 컴포넌트를 사용할 수 있어:
<marker coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}">
<view style="{styles.customMarker}">
<text style="{styles.markerText}">🏛️</text>
</view>
</marker>
// 스타일:
const styles = StyleSheet.create({
customMarker: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 20,
borderWidth: 2,
borderColor: 'white',
},
markerText: {
fontSize: 20,
},
});
2025년에는 3D 마커와 애니메이션 효과가 트렌드야. 다음과 같이 애니메이션을 추가할 수 있어:
import React, { useRef, useEffect } from 'react';
import { Animated } from 'react-native';
const AnimatedMarker = ({ coordinate }) => {
const scaleAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.spring(scaleAnim, {
toValue: 1,
friction: 5,
useNativeDriver: true,
}).start();
}, []);
return (
<marker coordinate="{coordinate}">
<animated.view style="{[" styles.custommarker transform: scale: scaleanim>
<text style="{styles.markerText}">🏛️</text>
</animated.view>
</marker>
);
};
커스텀 콜아웃(Callout) 만들기
마커를 탭했을 때 나타나는 정보 창(콜아웃)도 커스터마이징할 수 있어:
<marker coordinate="{marker.coordinate}">
<view style="{styles.customMarker}">
<text style="{styles.markerText}">🏛️</text>
</view>
<callout tooltip>
<view style="{styles.calloutContainer}">
<text style="{styles.calloutTitle}">{marker.title}</text>
<text style="{styles.calloutDescription}">{marker.description}</text>
<image source="{{" uri: marker.imageurl style="{styles.calloutImage}"></image>
<touchableopacity style="{styles.calloutButton}" onpress="{()"> navigateToDetail(marker.id)}
>
<text style="{styles.calloutButtonText}">자세히 보기</text>
</touchableopacity>
</view>
</callout>
</marker>
클러스터링으로 많은 마커 관리하기
마커가 많을 경우 성능 문제가 발생할 수 있어. 이럴 때는 클러스터링 기술을 사용하면 좋아:
import MapView from 'react-native-maps';
import ClusteredMapView from 'react-native-maps-clustering';
// MapView 대신 ClusteredMapView 사용
<clusteredmapview style="{styles.map}" initialregion="{initialRegion}" clustercolor="#3498db" clustertextcolor="#ffffff" clusterbordercolor="#ffffff" clusterborderwidth="{4}" radius="{50}">
{locations.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}"></marker>
))}
</clusteredmapview>
클러스터링을 사용하면 가까운 위치에 있는 여러 마커를 하나로 묶어서 표시할 수 있어. 사용자가 지도를 확대하면 클러스터가 개별 마커로 분리돼!
이제 마커와 오버레이를 사용하는 방법을 알았으니, 다음으로는 경로 찾기와 거리 계산 기능을 구현해보자! 🛣️
🛣️ 경로 찾기 및 거리 계산
위치 기반 앱에서 가장 유용한 기능 중 하나는 두 지점 간의 경로를 찾고 거리를 계산하는 거야. 이 기능을 구현하는 방법을 알아보자!
경로 API 사용하기
경로를 찾기 위해서는 보통 Google Directions API나 MapBox Directions API 같은 외부 API를 사용해. 여기서는 Google Directions API를 사용하는 방법을 알아볼게:
const getDirections = async (startLoc, destinationLoc) => {
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${startLoc}&destination=${destinationLoc}&key=YOUR_API_KEY`
);
const json = await response.json();
if (json.status !== 'OK') {
throw new Error(`경로를 찾을 수 없습니다: ${json.status}`);
}
const points = json.routes[0].overview_polyline.points;
const decodedPoints = decodePolyline(points);
return decodedPoints;
} catch (error) {
console.error(error);
return null;
}
};
여기서 decodePolyline 함수는 Google에서 제공하는 인코딩된 경로 포인트를 디코딩하는 함수야. 이 함수는 직접 구현하거나 라이브러리를 사용할 수 있어:
// polyline 라이브러리 설치: npm install @mapbox/polyline
import polyline from '@mapbox/polyline';
const decodePolyline = (encoded) => {
const points = polyline.decode(encoded);
return points.map(point => ({
latitude: point[0],
longitude: point[1]
}));
};
지도에 경로 표시하기
이제 가져온 경로 데이터를 지도에 표시해보자:
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Button } from 'react-native';
import MapView, { Marker, Polyline } from 'react-native-maps';
const RouteMap = () => {
const [origin] = useState({
latitude: 37.5665,
longitude: 126.9780,
});
const [destination] = useState({
latitude: 37.5113,
longitude: 127.0980,
});
const [routeCoordinates, setRouteCoordinates] = useState([]);
useEffect(() => {
getRoute();
}, []);
const getRoute = async () => {
const startLoc = `${origin.latitude},${origin.longitude}`;
const destinationLoc = `${destination.latitude},${destination.longitude}`;
const points = await getDirections(startLoc, destinationLoc);
if (points) {
setRouteCoordinates(points);
}
};
return (
<view style="{styles.container}">
<mapview style="{styles.map}" initialregion="{{" latitude: destination.latitude longitude: destination.longitude latitudedelta: longitudedelta:>
<marker coordinate="{origin}" title="출발지"></marker>
<marker coordinate="{destination}" title="도착지"></marker>
{routeCoordinates.length > 0 && (
<polyline coordinates="{routeCoordinates}" strokewidth="{4}" strokecolor="#3498db"></polyline>
)}
</mapview>
<button title="경로 다시 찾기" onpress="{getRoute}"></button>
</view>
);
};
Polyline 컴포넌트를 사용해 경로를 선으로 표시할 수 있어. strokeWidth와 strokeColor 속성으로 선의 두께와 색상을 설정할 수 있지!
다양한 이동 수단 지원하기
Google Directions API는 다양한 이동 수단(자동차, 대중교통, 도보, 자전거 등)을 지원해. mode 파라미터를 추가해 이동 수단을 지정할 수 있어:
const getDirections = async (startLoc, destinationLoc, mode = 'driving') => {
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${startLoc}&destination=${destinationLoc}&mode=${mode}&key=YOUR_API_KEY`
);
// 나머지 코드...
} catch (error) {
console.error(error);
return null;
}
};
mode 값으로는 'driving', 'walking', 'bicycling', 'transit' 등을 사용할 수 있어.
거리 및 소요 시간 계산하기
경로 API는 거리와 예상 소요 시간도 제공해. 이 정보를 활용해 사용자에게 유용한 정보를 제공할 수 있어:
const getDirections = async (startLoc, destinationLoc, mode = 'driving') => {
try {
const response = await fetch(
`https://maps.googleapis.com/maps/api/directions/json?origin=${startLoc}&destination=${destinationLoc}&mode=${mode}&key=YOUR_API_KEY`
);
const json = await response.json();
if (json.status !== 'OK') {
throw new Error(`경로를 찾을 수 없습니다: ${json.status}`);
}
const route = json.routes[0];
const leg = route.legs[0];
const distance = leg.distance.text;
const duration = leg.duration.text;
const steps = leg.steps;
const points = route.overview_polyline.points;
const decodedPoints = decodePolyline(points);
return {
distance,
duration,
steps,
coordinates: decodedPoints,
};
} catch (error) {
console.error(error);
return null;
}
};
이제 거리, 소요 시간, 상세 경로 안내 등의 정보를 사용자에게 보여줄 수 있어!
💡 2025년에는 AR 내비게이션이 대세야! 카메라 뷰에 경로 안내를 오버레이하는 기능을 추가하면 사용자 경험을 한층 업그레이드할 수 있어. 이를 위해서는 react-native-camera와 같은 라이브러리와 함께 사용하면 돼!
다음으로는 지오펜싱(Geofencing) 기능을 구현하는 방법을 알아보자! 🔍
🔍 지오펜싱 구현하기
지오펜싱은 특정 지리적 영역(가상의 경계)을 설정하고, 사용자가 그 영역에 들어오거나 나갈 때 알림을 받는 기능이야. 배달 앱, 위치 기반 마케팅, 자동 체크인 등 다양한 용도로 활용할 수 있지!
지오펜스 영역 설정하기
지오펜스 영역은 원형, 다각형, 또는 사각형 등 다양한 형태로 설정할 수 있어. 가장 간단한 원형 지오펜스부터 구현해보자:
import React from 'react';
import { StyleSheet, View, Alert } from 'react-native';
import MapView, { Marker, Circle } from 'react-native-maps';
import { getDistance } from 'geolib';
const GeofencingExample = () => {
const [userLocation, setUserLocation] = useState(null);
const [isInside, setIsInside] = useState(false);
// 지오펜스 중심점과 반경 설정
const geofenceCenter = {
latitude: 37.5665,
longitude: 126.9780,
};
const geofenceRadius = 500; // 미터 단위
useEffect(() => {
// 사용자 위치 가져오기
getCurrentLocation();
// 실시간 위치 추적
const watchId = Geolocation.watchPosition(
(position) => {
const { latitude, longitude } = position.coords;
const newLocation = { latitude, longitude };
setUserLocation(newLocation);
// 지오펜스 체크
checkGeofence(newLocation);
},
(error) => console.log(error),
{ enableHighAccuracy: true, distanceFilter: 10 }
);
return () => Geolocation.clearWatch(watchId);
}, []);
const checkGeofence = (location) => {
// 사용자 위치와 지오펜스 중심 사이의 거리 계산
const distance = getDistance(
{ latitude: location.latitude, longitude: location.longitude },
{ latitude: geofenceCenter.latitude, longitude: geofenceCenter.longitude }
);
const wasInside = isInside;
const nowInside = distance <= geofenceRadius;
if (wasInside !== nowInside) {
setIsInside(nowInside);
if (nowInside) {
Alert.alert('알림', '지오펜스 영역에 들어왔습니다!');
// 여기에 추가 작업 수행 (API 호출, 로컬 알림 등)
} else {
Alert.alert('알림', '지오펜스 영역에서 나갔습니다!');
// 여기에 추가 작업 수행
}
}
};
return (
<view style="{styles.container}">
<mapview style="{styles.map}" initialregion="{{" latitude: geofencecenter.latitude longitude: geofencecenter.longitude latitudedelta: longitudedelta:>
{/* 지오펜스 영역 표시 */}
<circle center="{geofenceCenter}" radius="{geofenceRadius}" strokewidth="{2}" strokecolor="{isInside" : fillcolor="{isInside"></circle>
{/* 지오펜스 중심점 */}
<marker coordinate="{geofenceCenter}" title="지오펜스 중심"></marker>
{/* 사용자 위치 */}
{userLocation && (
<marker coordinate="{userLocation}" title="내 위치" pincolor="{isInside" :></marker>
)}
</mapview>
</view>
);
};
이 코드에서는 geolib 라이브러리를 사용해 사용자 위치와 지오펜스 중심 사이의 거리를 계산하고, 그 거리가 지오펜스 반경보다 작은지 확인해. 사용자가 지오펜스 영역에 들어오거나 나갈 때 알림을 표시하고 있어!
다각형 지오펜스 구현하기
원형 외에도 다각형 형태의 지오펜스를 구현할 수 있어. 이는 더 복잡한 지리적 영역을 정의할 때 유용해:
import { Polygon } from 'react-native-maps';
import { isPointInPolygon } from 'geolib';
// 다각형 지오펜스 좌표 정의
const geofencePolygon = [
{ latitude: 37.5665, longitude: 126.9780 },
{ latitude: 37.5700, longitude: 126.9800 },
{ latitude: 37.5680, longitude: 126.9850 },
{ latitude: 37.5630, longitude: 126.9830 },
];
// 사용자가 다각형 내부에 있는지 확인
const checkPolygonGeofence = (location) => {
const isInPolygon = isPointInPolygon(
{ latitude: location.latitude, longitude: location.longitude },
geofencePolygon
);
// 이전 상태와 비교하여 변경되었을 때만 알림
if (isInPolygon !== isInside) {
setIsInside(isInPolygon);
if (isInPolygon) {
Alert.alert('알림', '지정된 영역에 들어왔습니다!');
} else {
Alert.alert('알림', '지정된 영역에서 나갔습니다!');
}
}
};
// 지도에 다각형 표시
<polygon coordinates="{geofencePolygon}" strokewidth="{2}" strokecolor="{isInside" : fillcolor="{isInside"></polygon>
백그라운드 지오펜싱
앱이 백그라운드 상태일 때도 지오펜싱을 작동시키려면 네이티브 모듈을 사용해야 해. 2025년 현재 가장 인기 있는 라이브러리는 'react-native-geofencing'이야:
import Geofencing from 'react-native-geofencing';
// 지오펜스 등록
const registerGeofence = async () => {
try {
await Geofencing.requestPermissions();
const geofenceId = 'my_geofence_1';
await Geofencing.addGeofence({
id: geofenceId,
latitude: 37.5665,
longitude: 126.9780,
radius: 500,
expiration: 86400000, // 24시간 (밀리초)
notifyOnEntry: true,
notifyOnExit: true,
notifyOnDwell: true,
loiteringDelay: 30000, // 30초 이상 머물러야 dwell 이벤트 발생
});
// 이벤트 리스너 등록
Geofencing.onGeofenceEvent((event) => {
const { geofenceId, transition } = event;
if (transition === Geofencing.GEOFENCE_TRANSITION_ENTER) {
// 영역 진입 시 로컬 알림 표시
LocalNotification.show({
title: '지오펜스 알림',
message: '지정된 영역에 들어왔습니다!',
});
} else if (transition === Geofencing.GEOFENCE_TRANSITION_EXIT) {
LocalNotification.show({
title: '지오펜스 알림',
message: '지정된 영역에서 나갔습니다!',
});
}
});
console.log('지오펜스가 성공적으로 등록되었습니다.');
} catch (error) {
console.error('지오펜스 등록 실패:', error);
}
};
⚠️ 백그라운드 위치 추적은 배터리 소모가 크고 개인정보 이슈가 있어. 반드시 사용자에게 명확한 설명과 함께 권한을 요청하고, 꼭 필요한 경우에만 사용하는 것이 좋아!
지오펜싱 활용 사례
지오펜싱은 다양한 분야에서 활용될 수 있어:
- 리테일: 고객이 매장 근처에 오면 프로모션 알림 전송
- 배달 서비스: 배달원이 목적지에 도착하면 자동 알림
- 출퇴근 관리: 직원이 회사에 도착하면 자동 출근 체크
- 가족 안전: 아이가 학교나 집을 벗어나면 부모에게 알림
- 스마트 홈: 집 근처에 오면 자동으로 조명이나 에어컨 켜기
재능넷에서도 지오펜싱을 활용해 특정 지역에 진입했을 때 해당 지역의 인기 재능 서비스를 추천하는 기능을 구현할 수 있을 거야. 예를 들어, 대학가에 들어서면 과외나 레포트 작성 도움 서비스를, 관광지에 가면 현지 가이드 서비스를 추천하는 식으로 말이야! 🎯
다음으로는 오프라인 환경에서도 지도를 사용할 수 있게 하는 방법을 알아보자! 📱
📱 오프라인 지도 지원
인터넷 연결이 불안정한 지역이나 해외 여행 중에도 앱이 제대로 작동하려면 오프라인 지도 기능이 필수야. 2025년에는 이 기능이 더욱 중요해졌지!
오프라인 지도 데이터 다운로드
오프라인 지도를 구현하는 가장 일반적인 방법은 필요한 지역의 지도 데이터를 미리 다운로드하는 거야. MapBox나 Google Maps는 이런 기능을 API로 제공해:
import MapboxGL from '@react-native-mapbox-gl/maps';
// MapBox 초기화
MapboxGL.setAccessToken('YOUR_MAPBOX_ACCESS_TOKEN');
// 오프라인 지도 다운로드
const downloadOfflineMap = async () => {
try {
// 다운로드할 영역 정의
const boundingBox = [
[126.9628, 37.5400], // 남서쪽 좌표 [경도, 위도]
[127.0228, 37.5800] // 북동쪽 좌표 [경도, 위도]
];
// 오프라인 팩 이름 (고유해야 함)
const packName = `seoul_downtown_${Date.now()}`;
// 오프라인 팩 생성 옵션
const options = {
name: packName,
styleURL: MapboxGL.StyleURL.Street,
bounds: boundingBox,
minZoom: 10,
maxZoom: 15,
};
// 다운로드 진행 상황 모니터링
const progressListener = (offlineRegion, status) => {
console.log(`다운로드 진행률: ${status.percentage}%`);
};
// 다운로드 완료 리스너
const completionListener = (offlineRegion, status) => {
if (status.complete) {
console.log('오프라인 지도 다운로드 완료!');
console.log(`다운로드된 타일: ${status.completedResourceCount}`);
console.log(`사용된 바이트: ${status.completedResourceSize}`);
}
};
// 오프라인 팩 생성 및 다운로드 시작
await MapboxGL.offlineManager.createPack(
options,
progressListener,
completionListener
);
Alert.alert('다운로드 시작', '오프라인 지도 다운로드가 시작되었습니다.');
} catch (error) {
console.error('오프라인 지도 다운로드 실패:', error);
Alert.alert('오류', '오프라인 지도 다운로드에 실패했습니다.');
}
};
오프라인 팩 관리하기
다운로드한 오프라인 지도 팩을 관리하는 기능도 필요해:
// 모든 오프라인 팩 목록 가져오기
const listOfflinePacks = async () => {
try {
const packs = await MapboxGL.offlineManager.getPacks();
console.log('오프라인 팩 목록:', packs);
return packs;
} catch (error) {
console.error('오프라인 팩 목록 가져오기 실패:', error);
return [];
}
};
// 특정 오프라인 팩 삭제하기
const deleteOfflinePack = async (packName) => {
try {
const packs = await MapboxGL.offlineManager.getPacks();
const packToDelete = packs.find(pack => pack.name === packName);
if (packToDelete) {
await MapboxGL.offlineManager.deletePack(packToDelete.name);
console.log(`오프라인 팩 "${packName}" 삭제 완료`);
return true;
} else {
console.log(`오프라인 팩 "${packName}"을(를) 찾을 수 없습니다.`);
return false;
}
} catch (error) {
console.error('오프라인 팩 삭제 실패:', error);
return false;
}
};
오프라인 경로 찾기
오프라인 상태에서도 경로 찾기가 가능하도록 구현할 수 있어. 이를 위해서는 경로 데이터도 미리 다운로드해야 해:
// 주요 경로 미리 다운로드
const preloadRoutes = async (startPoints, endPoints) => {
try {
const routes = [];
for (const start of startPoints) {
for (const end of endPoints) {
const startLoc = `${start.latitude},${start.longitude}`;
const endLoc = `${end.latitude},${end.longitude}`;
// 경로 데이터 가져오기
const routeData = await getDirections(startLoc, endLoc);
if (routeData) {
routes.push({
id: `${startLoc}_to_${endLoc}`,
startPoint: start,
endPoint: end,
...routeData
});
}
}
}
// 경로 데이터를 로컬 스토리지에 저장
await AsyncStorage.setItem('offlineRoutes', JSON.stringify(routes));
console.log(`${routes.length}개의 경로가 오프라인 사용을 위해 저장되었습니다.`);
return routes;
} catch (error) {
console.error('경로 미리 다운로드 실패:', error);
return [];
}
};
// 오프라인 상태에서 경로 찾기
const findOfflineRoute = async (startPoint, endPoint) => {
try {
// 저장된 경로 데이터 가져오기
const routesJson = await AsyncStorage.getItem('offlineRoutes');
if (!routesJson) {
throw new Error('저장된 오프라인 경로가 없습니다.');
}
const routes = JSON.parse(routesJson);
// 가장 가까운 시작점과 도착점 찾기
const nearestStart = findNearestPoint(startPoint, routes.map(r => r.startPoint));
const nearestEnd = findNearestPoint(endPoint, routes.map(r => r.endPoint));
// 해당 경로 찾기
const route = routes.find(
r => isPointsClose(r.startPoint, nearestStart) && isPointsClose(r.endPoint, nearestEnd)
);
if (route) {
return route;
} else {
throw new Error('해당 지점 사이의 오프라인 경로를 찾을 수 없습니다.');
}
} catch (error) {
console.error('오프라인 경로 찾기 실패:', error);
return null;
}
};
💡 2025년에는 AI 기반 오프라인 경로 예측이 발전했어. 적은 양의 데이터로도 다양한 경로를 예측할 수 있는 알고리즘이 개발되었지. 이를 활용하면 모든 경로를 미리 다운로드하지 않아도 괜찮아!
오프라인 POI(관심 지점) 데이터
지도에 표시할 관심 지점(POI) 데이터도 오프라인으로 사용할 수 있게 준비하자:
// POI 데이터 다운로드 및 저장
const downloadPOIData = async (region) => {
try {
// 서버에서 POI 데이터 가져오기
const response = await fetch(
`https://api.example.com/pois?lat=${region.latitude}&lng=${region.longitude}&radius=5000`
);
const pois = await response.json();
// 로컬에 저장
await AsyncStorage.setItem(`pois_${region.latitude}_${region.longitude}`, JSON.stringify(pois));
console.log(`${pois.length}개의 POI 데이터가 저장되었습니다.`);
return pois;
} catch (error) {
console.error('POI 데이터 다운로드 실패:', error);
return [];
}
};
// 오프라인 POI 데이터 사용
const getOfflinePOIs = async (region) => {
try {
// 가장 가까운 저장된 지역 찾기
const keys = await AsyncStorage.getAllKeys();
const poiKeys = keys.filter(key => key.startsWith('pois_'));
if (poiKeys.length === 0) {
throw new Error('저장된 POI 데이터가 없습니다.');
}
// 현재 위치와 가장 가까운 저장된 POI 데이터 찾기
let nearestKey = poiKeys[0];
let minDistance = Infinity;
for (const key of poiKeys) {
const [, lat, lng] = key.split('_');
const distance = getDistance(
{ latitude: region.latitude, longitude: region.longitude },
{ latitude: parseFloat(lat), longitude: parseFloat(lng) }
);
if (distance < minDistance) {
minDistance = distance;
nearestKey = key;
}
}
// 데이터 가져오기
const poisJson = await AsyncStorage.getItem(nearestKey);
return JSON.parse(poisJson);
} catch (error) {
console.error('오프라인 POI 데이터 가져오기 실패:', error);
return [];
}
};
이렇게 오프라인 지도, 경로, POI 데이터를 모두 준비하면 인터넷 연결 없이도 앱의 핵심 기능을 사용할 수 있어! 특히 해외 여행이나 산악 지역 등 인터넷 연결이 불안정한 곳에서 유용하지.
다음으로는 지도 앱의 성능을 최적화하는 방법을 알아보자! 🚀
🚀 성능 최적화 팁
지도 앱은 리소스를 많이 사용하는 앱 중 하나야. 특히 많은 마커나 오버레이를 표시할 때 성능 이슈가 발생할 수 있어. 2025년 기준으로 가장 효과적인 성능 최적화 방법을 알아보자!
1. 마커 클러스터링 활용
앞서 소개했던 클러스터링은 성능 최적화에 매우 중요해. 수백 개의 마커를 개별적으로 렌더링하는 대신 가까운 마커들을 그룹화하면 렌더링 부하를 크게 줄일 수 있어:
import ClusteredMapView from 'react-native-maps-clustering';
<clusteredmapview style="{styles.map}" data="{markers}" initialregion="{initialRegion}" clustercolor="#3498db" clustertextcolor="#ffffff" radius="{50}" extent="{512}" nodesize="{64}" minzoom="{1}" maxzoom="{20}" rendercluster="{(cluster," onpress> {
const { pointCount } = cluster.properties;
return (
<marker coordinate="{cluster.geometry.coordinates}" onpress="{onPress}">
<view style="{styles.clusterContainer}">
<text style="{styles.clusterText}">{pointCount}</text>
</view>
</marker>
);
}}
renderMarker={(marker) => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}"></marker>
)}
/></clusteredmapview>
2. 지연 로딩(Lazy Loading) 구현
사용자의 현재 화면에 보이는 영역에 대한 데이터만 로드하고, 화면 밖의 데이터는 필요할 때만 로드하는 방식을 구현하자:
const MapWithLazyLoading = () => {
const [visibleMarkers, setVisibleMarkers] = useState([]);
const [allMarkers, setAllMarkers] = useState([]);
// 지도 영역이 변경될 때 호출되는 함수
const onRegionChangeComplete = (region) => {
// 현재 보이는 영역 내의 마커만 필터링
const markersInView = allMarkers.filter(marker =>
isMarkerInRegion(marker.coordinate, region)
);
setVisibleMarkers(markersInView);
};
// 마커가 현재 지도 영역 내에 있는지 확인
const isMarkerInRegion = (coordinate, region) => {
const { latitude, longitude, latitudeDelta, longitudeDelta } = region;
const latDistance = latitudeDelta / 2;
const lngDistance = longitudeDelta / 2;
return (
coordinate.latitude <= latitude + latDistance &&
coordinate.latitude >= latitude - latDistance &&
coordinate.longitude <= longitude + lngDistance &&
coordinate.longitude >= longitude - lngDistance
);
};
return (
<mapview style="{styles.map}" initialregion="{initialRegion}" onregionchangecomplete="{onRegionChangeComplete}">
{visibleMarkers.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}"></marker>
))}
</mapview>
);
};
3. 메모이제이션 활용
React의 useMemo와 useCallback 훅을 사용해 불필요한 재계산과 렌더링을 방지하자:
import React, { useState, useMemo, useCallback } from 'react';
const OptimizedMap = ({ markers, initialRegion }) => {
const [region, setRegion] = useState(initialRegion);
// 마커 렌더링 함수 메모이제이션
const renderMarkers = useMemo(() => {
return markers.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}"></marker>
));
}, [markers]); // markers가 변경될 때만 재계산
// 지도 영역 변경 핸들러 메모이제이션
const onRegionChange = useCallback((newRegion) => {
setRegion(newRegion);
}, []);
return (
<mapview style="{styles.map}" region="{region}" onregionchange="{onRegionChange}">
{renderMarkers}
</mapview>
);
};
4. 네이티브 드라이버 활용
애니메이션이나 인터랙션에 네이티브 드라이버를 사용하면 성능이 크게 향상돼:
import { Animated } from 'react-native';
// 마커 애니메이션에 네이티브 드라이버 사용
const AnimatedMarker = ({ coordinate }) => {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true, // 네이티브 드라이버 활성화
}).start();
}, []);
return (
<marker coordinate="{coordinate}">
<animated.view style="{[" styles.custommarker opacity>
<text style="{styles.markerText}">📍</text>
</animated.view>
</marker>
);
};
5. 이미지 및 에셋 최적화
마커나 오버레이에 사용되는 이미지는 크기와 해상도를 최적화하는 것이 중요해:
// 이미지 크기에 따라 다른 해상도 사용
const getMarkerImage = (type, size = 'medium') => {
const sizes = {
small: '@1x',
medium: '@2x',
large: '@3x'
};
return {
uri: `https://example.com/markers/${type}${sizes[size]}.png`
};
};
6. 웹워커 활용
2025년에는 리액트 네이티브에서도 웹워커를 활용한 멀티스레딩이 가능해졌어. 복잡한 계산은 별도의 스레드에서 처리하자:
import { Worker } from 'react-native-workers';
// 웹워커 생성
const worker = new Worker('calculateRoutes.js');
// 메인 스레드에서 작업 요청
worker.postMessage({
startPoint: { latitude: 37.5665, longitude: 126.9780 },
endPoint: { latitude: 37.5113, longitude: 127.0980 },
waypoints: waypoints
});
// 결과 받기
worker.onmessage = (message) => {
const { routes } = message.data;
setOptimalRoute(routes[0]);
};
// 사용 완료 후 워커 종료
useEffect(() => {
return () => {
worker.terminate();
};
}, []);
💡 2025년에는 WebAssembly가 리액트 네이티브에서도 완벽하게 지원돼. 복잡한 경로 계산이나 지오펜싱 알고리즘을 C++이나 Rust로 작성하고 WebAssembly로 컴파일하면 JavaScript보다 훨씬 빠른 성능을 얻을 수 있어!
7. 렌더링 최적화
불필요한 렌더링을 방지하기 위해 React.memo와 PureComponent를 활용하자:
// 마커 컴포넌트 최적화
const MarkerItem = React.memo(({ coordinate, title, onPress }) => {
return (
<marker coordinate="{coordinate}" title="{title}" onpress="{onPress}"></marker>
);
}, (prevProps, nextProps) => {
// 좌표가 같으면 리렌더링하지 않음
return (
prevProps.coordinate.latitude === nextProps.coordinate.latitude &&
prevProps.coordinate.longitude === nextProps.coordinate.longitude &&
prevProps.title === nextProps.title
);
});
이런 최적화 기법들을 적용하면 수백 개의 마커가 있는 지도도 부드럽게 작동하는 앱을 만들 수 있어! 특히 오래된 기기나 저사양 기기에서도 좋은 사용자 경험을 제공할 수 있지.
이제 배운 내용을 활용해 실전 프로젝트를 만들어보자! 🍽️
🍽️ 실전 프로젝트: 내 주변 맛집 찾기 앱
지금까지 배운 내용을 종합해서 간단한 맛집 찾기 앱을 만들어보자! 이 앱은 사용자 위치 기반으로 주변 맛집을 보여주고, 경로 안내와 리뷰 기능을 제공할 거야.
1. 프로젝트 구조 설정
먼저 프로젝트의 폴더 구조를 설정하자:
FoodMapApp/
├── src/
│ ├── components/
│ │ ├── Map.tsx
│ │ ├── RestaurantMarker.tsx
│ │ ├── RestaurantCard.tsx
│ │ ├── SearchBar.tsx
│ │ └── FilterOptions.tsx
│ ├── screens/
│ │ ├── HomeScreen.tsx
│ │ ├── RestaurantDetailScreen.tsx
│ │ ├── RouteScreen.tsx
│ │ └── SettingsScreen.tsx
│ ├── services/
│ │ ├── locationService.ts
│ │ ├── restaurantService.ts
│ │ └── mapService.ts
│ ├── utils/
│ │ ├── permissions.ts
│ │ ├── distance.ts
│ │ └── storage.ts
│ ├── hooks/
│ │ ├── useLocation.ts
│ │ └── useRestaurants.ts
│ ├── context/
│ │ └── AppContext.tsx
│ └── App.tsx
├── assets/
│ ├── markers/
│ └── icons/
└── package.json
2. 주요 기능 구현
이제 앱의 핵심 기능을 구현해보자!
위치 서비스 구현 (locationService.ts)
import Geolocation from 'react-native-geolocation-service';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
export const requestLocationPermission = async () => {
const platform = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await check(platform);
if (result === RESULTS.DENIED) {
const requestResult = await request(platform);
return requestResult === RESULTS.GRANTED;
}
return result === RESULTS.GRANTED;
};
export const getCurrentLocation = () => {
return new Promise((resolve, reject) => {
Geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});
},
(error) => {
reject(error);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
});
};
export const watchLocation = (callback) => {
const watchId = Geolocation.watchPosition(
(position) => {
const { latitude, longitude } = position.coords;
callback({ latitude, longitude });
},
(error) => console.log(error),
{ enableHighAccuracy: true, distanceFilter: 10 }
);
return watchId;
};
맛집 데이터 서비스 (restaurantService.ts)
import AsyncStorage from '@react-native-async-storage/async-storage';
// 맛집 API에서 데이터 가져오기
export const fetchRestaurants = async (latitude, longitude, radius = 1000) => {
try {
// 실제 앱에서는 여기에 맛집 API 호출 코드가 들어갑니다
// 예: Google Places API, Yelp API 등
// 예시 데이터
const response = await fetch(
`https://api.example.com/restaurants?lat=${latitude}&lng=${longitude}&radius=${radius}`
);
if (!response.ok) {
throw new Error('맛집 데이터를 가져오는데 실패했습니다');
}
const data = await response.json();
// 오프라인 사용을 위해 데이터 캐싱
await AsyncStorage.setItem(
`restaurants_${latitude.toFixed(3)}_${longitude.toFixed(3)}`,
JSON.stringify(data)
);
return data;
} catch (error) {
console.error('온라인 데이터 가져오기 실패:', error);
// 오프라인 캐시에서 데이터 가져오기 시도
try {
const cachedData = await AsyncStorage.getItem(
`restaurants_${latitude.toFixed(3)}_${longitude.toFixed(3)}`
);
if (cachedData) {
return JSON.parse(cachedData);
}
} catch (cacheError) {
console.error('캐시 데이터 가져오기 실패:', cacheError);
}
// 샘플 데이터 반환 (실제 앱에서는 오류 처리 필요)
return getSampleRestaurants(latitude, longitude);
}
};
// 샘플 맛집 데이터 (API 실패 시 폴백용)
const getSampleRestaurants = (latitude, longitude) => {
return [
{
id: '1',
name: '맛있는 한식당',
rating: 4.5,
priceLevel: 2,
vicinity: '서울시 강남구 역삼동 123',
photos: ['https://example.com/photo1.jpg'],
coordinate: {
latitude: latitude + 0.001,
longitude: longitude + 0.001,
},
categories: ['한식', '찌개'],
},
{
id: '2',
name: '스시 오마카세',
rating: 4.8,
priceLevel: 4,
vicinity: '서울시 강남구 역삼동 456',
photos: ['https://example.com/photo2.jpg'],
coordinate: {
latitude: latitude - 0.001,
longitude: longitude + 0.002,
},
categories: ['일식', '스시'],
},
// 더 많은 샘플 데이터...
];
};
커스텀 훅: useRestaurants
import { useState, useEffect } from 'react';
import { fetchRestaurants } from '../services/restaurantService';
import { getCurrentLocation } from '../services/locationService';
export const useRestaurants = (radius = 1000) => {
const [restaurants, setRestaurants] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [location, setLocation] = useState(null);
useEffect(() => {
const loadData = async () => {
try {
setLoading(true);
// 현재 위치 가져오기
const currentLocation = await getCurrentLocation();
setLocation(currentLocation);
// 맛집 데이터 가져오기
const data = await fetchRestaurants(
currentLocation.latitude,
currentLocation.longitude,
radius
);
setRestaurants(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
loadData();
}, [radius]);
const refreshRestaurants = async () => {
if (!location) return;
try {
setLoading(true);
const data = await fetchRestaurants(
location.latitude,
location.longitude,
radius
);
setRestaurants(data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return { restaurants, loading, error, location, refreshRestaurants };
};
메인 지도 컴포넌트 (Map.tsx)
import React, { useRef, useEffect, useState } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import MapView, { Marker, Callout, PROVIDER_GOOGLE } from 'react-native-maps';
import { useRestaurants } from '../hooks/useRestaurants';
import RestaurantMarker from './RestaurantMarker';
import RestaurantCard from './RestaurantCard';
const Map = ({ navigation }) => {
const { restaurants, loading, error, location } = useRestaurants(1500);
const [selectedRestaurant, setSelectedRestaurant] = useState(null);
const mapRef = useRef(null);
useEffect(() => {
if (location && mapRef.current) {
mapRef.current.animateToRegion({
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
});
}
}, [location]);
const handleMarkerPress = (restaurant) => {
setSelectedRestaurant(restaurant);
// 선택된 맛집으로 지도 이동
mapRef.current.animateToRegion({
latitude: restaurant.coordinate.latitude,
longitude: restaurant.coordinate.longitude,
latitudeDelta: 0.005,
longitudeDelta: 0.005,
});
};
const handleGetDirections = () => {
if (!selectedRestaurant || !location) return;
navigation.navigate('Route', {
startLocation: location,
endLocation: selectedRestaurant.coordinate,
restaurantName: selectedRestaurant.name,
});
};
if (loading && !restaurants.length) {
return (
<view style="{styles.centered}">
<text>맛집을 찾는 중...</text>
</view>
);
}
if (error && !restaurants.length) {
return (
<view style="{styles.centered}">
<text>오류 발생: {error}</text>
</view>
);
}
return (
<view style="{styles.container}">
<mapview ref="{mapRef}" provider="{PROVIDER_GOOGLE}" style="{styles.map}" initialregion="{" location latitude: location.latitude longitude: location.longitude latitudedelta: longitudedelta: : undefined showsuserlocation showsmylocationbutton>
{restaurants.map((restaurant) => (
<restaurantmarker key="{restaurant.id}" restaurant="{restaurant}" onpress="{()"> handleMarkerPress(restaurant)}
isSelected={selectedRestaurant?.id === restaurant.id}
/>
))}
</restaurantmarker></mapview>
{location && (
<marker coordinate="{location}" title="내 위치" pincolor="blue"></marker>
)}
{selectedRestaurant && (
<view style="{styles.cardContainer}">
<restaurantcard restaurant="{selectedRestaurant}" ondirectionspress="{handleGetDirections}" ondetailspress="{()">
navigation.navigate('RestaurantDetail', { restaurant: selectedRestaurant })
}
onClose={() => setSelectedRestaurant(null)}
/>
</restaurantcard></view>
)}
</view>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
flex: 1,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
cardContainer: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
},
});
export default Map;
경로 안내 화면 (RouteScreen.tsx)
import React, { useEffect, useState } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import MapView, { Marker, Polyline } from 'react-native-maps';
import { getDirections } from '../services/mapService';
const RouteScreen = ({ route, navigation }) => {
const { startLocation, endLocation, restaurantName } = route.params;
const [routeData, setRouteData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchRoute = async () => {
try {
setLoading(true);
const startLoc = `${startLocation.latitude},${startLocation.longitude}`;
const endLoc = `${endLocation.latitude},${endLocation.longitude}`;
const data = await getDirections(startLoc, endLoc);
setRouteData(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchRoute();
}, [startLocation, endLocation]);
if (loading) {
return (
<view style="{styles.centered}">
<text>경로를 계산하는 중...</text>
</view>
);
}
if (error) {
return (
<view style="{styles.centered}">
<text>경로를 찾을 수 없습니다: {error}</text>
<touchableopacity style="{styles.button}" onpress="{()"> navigation.goBack()}
>
<text style="{styles.buttonText}">돌아가기</text>
</touchableopacity>
</view>
);
}
return (
<view style="{styles.container}">
<view style="{styles.header}">
<text style="{styles.title}">{restaurantName}까지의 경로</text>
{routeData && (
<view style="{styles.infoContainer}">
<text style="{styles.infoText}">거리: {routeData.distance}</text>
<text style="{styles.infoText}">예상 소요 시간: {routeData.duration}</text>
</view>
)}
</view>
<mapview style="{styles.map}" initialregion="{{" latitude: endlocation.latitude longitude: endlocation.longitude latitudedelta: longitudedelta:>
<marker coordinate="{startLocation}" title="내 위치" pincolor="blue"></marker>
<marker coordinate="{endLocation}" title="{restaurantName}" pincolor="red"></marker>
{routeData && (
<polyline coordinates="{routeData.coordinates}" strokewidth="{4}" strokecolor="#3498db"></polyline>
)}
</mapview>
{routeData && routeData.steps && (
<view style="{styles.stepsContainer}">
<text style="{styles.stepsTitle}">상세 경로 안내</text>
{routeData.steps.map((step, index) => (
<view key="{index}" style="{styles.stepItem}">
<text style="{styles.stepNumber}">{index + 1}</text>
<text style="{styles.stepText}">{step.html_instructions.replace(/<[^>]*>/g, ' ')}</text>
<text style="{styles.stepDistance}">{step.distance.text}</text>
</view>
))}
</view>
)}
</view>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
padding: 15,
backgroundColor: '#fff',
},
title: {
fontSize: 18,
fontWeight: 'bold',
},
infoContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 5,
},
infoText: {
fontSize: 14,
color: '#666',
},
map: {
height: 300,
},
stepsContainer: {
flex: 1,
padding: 15,
backgroundColor: '#fff',
},
stepsTitle: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
stepItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
stepNumber: {
width: 25,
height: 25,
borderRadius: 12.5,
backgroundColor: '#3498db',
color: '#fff',
textAlign: 'center',
lineHeight: 25,
marginRight: 10,
},
stepText: {
flex: 1,
fontSize: 14,
},
stepDistance: {
fontSize: 12,
color: '#666',
marginLeft: 10,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
button: {
marginTop: 20,
backgroundColor: '#3498db',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 5,
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
},
});
export default RouteScreen;
3. 앱 실행 및 테스트
이제 앱을 실행하고 테스트해보자! 다음 명령어로 앱을 실행할 수 있어:
npx react-native run-ios
// 또는
npx react-native run-android
실제 기기나 에뮬레이터에서 앱을 테스트해보면서 다음 사항을 확인해봐:
- 위치 권한 요청이 제대로 작동하는지
- 현재 위치가 정확하게 표시되는지
- 주변 맛집이 마커로 표시되는지
- 마커를 탭했을 때 맛집 정보가 표시되는지
- 경로 안내 기능이 제대로 작동하는지
- 오프라인 상태에서도 기본 기능이 작동하는지
이 프로젝트는 기본적인 기능만 구현한 것이야. 실제 앱에서는 리뷰 작성, 즐겨찾기, 필터링, 검색 등 더 많은 기능을 추가할 수 있어!
재능넷에서도 이런 위치 기반 서비스를 활용해 주변의 재능 제공자를 찾는 기능을 구현할 수 있을 거야. 예를 들어, 내 주변의 피아노 강사나 영어 회화 튜터를 지도에서 바로 찾을 수 있다면 정말 편리하겠지? 🎯
🚀 배포 및 마무리
이제 앱 개발이 완료되었으니 배포 준비를 해보자! 2025년 현재 앱 배포 과정은 더 간소화되었지만, 여전히 몇 가지 중요한 단계가 있어.
1. 앱 성능 최종 점검
배포 전에 앱의 성능을 최종 점검하는 것이 중요해:
- 메모리 누수 확인: React DevTools와 Flipper를 사용해 메모리 누수 확인
- 렌더링 성능 확인: 많은 마커가 있을 때도 부드럽게 작동하는지 확인
- 배터리 소모 테스트: 백그라운드에서 위치 추적 시 배터리 소모 확인
- 네트워크 오류 처리: 오프라인 상태나 네트워크 오류 시 적절히 처리되는지 확인
- 다양한 기기 테스트: 다양한 화면 크기와 OS 버전에서 테스트
2. 앱 아이콘 및 스플래시 스크린 설정
앱의 첫인상을 결정하는 아이콘과 스플래시 스크린을 설정하자:
// react-native-splash-screen 설치
npm install react-native-splash-screen --save
// App.tsx에서 스플래시 스크린 설정
import SplashScreen from 'react-native-splash-screen';
useEffect(() => {
// 앱 초기화 후 스플래시 스크린 숨기기
const initApp = async () => {
await requestLocationPermission();
SplashScreen.hide();
};
initApp();
}, []);
3. 앱 번들링 및 서명
앱 스토어에 제출하기 전에 앱을 번들링하고 서명해야 해:
Android 앱 번들링:
cd android
./gradlew bundleRelease
iOS 앱 번들링:
Xcode를 사용해 Archive를 생성하고 App Store Connect에 업로드해.
4. 앱 스토어 등록 준비
앱 스토어에 등록하기 위한 자료를 준비하자:
- 스크린샷: 다양한 기기용 스크린샷 (iPhone, iPad, Android 폰, 태블릿)
- 앱 설명: 앱의 주요 기능과 특징을 설명하는 마케팅 텍스트
- 개인정보 처리방침: 위치 데이터 수집에 관한 명확한 정책
- 키워드: 앱 검색에 사용될 키워드 (지도, 위치, 맛집, 네비게이션 등)
- 연령 등급: 앱의 콘텐츠에 맞는 연령 등급 설정
5. 앱 출시 후 모니터링
앱이 출시된 후에도 지속적인 모니터링과 개선이 필요해:
- 크래시 리포트: Firebase Crashlytics 등을 사용해 크래시 모니터링
- 사용자 피드백: 앱 스토어 리뷰와 인앱 피드백 확인
- 성능 메트릭: 앱 시작 시간, 지도 로딩 시간 등 성능 지표 모니터링
- 사용자 행동 분석: 어떤 기능을 가장 많이 사용하는지 분석
- 정기 업데이트: 버그 수정 및 새로운 기능 추가
💡 2025년에는 AI 기반 사용자 피드백 분석이 대세야! 사용자 리뷰와 피드백을 자동으로 분석해 개선 포인트를 추출하는 도구를 활용하면 더 효율적으로 앱을 발전시킬 수 있어!
6. 향후 개선 방향
앱의 첫 버전 출시 후 고려할 수 있는 향후 개선 방향:
- AR 내비게이션: 카메라 뷰에 경로 안내를 오버레이하는 AR 기능
- 소셜 기능: 친구와 맛집 정보 공유, 그룹 방문 계획 수립
- AI 추천: 사용자의 취향을 학습해 맞춤형 맛집 추천
- 예약 통합: 앱 내에서 직접 맛집 예약 기능
- 웨어러블 지원: Apple Watch, Galaxy Watch 등에서 간단한 경로 안내
위치 기반 서비스는 계속해서 발전하고 있어. 2025년 현재는 AR과 AI의 결합이 트렌드이며, 앞으로 더 많은 혁신이 이루어질 거야. 이 튜토리얼이 여러분의 위치 기반 앱 개발에 도움이 되었길 바라!
재능넷에서도 이런 위치 기반 기술을 활용해 더 혁신적인 재능 거래 플랫폼을 만들 수 있을 거야. 위치 기반 서비스는 단순한 기능이 아니라 사용자 경험을 완전히 바꿀 수 있는 강력한 도구니까! 🌟
🎯 마치며
지금까지 리액트 네이티브에서 지도 및 위치 기반 서비스를 구현하는 방법에 대해 알아봤어. 기본적인 지도 표시부터 시작해서 사용자 위치 추적, 마커 추가, 경로 찾기, 지오펜싱, 오프라인 지도 지원까지 다양한 기능을 구현해봤지!
위치 기반 서비스는 2025년 현재 모바일 앱의 핵심 기능 중 하나로, 사용자에게 맞춤형 경험을 제공하는 데 큰 역할을 해. 특히 AR과 결합된 위치 기반 서비스는 앞으로 더욱 발전할 전망이야.
이 튜토리얼에서 배운 내용을 바탕으로 여러분만의 창의적인 위치 기반 앱을 만들어보길 바라! 택시 앱, 배달 앱, 여행 가이드 앱, 피트니스 트래킹 앱 등 다양한 분야에서 활용할 수 있을 거야.
마지막으로, 위치 데이터는 민감한 개인정보라는 점을 항상 명심하고, 사용자의 프라이버시를 존중하는 앱을 만들어야 해. 필요한 권한만 요청하고, 수집한 데이터를 안전하게 관리하는 것이 중요해!
행운을 빌어! 멋진 위치 기반 앱을 만들어보자! 🚀
📑 목차
- 리액트 네이티브와 지도 서비스의 만남
- 개발 환경 설정하기
- 지도 라이브러리 선택과 설치
- 기본 지도 구현하기
- 사용자 위치 가져오기
- 마커와 커스텀 오버레이 추가하기
- 경로 찾기 및 거리 계산
- 지오펜싱 구현하기
- 오프라인 지도 지원
- 성능 최적화 팁
- 실전 프로젝트: 내 주변 맛집 찾기 앱
- 배포 및 마무리
🌟 리액트 네이티브와 지도 서비스의 만남
모바일 앱에서 지도와 위치 기능은 이제 선택이 아닌 필수가 되었어. 배달 앱, 데이팅 앱, 여행 앱 등 거의 모든 인기 앱들이 지도 기능을 탑재하고 있지. 특히 2025년 현재는 AR(증강현실)과 결합된 위치 기반 서비스가 대세인데, 이 모든 기능의 기본은 지도 API를 잘 활용하는 거야.
리액트 네이티브는 자바스크립트로 iOS와 안드로이드 앱을 동시에 개발할 수 있는 프레임워크야. 하나의 코드베이스로 두 플랫폼을 모두 지원한다는 점이 가장 큰 장점이지. 여기에 지도 기능을 추가하면 정말 강력한 앱을 만들 수 있어!
"위치 기반 서비스는 단순한 기능이 아니라 사용자 경험의 핵심 요소입니다. 사용자가 어디에 있는지 알면 그에 맞는 최적의 서비스를 제공할 수 있죠."
- 실리콘밸리 UX 디자이너
재능넷에서도 위치 기반 서비스를 활용한 재능 거래가 활발하게 이루어지고 있어. 예를 들어, 내 주변의 사진 촬영 전문가를 찾거나, 가까운 위치의 요리 강사를 찾는 등 지역 기반 재능 매칭이 가능하지. 이런 기능을 직접 구현해보면 재능넷 같은 플랫폼에 대한 이해도 더 깊어질 거야! 😉
⚙️ 개발 환경 설정하기
자, 이제 본격적으로 개발 환경을 세팅해보자! 2025년 기준 최신 버전으로 설정할 거야.
1. Node.js 및 npm 설치
리액트 네이티브를 사용하려면 Node.js가 필요해. 터미널에서 다음 명령어로 버전을 확인해봐:
node -v
npm -v
2025년 3월 기준으로는 Node.js 20.x 이상, npm 10.x 이상을 사용하는 것이 좋아.
2. 리액트 네이티브 CLI 설치
리액트 네이티브 프로젝트를 쉽게 생성하고 관리할 수 있는 CLI를 설치하자:
npm install -g react-native-cli
3. 새 프로젝트 생성
이제 새 프로젝트를 만들어보자. 2025년에는 TypeScript 템플릿이 표준이 되었으니 TS로 시작하는 게 좋아:
npx react-native init MapApp --template react-native-template-typescript
💡 2025년부터는 리액트 네이티브가 새로운 아키텍처(Fabric)를 기본으로 사용하고 있어. 이 아키텍처는 성능이 크게 향상되었고, 특히 지도처럼 복잡한 UI를 다룰 때 더 효율적이야!
4. iOS 및 Android 설정
iOS 개발을 위해서는 Xcode가, Android 개발을 위해서는 Android Studio가 필요해. 각 플랫폼별 설정은 다음과 같아:
iOS 설정:
cd MapApp/ios
pod install
Android 설정:
Android의 경우 build.gradle 파일에서 SDK 버전을 확인하고, 필요하다면 Android Studio를 통해 필요한 SDK를 설치해야 해.
여기까지 했다면 기본적인 개발 환경 설정은 완료된 거야! 이제 지도 라이브러리를 설치해보자. 🎮
🧩 지도 라이브러리 선택과 설치
리액트 네이티브에서 지도를 구현하는 데 사용할 수 있는 라이브러리는 여러 가지가 있어. 2025년 현재 가장 인기 있는 옵션들을 살펴보자!
위 차트에서 볼 수 있듯이, react-native-maps가 가장 인기 있는 선택이야. 이 라이브러리는 Airbnb에서 시작되었고, 현재는 리액트 네이티브 커뮤니티에서 관리하고 있어. 안정적이고 문서화가 잘 되어 있어서 초보자에게도 추천해!
react-native-maps 설치하기
터미널에서 다음 명령어를 실행해 라이브러리를 설치하자:
npm install react-native-maps --save
iOS에서는 추가 설정이 필요해:
cd ios && pod install
API 키 설정
대부분의 지도 서비스는 API 키가 필요해. Google Maps를 사용한다면 다음과 같이 설정해야 해:
iOS (AppDelegate.mm):
#import <googlemaps>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[GMSServices provideAPIKey:@"YOUR_API_KEY"];
// ...
}</googlemaps>
Android (AndroidManifest.xml):
<application>
<!-- ... -->
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_API_KEY"></meta-data>
</application>
⚠️ API 키는 절대 GitHub 같은 공개 저장소에 올리면 안 돼! 환경 변수나 별도의 설정 파일을 사용해서 관리하는 것이 좋아. 2025년에는 API 키 노출로 인한 보안 사고가 더 심각해졌으니 조심하자!
이제 기본적인 라이브러리 설치는 완료됐어. 다음으로 실제 지도를 화면에 표시해보자! 🗺️
🗺️ 기본 지도 구현하기
자, 이제 앱에 지도를 표시해보자! 가장 기본적인 지도 컴포넌트부터 시작할게.
첫 번째 지도 컴포넌트
App.tsx 파일을 열고 다음과 같이 코드를 작성해봐:
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
const App = () => {
return (
<view style="{styles.container}">
<text style="{styles.title}">내 첫 지도 앱</text>
<mapview provider="{PROVIDER_GOOGLE}" style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta:></mapview>
</view>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 20,
fontWeight: 'bold',
padding: 15,
backgroundColor: '#fff',
},
map: {
flex: 1,
},
});
export default App;
이 코드는 서울 시청 부근을 중심으로 하는 기본 지도를 표시해. initialRegion 속성에서 위도(latitude), 경도(longitude), 그리고 지도의 확대/축소 수준을 설정할 수 있어.
💡 latitudeDelta와 longitudeDelta는 지도에 표시될 영역의 크기를 결정해. 값이 작을수록 더 확대된 지도가 표시돼!
지도 유형 변경하기
지도에는 여러 유형이 있어. 표준, 위성, 하이브리드 등 다양한 스타일을 적용할 수 있지:
<mapview provider="{PROVIDER_GOOGLE}" style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta: maptype="satellite"></mapview>
지도 컨트롤 추가하기
사용자가 지도를 확대/축소하거나 이동할 수 있도록 컨트롤을 추가할 수 있어:
<mapview zoomenabled="{true}" scrollenabled="{true}" rotateenabled="{true}" pitchenabled="{true}" showscompass="{true}" showsscale="{true}" showstraffic="{true}" showsbuildings="{true}" showsuserlocation="{true}"></mapview>
이런 속성들을 조합해서 사용자에게 더 나은 지도 경험을 제공할 수 있어. 특히 showsUserLocation 속성은 사용자의 현재 위치를 지도에 표시해주는 아주 유용한 기능이야!
커스텀 스타일 적용하기
Google Maps에서는 지도의 스타일을 커스터마이징할 수 있어. 2025년에는 더 다양한 스타일 옵션이 추가되었지:
const mapStyle = [
{
"elementType": "geometry",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
// 더 많은 스타일 규칙...
];
// 그리고 MapView에 적용:
<mapview custommapstyle="{mapStyle}"></mapview>
이렇게 하면 지도의 색상, 라벨, 도로 등의 스타일을 완전히 바꿀 수 있어. 다크 모드 지도나 앱의 브랜드 색상에 맞는 지도를 만들 수 있지!
이제 기본적인 지도는 구현했어. 다음으로는 사용자의 현재 위치를 가져오는 방법을 알아보자! 📍
📍 사용자 위치 가져오기
위치 기반 서비스의 핵심은 바로 사용자의 현재 위치를 정확하게 파악하는 거야. 리액트 네이티브에서는 Geolocation API를 사용해 사용자의 위치 정보를 가져올 수 있어.
위치 권한 요청하기
사용자 위치를 가져오기 전에 먼저 권한을 요청해야 해. 2025년에는 개인정보 보호가 더욱 강화되어 명시적인 권한 요청이 필수야!
먼저 필요한 패키지를 설치하자:
npm install react-native-permissions
그리고 다음과 같이 권한을 요청할 수 있어:
import React, { useEffect, useState } from 'react';
import { StyleSheet, View, Text, Alert } from 'react-native';
import MapView from 'react-native-maps';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import Geolocation from 'react-native-geolocation-service';
const App = () => {
const [location, setLocation] = useState(null);
useEffect(() => {
requestLocationPermission();
}, []);
const requestLocationPermission = async () => {
const platform = Platform.OS === 'ios'
? PERMISSIONS.IOS.LOCATION_WHEN_IN_USE
: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
const result = await check(platform);
switch (result) {
case RESULTS.UNAVAILABLE:
Alert.alert('위치 서비스를 사용할 수 없습니다.');
break;
case RESULTS.DENIED:
const requestResult = await request(platform);
if (requestResult === RESULTS.GRANTED) {
getCurrentLocation();
}
break;
case RESULTS.GRANTED:
getCurrentLocation();
break;
case RESULTS.BLOCKED:
Alert.alert(
'위치 권한이 차단되었습니다.',
'설정에서 위치 권한을 허용해주세요.'
);
break;
}
};
const getCurrentLocation = () => {
Geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
},
(error) => {
console.log(error.code, error.message);
Alert.alert('위치를 가져올 수 없습니다.', error.message);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
);
};
return (
<view style="{styles.container}">
<text style="{styles.title}">내 위치 찾기</text>
{location ? (
<mapview style="{styles.map}" initialregion="{{" latitude: location.latitude longitude: location.longitude latitudedelta: longitudedelta: showsuserlocation="{true}"></mapview>
) : (
<view style="{styles.loading}">
<text>위치를 가져오는 중...</text>
</view>
)}
</view>
);
};
💡 enableHighAccuracy 옵션을 true로 설정하면 더 정확한 위치를 얻을 수 있지만, 배터리 소모가 증가할 수 있어. 앱의 성격에 따라 적절히 설정하는 것이 좋아!
실시간 위치 추적하기
사용자가 이동할 때마다 위치를 업데이트하고 싶다면 watchPosition 메서드를 사용할 수 있어:
useEffect(() => {
let watchId;
const startLocationTracking = () => {
watchId = Geolocation.watchPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
},
(error) => {
console.log(error.code, error.message);
},
{
enableHighAccuracy: true,
distanceFilter: 10, // 10미터마다 업데이트
interval: 5000, // 5초마다 업데이트 (Android only)
fastestInterval: 2000 // 가장 빠른 업데이트 간격 (Android only)
}
);
};
requestLocationPermission().then(() => {
startLocationTracking();
});
// 컴포넌트가 언마운트될 때 위치 추적 중지
return () => {
if (watchId) {
Geolocation.clearWatch(watchId);
}
};
}, []);
distanceFilter 옵션을 사용하면 사용자가 일정 거리 이상 이동했을 때만 위치가 업데이트돼. 이는 배터리 소모를 줄이는 데 도움이 돼!
위치 정확도 향상시키기
2025년에는 위치 정확도를 높이기 위한 여러 기술이 발전했어. 특히 GPS, 와이파이, 셀룰러 네트워크, 블루투스 비콘 등을 조합해 더 정확한 위치를 얻을 수 있지:
// 고급 위치 설정 (Android)
if (Platform.OS === 'android') {
Geolocation.setRNConfiguration({
enableHighAccuracy: true,
skipPermissionRequests: false,
authorizationLevel: 'whenInUse',
});
}
⚠️ 실시간 위치 추적은 배터리를 많이 소모할 수 있어. 꼭 필요한 경우에만 사용하고, 사용하지 않을 때는 반드시 clearWatch로 추적을 중지해야 해!
이제 사용자의 위치를 가져오는 방법을 알았으니, 다음으로는 지도에 마커와 커스텀 오버레이를 추가하는 방법을 알아보자! 📌
📌 마커와 커스텀 오버레이 추가하기
지도에 마커를 추가하면 특정 위치를 표시하고 정보를 제공할 수 있어. 리액트 네이티브 맵스에서는 Marker 컴포넌트를 사용해 이를 구현할 수 있지.
기본 마커 추가하기
가장 간단한 마커부터 시작해보자:
import React from 'react';
import { StyleSheet, View } from 'react-native';
import MapView, { Marker } from 'react-native-maps';
const App = () => {
return (
<view style="{styles.container}">
<mapview style="{styles.map}" initialregion="{{" latitude: longitude: latitudedelta: longitudedelta:>
<marker coordinate="{{" latitude: longitude: title="서울 시청" description="서울특별시 중구 세종대로 110"></marker>
</mapview>
</view>
);
};
이렇게 하면 서울 시청 위치에 기본 마커가 표시돼. title과 description 속성을 사용하면 마커를 탭했을 때 정보 창이 표시돼!
여러 마커 표시하기
보통은 하나가 아닌 여러 개의 마커를 표시해야 할 때가 많아. 데이터 배열을 사용해 여러 마커를 효율적으로 표시할 수 있어:
const locations = [
{
id: 1,
title: '서울 시청',
description: '서울특별시 중구 세종대로 110',
coordinate: { latitude: 37.5665, longitude: 126.9780 },
},
{
id: 2,
title: '경복궁',
description: '서울특별시 종로구 사직로 161',
coordinate: { latitude: 37.5796, longitude: 126.9770 },
},
{
id: 3,
title: '남산타워',
description: '서울특별시 용산구 남산공원길 105',
coordinate: { latitude: 37.5512, longitude: 126.9882 },
},
];
// MapView 내부에서:
{locations.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}"></marker>
))}
커스텀 마커 디자인
기본 마커 대신 커스텀 이미지나 컴포넌트를 사용할 수 있어:
<marker coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}">
<view style="{styles.customMarker}">
<text style="{styles.markerText}">🏛️</text>
</view>
</marker>
// 스타일:
const styles = StyleSheet.create({
customMarker: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 20,
borderWidth: 2,
borderColor: 'white',
},
markerText: {
fontSize: 20,
},
});
2025년에는 3D 마커와 애니메이션 효과가 트렌드야. 다음과 같이 애니메이션을 추가할 수 있어:
import React, { useRef, useEffect } from 'react';
import { Animated } from 'react-native';
const AnimatedMarker = ({ coordinate }) => {
const scaleAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.spring(scaleAnim, {
toValue: 1,
friction: 5,
useNativeDriver: true,
}).start();
}, []);
return (
<marker coordinate="{coordinate}">
<animated.view style="{[" styles.custommarker transform: scale: scaleanim>
<text style="{styles.markerText}">🏛️</text>
</animated.view>
</marker>
);
};
커스텀 콜아웃(Callout) 만들기
마커를 탭했을 때 나타나는 정보 창(콜아웃)도 커스터마이징할 수 있어:
<marker coordinate="{marker.coordinate}">
<view style="{styles.customMarker}">
<text style="{styles.markerText}">🏛️</text>
</view>
<callout tooltip>
<view style="{styles.calloutContainer}">
<text style="{styles.calloutTitle}">{marker.title}</text>
<text style="{styles.calloutDescription}">{marker.description}</text>
<image source="{{" uri: marker.imageurl style="{styles.calloutImage}"></image>
<touchableopacity style="{styles.calloutButton}" onpress="{()"> navigateToDetail(marker.id)}
>
<text style="{styles.calloutButtonText}">자세히 보기</text>
</touchableopacity>
</view>
</callout>
</marker>
클러스터링으로 많은 마커 관리하기
마커가 많을 경우 성능 문제가 발생할 수 있어. 이럴 때는 클러스터링 기술을 사용하면 좋아:
import MapView from 'react-native-maps';
import ClusteredMapView from 'react-native-maps-clustering';
// MapView 대신 ClusteredMapView 사용
<clusteredmapview style="{styles.map}" initialregion="{initialRegion}" clustercolor="#3498db" clustertextcolor="#ffffff" clusterbordercolor="#ffffff" clusterborderwidth="{4}" radius="{50}">
{locations.map(marker => (
<marker key="{marker.id}" coordinate="{marker.coordinate}" title="{marker.title}" description="{marker.description}"></marker>
))}
</clusteredmapview>
클러스터링을 사용하면 가까운 위치에 있는 여러 마커를 하나로 묶어서 표시할 수 있어. 사용자가 지도를 확대하면 클러스터가 개별 마커로 분리돼!
이제 마커와 오버레이를 사용하는 방법을 알았으니, 다음으로는 경로 찾기와 거리 계산 기능을 구현해보자! 🛣️
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개