[프로젝트-DediCats] Devlog-3

김대연·2020년 2월 7일
0

Project DediCats

목록 보기
4/16

이번 포스트는 현재까지 프로젝트를 진행하며 가장 많은 시간을 들이게 한 react-native-maps를 활용하여 만든 페이지에 관한 내용이다.


현재까지 작업한 메인 페이지이며 react-native-mapsreact-native-carousel을 이용하여 구현하였다.


먼저 Main.js는 지도 외 다른 컴포넌트들을 종합적으로 포함한 page이다.

import React from 'react';
import MainMap from '../components/main/MainMap';

const Main = () => <MainMap />;

export default Main;

MainMap.jsimport해서 실제 구현을 담당하는 컴포넌트이다. 해당 파일은 튜토리얼 강좌를 참고하여 작성했다. 처음부터 끝까지 시각적으로 잘 설명해주는 영상이다. 이 강좌를 바탕으로 작성한 후, 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;  

  • 오늘 발생한 에러 리뷰 - "Dynamic observable objects cannot be frozen"

간단하게 직역에 가깝게 보자면 "동적 observalbe 객체들은 동결될 수 없다." 정도가 되겠다. 내용에서 유추해 보자면 현재 동적 observalbe 객체들이 변화 혹은 변경과 같은 행위가 이뤄질 수 없는 의미와 비슷하다고 볼 수 있다.

그래서 해당 문장을 검색해 보았더니 MobX github 페이지에서 아래와 같은 내용을 찾을 수 있었다.

대략 번역해보자면 이렇다.

  • Observable 객체들은 더이상 동결(이라고 표현하는게 맞는지 모르겠지만) 될 수 없습니다 (그렇지 않다면 un-observable한 상태가 됩니다). observable 객체를 React component 의 style 속성으로 전달하는 것과 같은 행위가 이러한 예기치 못한 상황을 일으킬 수도 있습니다. 마치 <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의 값인 currentRegionUserStore에서 받아왔는데, observable 이기 때문에 Spread operator 를 이용하여 non-observable 하게 변경시켜 주었다. 자주 발생하기 쉬운 에러 같아 이렇게 정리해본다.


지도 API가 이렇게 까다롭고 발목을 잡게 될지 몰랐다. 이 코드 이전에 MobX로 상태 관리를 옮기려고 몇 state 들과 함수들을 리팩토링하다가 코드가 누더기가 되서 새로 브랜치를 따 현재처럼 만들 수 있었다. 필요하다면 과감하게 새로 시작할 수도 있어야한다는 것을 배웠다.

0개의 댓글