Mobile 개발/RN Project - Map Chat

RN (React Native) - 네이버지도 연동

히핑소 2020. 9. 4. 23:31
반응형

리액트 네이티브 네이버지도 연동 방법 입니다.

개발환경 : Android, react-native cli (expo 아님)

구글링을 열심히 해본 결과 

크게 2군데 react-naver map 브릿지를 제공하고 있었습니다.

- zeakd.github.io/react-naver-maps/#/React%20Naver%20Maps?id=introduction

장점, Guide Doc 제공. 
단점, 지금 app run 안됨... err 발생.

 

React Naver Maps Style Guide

 

zeakd.github.io

- github.com/QuadFlask/react-native-naver-map

장점, 아주 잘 동작함
단점, doc 이 없음

 

QuadFlask/react-native-naver-map

🗺️naver map for react-native. Contribute to QuadFlask/react-native-naver-map development by creating an account on GitHub.

github.com

잘 동작하는 녀석으로 골라 사용해보겠습니다.

우선, naver map service를 등록하고

api key를 발급받아야합니다.

이게 조금 귀찮은게 naver id가 있어도 따로 회원가입을 해야 하네요.

이용 신청하기 - Application 등록 - map 모두 추가

그리고, 본인 app의 package name 을 꼭 넣어야 합니다.

넣지않으면 map이 안보이는 불상사가 발생합니다.

이걸로 몇시간 삽질했네요.

아직 모른다면, project init 후 설정하세요.

 

그리고, 인증 정보의 Client ID 를 따로 기록해 둡니다.

 

이제 사전 준비가 끝났으니 app project 를 생성합니다.

react-native init naverMapsInsider
cd naverMapsInsider
npm install react-native-nmap --save

project android folder 안에서 다음과 같이 진행합니다.

AndroidManifest.xml
<manifest>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>    
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <application>
        <meta-data android:name="com.naver.maps.map.CLIENT_ID"
        android:value="YOUR_CLIENT_ID_HERE" />
    </application>
</manifest>

build.gradle
allprojects {
    repositories {
        google()
         jcenter()
         maven { url 'https://navercorp.bintray.com/maps' }
    }
}

혹시, android sdk 를 못찾는다는 compile error 발생한다면 아래 추가
android root folder 에
local.properties 파일 생성 후 아래 입력.
sdk.dir=C\:\\Users\\xxxxx\\AppData\\Local\\Android\\Sdk

그리고 app 에서 사용할 component 들을 install 합니다.

stack , bottom tab 등의 react-navigation component 들을 사용해볼 예정입니다.

npm install --save react-navigation
npm install @react-navigation/stack
npm install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
npm install @react-navigation/bottom-tabs
npm install @react-navigation/native

이제 모든 준비가 끝났고, app을 실행 합니다.

react-native run-android

이제 map을 띄우기 위해 code 수정

App.js

import 'react-native-gesture-handler';
import React, {useEffect} from 'react';
import NaverMapView, {Circle, Marker, Path, Polyline, Polygon, Align} from "./map";
import {PermissionsAndroid, Platform, Text, TouchableOpacity, View} from "react-native";
import {NavigationContainer} from '@react-navigation/native';
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
import {createStackNavigator} from "@react-navigation/stack";

const P0 = {latitude: 37.564362, longitude: 126.977011};
const P1 = {latitude: 37.565051, longitude: 126.978567};
const P2 = {latitude: 37.565383, longitude: 126.976292};
const P4 = {latitude: 37.564834, longitude: 126.977218};

const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();

const App = () => {
    return <NavigationContainer>
        <Stack.Navigator>
            <Stack.Screen name="home" component={HomeScreen}/>
            <Stack.Screen name="stack" component={MapViewScreen}/>
        </Stack.Navigator>
    </NavigationContainer>
}

const HomeScreen = () =>
    <Tab.Navigator>
        <Tab.Screen name={"map"} component={MapViewScreen}/>
        <Tab.Screen name={"text"} component={TextScreen}/>
    </Tab.Navigator>

const TextScreen = () => {
    return <Text>text</Text>
}

const MapViewScreen = ({navigation}) => {
    useEffect(() => {
        requestLocationPermission();
    }, []);

    return <>
        <NaverMapView style={{width: '100%', height: '100%'}}
                      showsMyLocationButton={true}
                      center={{...P0, zoom: 16}}
                      onTouch={e => console.warn('onTouch', JSON.stringify(e.nativeEvent))}
                      onCameraChange={e => console.warn('onCameraChange', JSON.stringify(e))}
                      onMapClick={e => console.warn('onMapClick', JSON.stringify(e))}
                      useTextureView>
            <Marker coordinate={P0} onClick={() => console.warn('onClick! p0')} caption={{text: "test caption", align: Align.Left}}/>
            <Marker coordinate={P1} pinColor="blue" onClick={() => console.warn('onClick! p1')}/>
            <Marker coordinate={P2} pinColor="red" onClick={() => console.warn('onClick! p2')}/>
            <Marker coordinate={P4} onClick={() => console.warn('onClick! p4')} image={require("./marker.png")} width={48} height={48}/>
            <Path coordinates={[P0, P1]} onClick={() => console.warn('onClick! path')} width={10}/>
            <Polyline coordinates={[P1, P2]} onClick={() => console.warn('onClick! polyline')}/>
            <Circle coordinate={P0} color={"rgba(255,0,0,0.3)"} radius={200} onClick={() => console.warn('onClick! circle')}/>
            <Polygon coordinates={[P0, P1, P2]} color={`rgba(0, 0, 0, 0.5)`} onClick={() => console.warn('onClick! polygon')}/>
        </NaverMapView>
        <TouchableOpacity style={{position: 'absolute', bottom: '10%', right: 8}} onPress={() => navigation.navigate('stack')}>
            <View style={{backgroundColor: 'gray', padding: 4}}>
                <Text style={{color: 'white'}}>open stack</Text>
            </View>
        </TouchableOpacity>
        <Text style={{position: 'absolute', top: '95%', width: '100%', textAlign: 'center'}}>Icon made by Pixel perfect from www.flaticon.com</Text>
    </>
};

async function requestLocationPermission() {
    if (Platform.OS !== 'android') return;
    try {
        const granted = await PermissionsAndroid.request(
            PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
            {
                title: 'Location Permission',
                message: 'show my location need Location permission',
                buttonNeutral: 'Ask Me Later',
                buttonNegative: 'Cancel',
                buttonPositive: 'OK',
            },
        );
        if (granted === PermissionsAndroid.RESULTS.GRANTED) {
            console.log('You can use the location');
        } else {
            console.log('Location permission denied');
        }
    } catch (err) {
        console.warn(err);
    }
}


export default App;

~/map/index.js

import React, { Component } from 'react';
import { requireNativeComponent, findNodeHandle, UIManager, Platform, NativeModules, Image, } from 'react-native';
const RNNaverMapView = requireNativeComponent('RNNaverMapView');
const RNNaverMapViewTexture = Platform.select({
    android: () => requireNativeComponent('RNNaverMapViewTexture'),
    ios: () => RNNaverMapView
})();
const RNNaverMapMarker = requireNativeComponent('RNNaverMapMarker');
const RNNaverMapPathOverlay = requireNativeComponent('RNNaverMapPathOverlay');
const RNNaverMapPolylineOverlay = requireNativeComponent('RNNaverMapPolylineOverlay');
const RNNaverMapCircleOverlay = requireNativeComponent('RNNaverMapCircleOverlay');
const RNNaverMapPolygonOverlay = requireNativeComponent('RNNaverMapPolygonOverlay');
export var TrackingMode;
(function (TrackingMode) {
    TrackingMode[TrackingMode["None"] = 0] = "None";
    TrackingMode[TrackingMode["NoFollow"] = 1] = "NoFollow";
    TrackingMode[TrackingMode["Follow"] = 2] = "Follow";
    TrackingMode[TrackingMode["Face"] = 3] = "Face";
})(TrackingMode || (TrackingMode = {}));
export var MapType;
(function (MapType) {
    MapType[MapType["Basic"] = 0] = "Basic";
    MapType[MapType["Navi"] = 1] = "Navi";
    MapType[MapType["Satellite"] = 2] = "Satellite";
    MapType[MapType["Hybrid"] = 3] = "Hybrid";
    MapType[MapType["Terrain"] = 4] = "Terrain";
})(MapType || (MapType = {}));
export var LayerGroup;
(function (LayerGroup) {
    LayerGroup["LAYER_GROUP_BUILDING"] = "building";
    LayerGroup["LAYER_GROUP_TRANSIT"] = "transit";
    LayerGroup["LAYER_GROUP_BICYCLE"] = "bike";
    LayerGroup["LAYER_GROUP_TRAFFIC"] = "ctt";
    LayerGroup["LAYER_GROUP_CADASTRAL"] = "landparcel";
    LayerGroup["LAYER_GROUP_MOUNTAIN"] = "mountain";
})(LayerGroup || (LayerGroup = {}));
export var Gravity;
(function (Gravity) {
    Gravity[Gravity["NO_GRAVITY"] = 0] = "NO_GRAVITY";
    Gravity[Gravity["AXIS_SPECIFIED"] = 1] = "AXIS_SPECIFIED";
    Gravity[Gravity["AXIS_PULL_BEFORE"] = 2] = "AXIS_PULL_BEFORE";
    Gravity[Gravity["AXIS_PULL_AFTER"] = 4] = "AXIS_PULL_AFTER";
    Gravity[Gravity["AXIS_X_SHIFT"] = 0] = "AXIS_X_SHIFT";
    Gravity[Gravity["AXIS_Y_SHIFT"] = 4] = "AXIS_Y_SHIFT";
    Gravity[Gravity["TOP"] = 48] = "TOP";
    Gravity[Gravity["BOTTOM"] = 80] = "BOTTOM";
    Gravity[Gravity["LEFT"] = 3] = "LEFT";
    Gravity[Gravity["RIGHT"] = 5] = "RIGHT";
    Gravity[Gravity["CENTER_VERTICAL"] = 16] = "CENTER_VERTICAL";
    Gravity[Gravity["CENTER_HORIZONTAL"] = 1] = "CENTER_HORIZONTAL";
})(Gravity || (Gravity = {}));
export var Align;
(function (Align) {
    Align[Align["Center"] = 0] = "Center";
    Align[Align["Left"] = 1] = "Left";
    Align[Align["Right"] = 2] = "Right";
    Align[Align["Top"] = 3] = "Top";
    Align[Align["Bottom"] = 4] = "Bottom";
    Align[Align["TopLeft"] = 5] = "TopLeft";
    Align[Align["TopRight"] = 6] = "TopRight";
    Align[Align["BottomRight"] = 7] = "BottomRight";
    Align[Align["BottomLeft"] = 8] = "BottomLeft";
})(Align || (Align = {}));
export default class NaverMapView extends Component {
    constructor() {
        super(...arguments);
        this.resolveRef = (ref) => {
            this.ref = ref;
            this.nodeHandle = findNodeHandle(ref);
        };
        this.animateToCoordinate = (coord) => {
            this.dispatchViewManagerCommand('animateToCoordinate', [coord]);
        };
        this.animateToTwoCoordinates = (c1, c2) => {
            this.dispatchViewManagerCommand('animateToTwoCoordinates', [c1, c2]);
        };
        this.animateToCoordinates = (coords, bounds) => {
            this.dispatchViewManagerCommand("animateToCoordinates", [coords, bounds]);
        };
        this.animateToRegion = (region) => {
            this.dispatchViewManagerCommand('animateToRegion', [region]);
        };
        this.setLocationTrackingMode = (mode) => {
            this.dispatchViewManagerCommand('setLocationTrackingMode', [mode]);
        };
        this.showsMyLocationButton = (show) => {
            this.dispatchViewManagerCommand('showsMyLocationButton', [show]);
        };
        this.dispatchViewManagerCommand = (command, arg) => {
            return Platform.select({
                // @ts-ignore
                android: () => UIManager.dispatchViewManagerCommand(this.nodeHandle, 
                // @ts-ignore
                UIManager.getViewManagerConfig('RNNaverMapView').Commands[command], arg),
                ios: () => NativeModules[`RNNaverMapView`][command](this.nodeHandle, ...arg),
            })();
        };
        this.handleOnCameraChange = (event) => this.props.onCameraChange && this.props.onCameraChange(event.nativeEvent);
        this.handleOnMapClick = (event) => this.props.onMapClick && this.props.onMapClick(event.nativeEvent);
    }
    render() {
        const { onInitialized, center, tilt, bearing, mapPadding, logoMargin, nightMode, useTextureView, } = this.props;
        const ViewClass = useTextureView ? RNNaverMapViewTexture : RNNaverMapView;
        return React.createElement(ViewClass, Object.assign({ ref: this.resolveRef }, this.props, { onInitialized: onInitialized, center: center, mapPadding: mapPadding, logoMargin: logoMargin, tilt: tilt, bearing: bearing, nightMode: nightMode, onCameraChange: this.handleOnCameraChange, onMapClick: this.handleOnMapClick }));
    }
}
export class Marker extends Component {
    render() {
        return React.createElement(RNNaverMapMarker, Object.assign({}, this.props, { image: getImageUri(this.props.image) }));
    }
}
export class Circle extends Component {
    render() {
        return React.createElement(RNNaverMapCircleOverlay, Object.assign({}, this.props));
    }
}
export class Polyline extends Component {
    render() {
        return React.createElement(RNNaverMapPolylineOverlay, Object.assign({}, this.props));
    }
}
export class Polygon extends Component {
    render() {
        return React.createElement(RNNaverMapPolygonOverlay, Object.assign({}, this.props));
    }
}
export class Path extends Component {
    render() {
        return React.createElement(RNNaverMapPathOverlay, Object.assign({}, this.props, { pattern: getImageUri(this.props.pattern) }));
    }
}
function getImageUri(src) {
    let imageUri = null;
    if (src) {
        let image = Image.resolveAssetSource(src) || { uri: null };
        imageUri = image.uri;
    }
    return imageUri;
}

~/map/index.d.ts

import React, { Component } from 'react';
import { StyleProp, ViewStyle, ImageSourcePropType } from 'react-native';
declare const RNNaverMapView: any;
export interface Coord {
    latitude: number;
    longitude: number;
}
export interface Region extends Coord {
    latitudeDelta: number;
    longitudeDelta: number;
}
export declare enum TrackingMode {
    None = 0,
    NoFollow = 1,
    Follow = 2,
    Face = 3
}
export declare enum MapType {
    Basic = 0,
    Navi = 1,
    Satellite = 2,
    Hybrid = 3,
    Terrain = 4
}
export declare enum LayerGroup {
    LAYER_GROUP_BUILDING = "building",
    LAYER_GROUP_TRANSIT = "transit",
    LAYER_GROUP_BICYCLE = "bike",
    LAYER_GROUP_TRAFFIC = "ctt",
    LAYER_GROUP_CADASTRAL = "landparcel",
    LAYER_GROUP_MOUNTAIN = "mountain"
}
export declare enum Gravity {
    NO_GRAVITY = 0,
    AXIS_SPECIFIED = 1,
    AXIS_PULL_BEFORE = 2,
    AXIS_PULL_AFTER = 4,
    AXIS_X_SHIFT = 0,
    AXIS_Y_SHIFT = 4,
    TOP = 48,
    BOTTOM = 80,
    LEFT = 3,
    RIGHT = 5,
    CENTER_VERTICAL = 16,
    CENTER_HORIZONTAL = 1
}
export declare enum Align {
    Center = 0,
    Left = 1,
    Right = 2,
    Top = 3,
    Bottom = 4,
    TopLeft = 5,
    TopRight = 6,
    BottomRight = 7,
    BottomLeft = 8
}
export interface Rect {
    left?: number;
    top?: number;
    right?: number;
    bottom?: number;
}
export interface NaverMapViewProps {
    style?: StyleProp<ViewStyle>;
    center?: Coord & {
        zoom?: number;
        tilt?: number;
        bearing?: number;
    };
    tilt?: number;
    bearing?: number;
    mapPadding?: Rect;
    logoMargin?: Rect;
    logoGravity?: Gravity;
    onInitialized?: Function;
    onCameraChange?: (event: {
        latitude: number;
        longitude: number;
        zoom: number;
    }) => void;
    onMapClick?: (event: {
        x: number;
        y: number;
        latitude: number;
        longitude: number;
    }) => void;
    onTouch?: () => void;
    showsMyLocationButton?: boolean;
    compass?: boolean;
    scaleBar?: boolean;
    zoomControl?: boolean;
    mapType?: MapType;
    buildingHeight?: number;
    nightMode?: boolean;
    useTextureView?: boolean;
}
export default class NaverMapView extends Component<NaverMapViewProps> {
    ref?: RNNaverMapView;
    nodeHandle?: null | number;
    private resolveRef;
    animateToCoordinate: (coord: Coord) => void;
    animateToTwoCoordinates: (c1: Coord, c2: Coord) => void;
    animateToCoordinates: (coords: Coord[], bounds?: {
        top: number;
        bottom: number;
        left: number;
        right: number;
    }) => void;
    animateToRegion: (region: Region) => void;
    setLocationTrackingMode: (mode: number) => void;
    showsMyLocationButton: (show: boolean) => void;
    private dispatchViewManagerCommand;
    handleOnCameraChange: (event: React.SyntheticEvent<{}, {
        latitude: number;
        longitude: number;
        zoom: number;
    }>) => void;
    handleOnMapClick: (event: React.SyntheticEvent<{}, {
        x: number;
        y: number;
        latitude: number;
        longitude: number;
    }>) => void;
    render(): JSX.Element;
}
interface RNNaverMapView extends React.Component<{}, any> {
}
export interface MapOverlay {
    coordinate: Coord;
    onClick?: () => void;
}
export interface MarkerProps extends MapOverlay {
    anchor?: {
        x: number;
        y: number;
    };
    pinColor?: string;
    rotation?: number;
    flat?: boolean;
    image?: ImageSourcePropType;
    width?: number;
    height?: number;
    alpha?: number;
    animated?: boolean;
    caption?: {
        text?: string;
        align?: Align;
        textSize?: number;
        color?: string;
        haloColor?: string;
    };
    subCaption?: {
        text?: string;
        textSize?: number;
        color?: number;
        haloColor?: number;
    };
}
export declare class Marker extends Component<MarkerProps> {
    render(): JSX.Element;
}
export interface CircleProps extends MapOverlay {
    radius?: number;
    color?: string;
    outlineWidth?: number;
    outlineColor?: string;
    zIndex?: number;
}
export declare class Circle extends Component<CircleProps> {
    render(): JSX.Element;
}
interface PolylineProps extends Omit<MapOverlay, "coordinate"> {
    coordinates: Coord[];
    strokeWidth?: number;
    strokeColor?: string;
}
export declare class Polyline extends Component<PolylineProps> {
    render(): JSX.Element;
}
interface PolygonProps extends Omit<MapOverlay, "coordinate"> {
    coordinates: Coord[];
    outlineWidth?: number;
    outlineColor?: string;
    color?: string;
    holes?: Coord[][];
}
export declare class Polygon extends Component<PolygonProps> {
    render(): JSX.Element;
}
export interface PathProps extends Omit<MapOverlay, "coordinate"> {
    coordinates: Coord[];
    width?: number;
    color?: string;
    outlineWidth?: number;
    passedColor?: string;
    outlineColor?: string;
    passedOutlineColor?: string;
    pattern?: ImageSourcePropType;
    patternInterval?: number;
    progress?: number;
    zIndex?: number;
}
export declare class Path extends Component<PathProps> {
    render(): JSX.Element;
}
export {};

 세군데 수정해주면, 다음과 같이 동작 합니다.

반응형