이번 포스트는 현재까지 프로젝트를 진행하며 가장 많은 시간을 들이게 한 react-native-maps
를 활용하여 만든 페이지에 관한 내용이다.
현재까지 작업한 메인 페이지이며 react-native-maps와 react-native-carousel을 이용하여 구현하였다.
먼저 Main.js
는 지도 외 다른 컴포넌트들을 종합적으로 포함한 page이다.
import React from 'react';
import MainMap from '../components/main/MainMap';
const Main = () => <MainMap />;
export default Main;
MainMap.js
는 import
해서 실제 구현을 담당하는 컴포넌트이다. 해당 파일은 튜토리얼 강좌를 참고하여 작성했다. 처음부터 끝까지 시각적으로 잘 설명해주는 영상이다. 이 강좌를 바탕으로 작성한 후, MobX
를 이용하여 상태관리를 하기 위해 리팩토링을 진행한 파일이다.
/* eslint-disable react/state-in-constructor */
import React from 'react';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
import {
StyleSheet,
View,
Dimensions,
} from 'react-native';
import Carousel from 'react-native-snap-carousel';
import { inject, observer } from 'mobx-react';
import BriefCatInfo from './BriefCatInfo';
import MainMarker from './MainMarker';
//화면의 width와 height
const { width, height } = Dimensions.get('window');
// css styles
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
},
map: {
width,
height,
...StyleSheet.absoluteFillObject,
},
carousel: {
position: 'absolute',
bottom: 0,
marginBottom: 10,
},
card: {
backgroundColor: '#ececec',
height: 200,
width: 300,
padding: 24,
borderRadius: 24,
},
cardImg: {
height: 120,
width: 200,
},
cardtitle: {
fontSize: 12,
marginTop: 5,
fontWeight: 'bold',
},
cardDescription: {
fontSize: 12,
color: '#444',
},
});
class MainMap extends React.Component {
state = {
isShowingCarousel: false, // Carousel을 보여주기 위한 state
};
componentDidMount() {
//UserStore에서 inject해서 사용
this.props.getLocationPermission();
}
// Carousel에서 'X'버튼을 누르면 다시 state를 false로 변경해 Carousel을 렌더하지 않는다.
hideCarousel = () => {
this.setState({ isShowingCarousel: false });
}
// Carousel을 state에 따라 BriefCaseInfo에서 렌더
renderCarouselItem = ({ item }) => {
const { isShowingCarousel } = this.state;
return (
<BriefCatInfo
item={item}
isShowingCarousel={isShowingCarousel}
hideCarousel={this.hideCarousel}
/>
);
};
// Carousel을 스와이프하면 해당 마커로 region을 이동
onCarouselItemChange = (index) => {
const { onRegionChangeComplete, markers } = this.props;
const location = markers[index];
const region = {
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.005,
longitudeDelta: 0.005,
};
onRegionChangeComplete(region);
this._map.animateToRegion(region);
}
// 마커를 클릭했을 때, 해당 마커로 region을 이동하고 isShowingCarousel state 변경
onMarkerPressed = (location, index) => {
const { isShowingCarousel } = this.state;
const { onRegionChangeComplete } = this.props;
const region = {
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.005,
longitudeDelta: 0.005,
};
onRegionChangeComplete(region); // region 변경
this._map.animateToRegion(region); // animation
this._carousel.snapToItem(index); // carousel item 변경
// 마커를 클릭하면 carousel을 보여주기 위해 state 변경
if (!isShowingCarousel) {
this.setState({ isShowingCarousel: true });
}
}
render() {
console.disableYellowBox = 'true';
const { markers, currentRegion, onRegionChangeComplete } = this.props;
return (
<View style={styles.container}>
<MapView
provider={PROVIDER_GOOGLE}
ref={(map) => this._map = map}
style={styles.map}
showsUserLocation={true}
region={{ ...currentRegion }}
onRegionChangeComplete={onRegionChangeComplete}
>
// 각 마커 element를 MapView안에서 map으로 전달.
{
markers.map((marker, index) => (
<MainMarker
key={marker.name}
marker={marker}
index={index}
onMarkerPressed={this.onMarkerPressed}
currentRegion={currentRegion}
/>
))
}
</MapView>
<Carousel
ref={(c) => { this._carousel = c; }}
data={markers}
renderItem={this.renderCarouselItem}
onSnapToItem={(index) => this.onCarouselItemChange(index)}
removeClippedSubviews={false}
sliderWidth={width}
itemWidth={300}
containerCustomStyle={styles.carousel}
/>
</View>
);
}
}
// MobX observable 과 action을 해당 class에 inject(주입)하여 사용한다.
export default inject(({ cat, user }) => ({
markers: cat.markers,
currentPosition: user.currentPosition,
currentRegion: user.currentRegion,
currentBoundingBox: user.currentBoundingBox,
getLocationPermission: user.getLocationPermission,
onRegionChangeComplete: user.onRegionChangeComplete,
}))(
observer(MainMap),
);
UserStore.js
에는 위치 관련 state들과 함수들을 저장하고 있다.
import { observable, action, computed, decorate, runInAction } from 'mobx';
import { Alert, AsyncStorage } from 'react-native';
import axios from 'axios';
import { SERVER_URL } from 'react-native-dotenv';
import * as Location from 'expo-location';
import * as Permissions from 'expo-permissions';
class UserStore {
constructor(root) {
this.root = root;
}
// 현재 위치 좌표
currentPosition = {
latitude: 0,
longitude: 0,
};
// 현재 region
currentRegion = {
latitude: 0,
latitudeDelta: 0,
longitude: 0,
longitudeDelta: 0,
}
// 현재 화면의 북동/남서쪽-위/경도
currentBoundingBox = {
NElatitude: 0,
NElongitude: 0,
SWlatitude: 0,
SWlongitude: 0,
};
// 비동기적으로 위치 권한을 요구하는 함수.
getLocationPermission = async () => {
const { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
console.log('Not granted');
Alert.alert('위치 정보 사용을 허용해주세요!');
} else {
this.getWatchPosition();
}
};
// 현재 위치를 받아 변수와 함수에 전달한다.
getWatchPosition = () => {
navigator.geolocation.watchPosition(
(position) => {
console.log('In getWatchPosition');
const { latitude, longitude } = position.coords;
console.log('Before currentRegion');
this.currentPosition = {
latitude,
longitude,
};
this.onRegionChangeComplete({
latitude,
longitude,
latitudeDelta: 0.005,
longitudeDelta: 0.005,
});
},
(error) => { Alert.alert(error.code, error.message); },
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 },
);
}
// 현재 화면의 북동쪽, 남서쪽 좌표를 계산한다. 화면 내에 렌더할 마커의 위치를 계산하기 위함이다.
getBoundingBox = (region) => {
this.currentBoundingBox = {
NElatitude: region.latitude + region.latitudeDelta / 2, // northLat - max lat
NElongitude: region.longitude + region.longitudeDelta / 2, // eastLng - max lng
SWlatitude: region.latitude - region.latitudeDelta / 2, // southLat - min lat
SWlongitude: region.longitude - region.longitudeDelta / 2, // westLng - min lng
};
};
// 지도 화면을 움직일 때, 움직임이 멈추면 그 위치의 값을 받아 변수와 함수에 전달한다.
onRegionChangeComplete = (region) => {
this.currentRegion = region;
this.getBoundingBox(region);
}
}
// decorate로 observable 인지 action인지 define한다.
decorate(UserStore, {
currentPosition: observable,
currentRegion: observable,
currentBoundingBox: observable,
getLocationPermission: action,
getWatchPosition: action,
getBoundingBox: action,
onRegionChangeComplete: action,
});
export default UserStore;
간단하게 직역에 가깝게 보자면 "동적 observalbe 객체들은 동결될 수 없다." 정도가 되겠다. 내용에서 유추해 보자면 현재 동적 observalbe 객체들이 변화 혹은 변경과 같은 행위가 이뤄질 수 없는 의미와 비슷하다고 볼 수 있다.
그래서 해당 문장을 검색해 보았더니 MobX github 페이지에서 아래와 같은 내용을 찾을 수 있었다.
대략 번역해보자면 이렇다.
<span style={someObservableObject} />
에서 React 가 style 객체들을 동결시키는 것처럼 말입니다. 이 문제의 대처법은 <span style={{...someObservableObject}} />
처럼 단순히 non-observable 한 객체를 전달해주면 됩니다.여기서 이 "...
" Spread operator 를 사용해줌으로서 기존의 객체의 값을 분리해 와서 새로운 객체에 담아 non-observable 하게 사용할 수 있게 된다.
위의 MainMap.js
에서도 MapView
를 렌더할 때 같은 문제가 발생했다.
<MapView
provider={PROVIDER_GOOGLE}
ref={(map) => this._map = map}
style={styles.map}
showsUserLocation={true}
region={{ ...currentRegion }}
onRegionChangeComplete={onRegionChangeComplete}
>
// 각 마커 element를 MapView안에서 map으로 전달.
{
markers.map((marker, index) => (
<MainMarker
key={marker.name}
marker={marker}
index={index}
onMarkerPressed={this.onMarkerPressed}
currentRegion={currentRegion}
/>
))
}
</MapView>
이 중 region
의 값인 currentRegion
을 UserStore
에서 받아왔는데, observable 이기 때문에 Spread operator 를 이용하여 non-observable 하게 변경시켜 주었다. 자주 발생하기 쉬운 에러 같아 이렇게 정리해본다.
지도 API가 이렇게 까다롭고 발목을 잡게 될지 몰랐다. 이 코드 이전에 MobX로 상태 관리를 옮기려고 몇 state 들과 함수들을 리팩토링하다가 코드가 누더기가 되서 새로 브랜치를 따 현재처럼 만들 수 있었다. 필요하다면 과감하게 새로 시작할 수도 있어야한다는 것을 배웠다.