qcoding

7)[사이드프로젝트]App개발-전국국밥_지역별 국밥찾기 페이지 본문

[사이드 프로젝트]App개발 - 전국 국밥 찾기

7)[사이드프로젝트]App개발-전국국밥_지역별 국밥찾기 페이지

Qcoding 2022. 2. 6. 11:54
반응형

지역별 국밥찾기 페이지

1) 사용되는 구성 npm

- 여기서 해당 페이지는 react-native-maps / react-native-maps-clustering / styled-component/ expo-location 가 주로 사용되었다.

"dependencies": {
    "@react-navigation/bottom-tabs": "^6.0.9",
    "@react-navigation/drawer": "^6.1.8",
    "@react-navigation/native": "^6.0.6",
    "@react-navigation/native-stack": "^6.2.5",
    "@types/styled-components": "^5.1.19",
    "@types/styled-components-react-native": "^5.1.3",
    "expo": "~44.0.2",
    "expo-app-loading": "~1.3.0",
    "expo-location": "~14.0.1",
    "expo-status-bar": "~1.2.0",
    "expo-web-browser": "~10.1.0",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "react-native": "0.64.3",
    "react-native-gesture-handler": "^2.2.0",
    "react-native-map-clustering": "^3.4.2",
    "react-native-maps": "^0.29.4",
    "react-native-modal": "^13.0.0",
    "react-native-push-notification": "^8.1.1",
    "react-native-reanimated": "^1.13.3",
    "react-native-safe-area-context": "3.3.2",
    "react-native-screens": "~3.10.1",
    "react-native-splash-screen": "^3.3.0",
    "react-native-web": "0.17.1",
    "realm": "^10.12.0",
    "styled-components": "^5.3.3"
  },

2) 구글맵 사용 환경셋팅

-> 구글맵 사용법은 아래의 블로그가 잘 정리되어 있어서 따라하면 좋을 것 같다.

https://agilog.tistory.com/1

 

리액트 네이티브에서 구글 맵 적용하기 Using a google maps API on the React Native, react-native-maps

리액트 네이티브 프로젝트를 진행하면서 맵뷰 Map View를 사용해야 할 일이 생겼습니다. 지도 API는 여러 가지가 있었는데 가장 최근까지 많은 커밋이 이루어진 지도 라이브러리를 사용하기로 했

agilog.tistory.com

-> 여기서 " 그리고 Gradle 빌드를 해주시면 끝입니다. " 라는 말이 나오는 데, 내 경우에는 따로 Gradle 빌드를 해주지 않아도 적용되었고, 아마 npm start 명령어나 npm run android 명령어 사용 시 자동으로 빌드가 되어서 그런 것 같다. 

한가지 중요한 것은 안드로이드 에뮬레이터 사용시에는 아래의 사진에서 Play Store에 표시가 된 경우에만 지도가 표시가 된다. 그렇지 않으면 서비스에 연결할 수 없다는 표시가 나면서 에뮬레이터가 지도가 표시 되지 않는다.

안드로이드 스튜디오 에뮬레이터 선택

3) expo-location API 사용

-> 앱 사용 시 사용자의 위치를 기준으로 맵을 띄우기 위하여 처음에 사용자 정보를 받아온다. 나는 Create React Native App 을 사용하기 때문에 expo 에서 제공하는 API가 사용가능하다. 만일 react native cli를 사용한다면 동일한 기능을하는 geolocation-serive를 사용하면 된다.

https://dev-yakuza.posstree.com/ko/react-native/react-native-geolocation-service/

 

React Native에서 현재 위치 정보 가져오기

react-native-geolocation-service 라이브러리를 이용하여 React Native에서 현재 위치 정보를 가져오는 방법에 대해서 알아봅시다.

dev-yakuza.posstree.com

4) 페이지 구성

해당 페이지는 전체가 FlatList로 되어있으며, 상단의 지도와 지역선택의 전체가 ListHeaderComponent={} 에 들어가 있으며, stickyHeaderIndices={[0]} 옵션을 주어서 화면에 고정되게 하였다. css상에서 zIndex를 높여서 항상위에 가도록 구성하였다.

FlatList내에서는 각 Item들이 한행에 3개씩 되도록 numColumn={3}으로 변경하여 작성하였다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

// FlatList 코드
      
      <FlatList
            // horizontal={true}
            contentContainerStyle={{backgroundColor:"white"}}
            ref={listRef}
            data={food_list[selRegion]}
            keyExtractor={(item) => item.diningCode_url + ""} 
            stickyHeaderIndices={[0]}
            ListHeaderComponent={
                <View style={{marginTop:0,zIndex:3,backgroundColor:"white"}}>
                    <View style={{justifyContent:"center"}}>
                        <MapView
                            // style={{flex:1,width:"100%",height:"100%"}}
                            style={styles.map}
                            clusterColor="#ffaf40"
                            clusterTextColor="white"
                            extent={200}
                            animationEnabled={false}
                            ref={map_ref}
                            style={{height:SCREEN_HEIGHT*0.4}}

                            initialRegion={{
                                latitude: region.latitude,
                                longitude: region.longitude,
                                latitudeDelta: LATITUDE_DELTA*0.1,
                                longitudeDelta: LONGITUDE_DELTA*0.1,
                            }}
                            showsUserLocation={true}
                            showsMyLocationButton={true}
                            onRegionChange={() => {

                            }}
                            onRegionChangeComplete={() => {
                                // console.log("지도움직이는것 멈춤");
                            }}
                            provider={PROVIDER_GOOGLE}
                            provider="google">
                            {food_list[selRegion].map((food, index) => (
                                <Marker
                                    coordinate={{
                                        latitude: food.lat,
                                        longitude: food.lon,
                                    }}
                                    title={food.name}
                                    description={food.keyword}
                                    // icon={require('../assets/alla.png')}
                                    key={index}
                                    onPress={()=>{
                                            // setRegion({
                                            //     name: food.name,
                                            //     address: food.address,
                                            //     latitude: food.lat,
                                            //     longitude: food.lon,
                                            //     latitudeDelta: LATITUDE_DELTA*0.1,
                                            //     longitudeDelta: LONGITUDE_DELTA*0.1,
                                            // })
                                        }
                                    }>
                                <Callout onPress={()=>{
                                    navigation.navigate("Stack",{screen:"Detail",params:{item:food}})
                                }}></Callout>
                                </Marker>
                            ))}
                        </MapView>
                    </View>
                    <ScrollView horizontal={true} showsHorizontalScrollIndicator={false} 
                        contentContainerStyle={{height:50,alignItems:"center",paddingLeft:7}} 
                    >
                        {region_list.map((region,index)=>{
                            // console.log(region,index)
                            return(
                                <Region index={index} region_list={region_list} key={index} region={region} selectRegion={selectRegion} setRegion={setRegion}></Region>
                            )

                        })}
                    </ScrollView>
                </View>
            }
            numColumns={3}
            columnWrapperStyle={{
                justifyContent: "space-between",
                }}
            ItemSeparatorComponent={() => <View style={{ height: 10}} />}
            renderItem={({item}) => {
                    return(
                        <FoodComponent setRegion={setRegion} item={item} scrollToTop={scrollToTop}></FoodComponent>
                    )
                }}

        >
        </FlatList>

5) 기능별 주요 구성

1) 처음 시작 시 사용자 위치받아오기

--> expo location을 통해서 처음 시작 시 gps정보를 받아오고 이정보를 setRegion을 통해서 위도와 경도값을 넣어준다.

아래의 구글맵 로딩시에는 처음 위치를 해당 위치로 렌더링되게 한다.

// expo location 
    const getLocation=async()=>{
        let { status } = await Location.requestForegroundPermissionsAsync();
        if (status !== "granted") {
          Alert.alert("내 주변 위치 음식점을 찾기위해 위치정보가 필요합니다.");
          return;
        }
        let locationSuccess = false;
          while (!locationSuccess) {
          try {
              console.log("try")
              let { coords: { latitude, longitude }} = await Location.getCurrentPositionAsync({
              accuracy: Location.Accuracy.High,
              });
              locationSuccess = true;
              console.log("set map loading")
              setRegion({
                  latitude: latitude,
                  longitude: longitude,
                  latitudeDelta: LATITUDE_DELTA,
                  longitudeDelta: LONGITUDE_DELTA,
                });
          } 
          catch (ex) {
              console.log("retring....");
          }
          finally {
            console.log("finally")
              setisloading(false);
              // 여기위치 맞나확인할것
              SplashScreen.hide();
          }
          }
          const unsubscribe = navigation.addListener('focus', () => {
            console.log('focus작동함');
          });
          return unsubscribe;
    }

    useEffect(() => {
        getLocation();
      }, []);

 

2) 음식점 위치버튼 클릭 시 지도상에서 해당 위치로 이동

-> 이 기능에는 크게 2가지가 있다. 한개는 음식점마다 있는 지도 버튼 클릭 시 지도의 위도 경도가 바뀌는 애니메이션이고, 지금은 사용하지 않지만 맨처음 구성 시 listHeaderComponent를 위에다가 고정하지 않았을 때 전체 스크롤이 내려가 있는 상태에서 지도 화면으로 이동을 위해 스크롤이 올라가는 애니메이션이다.

전체적인 구성은 1) ref로 컴포넌트를 선택 2) ref.currnet 값이 존재할 때 지도를 이동하거나 , 스크롤을 맨 위로 올라가게 애니메이션을 적용하였다.

// 특정 컴포넌트를 지정하기 위해 ref 사용

const listRef = useRef(null);
const map_ref = useRef(null);


// scroll to top FlatList를 움직이는 함수
// 지도 클릭 시 맨위로 화면을 올려서 지도 위치에 맞게 보여줌
const scrollToTop=()=>listRef.current.scrollToOffset({ offset: 0, animated: true });

// 지도 움직이는 함수
const moveMapView = () => {
    map_ref.current.animateToRegion(region, 1000);
};

// 지도 animation
if (map_ref.current) {
    moveMapView();

}

// ref 적용
 <FlatList
    ref={listRef} />
    
 // ref 적용
   <MapView
    ref={map_ref}

6) 전체코드

◆Food.js

-> 위에서 적용되어 있는 클러스터링을 사용하려면 <MapView> 컴포넌트를 사요할 때 'react-native-maps'가 아닌 'react-native-clustering"에서 import하면 된다. 클러스터 색이나 크기등은 공식문서를 통해 찾아가면서 변경하면된다.

import React,{useState,useContext,useEffect,useRef} from 'react'
import MapView ,{ PROVIDER_GOOGLE } from "react-native-map-clustering";
// import MapView ,{ PROVIDER_GOOGLE } from 'react-native-maps'
import { Marker, Callout, Circle} from "react-native-maps";
import { View, FlatList,StyleSheet,ScrollView,ActivityIndicator} from 'react-native'
import styled from 'styled-components/native';
import { DBContext } from './../Context';
import { Asset,useAssets } from 'expo-asset';
import Region from '../components/Region';
import {SCREEN_HEIGHT,LATITUDE_DELTA,LONGITUDE_DELTA} from '../util'
import FoodComponent from '../components/FoodComponent';
import * as Location from 'expo-location';
import SplashScreen from 'react-native-splash-screen'

const Food = ({navigation}) => {
    
    // realm에서 자료 받아오기
    const {customData:food_list}=useContext(DBContext);
    const region_list=Object.keys(food_list);

    const [isloading, setisloading] = useState(true);
    const [selRegion,setSelRegion]=useState(`${region_list[0]}`);

    const [region, setRegion] = useState({
        name: "장소를 선택해주세요",
        address: "장소를 선택해주세요",
        latitude: 37.4822971,
        longitude: 126.9030901,
        latitudeDelta: LATITUDE_DELTA*0.1,
        longitudeDelta: LONGITUDE_DELTA*0.1,
      });

    const listRef = useRef(null);
    const map_ref = useRef(null);

    // expo location 
    const getLocation=async()=>{
        let { status } = await Location.requestForegroundPermissionsAsync();
        if (status !== "granted") {
          Alert.alert("내 주변 위치 음식점을 찾기위해 위치정보가 필요합니다.");
          return;
        }
        let locationSuccess = false;
          while (!locationSuccess) {
          try {
              console.log("try")
              let { coords: { latitude, longitude }} = await Location.getCurrentPositionAsync({
              accuracy: Location.Accuracy.High,
              });
              locationSuccess = true;
              console.log("set map loading")
              setRegion({
                  latitude: latitude,
                  longitude: longitude,
                  latitudeDelta: LATITUDE_DELTA,
                  longitudeDelta: LONGITUDE_DELTA,
                });
          } 
          catch (ex) {
              console.log("retring....");
          }
          finally {
            console.log("finally")
              setisloading(false);
              // 여기위치 맞나확인할것
              SplashScreen.hide();
          }
          }
          const unsubscribe = navigation.addListener('focus', () => {
            console.log('focus작동함');
          });
          return unsubscribe;
    }

    useEffect(() => {
        getLocation();
      }, []);

    // scroll to top FlatList를 움직이는 함수
    // 지도 클릭 시 맨위로 화면을 올려서 지도 위치에 맞게 보여줌
    const scrollToTop=()=>listRef.current.scrollToOffset({ offset: 0, animated: true });
    

    const selectRegion=(region)=>{
        return(
            setSelRegion(region)
        )

    }
    // 지도 움직이는 함수
    const moveMapView = () => {
        map_ref.current.animateToRegion(region, 1000);
    };

    // 지도 animation
    if (map_ref.current) {
        moveMapView();
        
    }

    return ( isloading?
        <View style={{flex:1,justifyContent:"center",alignItems:"center"}}>
            <ActivityIndicator size="large" color="blue"></ActivityIndicator>
    </View>:<>
        <FlatList
            // horizontal={true}
            contentContainerStyle={{backgroundColor:"white"}}
            ref={listRef}
            data={food_list[selRegion]}
            keyExtractor={(item) => item.diningCode_url + ""} 
            stickyHeaderIndices={[0]}
            ListHeaderComponent={
                <View style={{marginTop:0,zIndex:3,backgroundColor:"white"}}>
                    <View style={{justifyContent:"center"}}>
                        <MapView
                            // style={{flex:1,width:"100%",height:"100%"}}
                            style={styles.map}
                            clusterColor="#ffaf40"
                            clusterTextColor="white"
                            extent={200}
                            animationEnabled={false}
                            ref={map_ref}
                            style={{height:SCREEN_HEIGHT*0.4}}

                            initialRegion={{
                                latitude: region.latitude,
                                longitude: region.longitude,
                                latitudeDelta: LATITUDE_DELTA*0.1,
                                longitudeDelta: LONGITUDE_DELTA*0.1,
                            }}
                            showsUserLocation={true}
                            showsMyLocationButton={true}
                            onRegionChange={() => {

                            }}
                            onRegionChangeComplete={() => {
                                // console.log("지도움직이는것 멈춤");
                            }}
                            provider={PROVIDER_GOOGLE}
                            provider="google">
                            {food_list[selRegion].map((food, index) => (
                                <Marker
                                    coordinate={{
                                        latitude: food.lat,
                                        longitude: food.lon,
                                    }}
                                    title={food.name}
                                    description={food.keyword}
                                    // icon={require('../assets/alla.png')}
                                    key={index}
                                    onPress={()=>{
                                            // setRegion({
                                            //     name: food.name,
                                            //     address: food.address,
                                            //     latitude: food.lat,
                                            //     longitude: food.lon,
                                            //     latitudeDelta: LATITUDE_DELTA*0.1,
                                            //     longitudeDelta: LONGITUDE_DELTA*0.1,
                                            // })
                                        }
                                    }>
                                <Callout onPress={()=>{
                                    navigation.navigate("Stack",{screen:"Detail",params:{item:food}})
                                }}></Callout>
                                </Marker>
                            ))}
                        </MapView>
                    </View>
                    <ScrollView horizontal={true} showsHorizontalScrollIndicator={false} 
                        contentContainerStyle={{height:50,alignItems:"center",paddingLeft:7}} 
                    >
                        {region_list.map((region,index)=>{
                            // console.log(region,index)
                            return(
                                <Region index={index} region_list={region_list} key={index} region={region} selectRegion={selectRegion} setRegion={setRegion}></Region>
                            )

                        })}
                    </ScrollView>
                </View>
            }
            numColumns={3}
            columnWrapperStyle={{
                justifyContent: "space-between",
                }}
            ItemSeparatorComponent={() => <View style={{ height: 10}} />}
            renderItem={({item}) => {
                    return(
                        <FoodComponent setRegion={setRegion} item={item} scrollToTop={scrollToTop}></FoodComponent>
                    )
                }}

        >
        </FlatList>
    </>
    )
}

const styles = StyleSheet.create({
    map: {
        ...StyleSheet.absoluteFillObject,
        height:'100%',
    },
});

export default Food

◆FoodComponent.js  ( FlatList의 renderItem에 사용되는 component )

옆에 보이는 그림이 FoodComponent이며 터치시에 아래의 지도이동 아이콘과 Detail 페이지 이동 아이콘이 접었다가 펴지면서 생기고, 

1) 지도페이지 클릭 시 props로 받은 setRegion에 위도,경도,이름, 주소 등을 넣게 되고 위의 ref를 통해 지도상으로 이동한다.

 

2) Detail 페이지 이동 아이콘 클릭 시에는        navigation.navigate("Stack",{screen:"Detail",params:{item}})
} 를 통해 Stack Navigator에 있는 Detail 페이지로 이동하며 params를 통해  item 정보를 보내게 된다.

 

import React,{useState} from 'react'
import styled from 'styled-components/native';
import { FontAwesome } from '@expo/vector-icons'; 
import {LATITUDE_DELTA,LONGITUDE_DELTA} from '../util';
import { useNavigation } from '@react-navigation/native';

const FoodListContainer=styled.TouchableOpacity`
    flex:0.31;
    align-items:center;
    justify-content:center;
    padding:7px 5px;
    background-color:rgba(200,200,200,0.4);
    border-radius:20px;
    /* border-width:2px;
    border-color:red; */
    
`;
const FoodTitleText=styled.Text`
    font-size:15px;
    font-weight:900;
    color:black;
`;

const FoodImage=styled.Image`
    width:60px;
    height:60px;
    border-radius:30px;
`;

const FoodKeyword=styled.Text`
    font-size:10px;
    font-weight:200;
    color:gray;
`;

const FoodScore=styled.Text`
    font-size:10px;
    font-weight:200;
    color:gray;
`
const FoodBtnContainer=styled.View`
    width:75px;
    justify-content:space-between;
    align-items:center;
    flex-direction:row;
    padding:5px 5px;
`;

const FoodBtn=styled.TouchableOpacity``;



const FoodComponent=({item,setRegion,scrollToTop})=>{

    const [detail,setDetail]=useState(false)

    // detail page로 가기 위한 navigation
    const navigation=useNavigation();
    const goToDetail=()=>{
        navigation.navigate("Stack",{screen:"Detail",params:{item}})
        }

    return(
        <FoodListContainer onPress={()=>{setDetail(prev=>!prev)}}>
            <FoodImage source={{uri:item.img_url}} ></FoodImage>
            <FoodTitleText>{item.name.length>8?`${item.name.slice(0,7)}...`:`${item.name}`}</FoodTitleText>
            <FoodScore>{item.score>=90?`⭐⭐⭐⭐⭐`:item.score>=80?`⭐⭐⭐⭐`:item.score>=70?`⭐⭐⭐`:item.score>=60?`⭐⭐`:`⭐`}</FoodScore>
            <FoodKeyword>
                {item.recommend_food.length>8?`${item.recommend_food.slice(0,8)}...`:`${item.recommend_food}`}
            </FoodKeyword>
            {detail&&
            <>
            <FoodKeyword>{item.address.length>10?`${item.address.slice(0,11)}...`:`${item.address}`}</FoodKeyword>             
            <FoodBtnContainer>
                <FoodBtn onPress={()=>{
                    return(
                        // scrollToTop(),
                        setRegion({
                            name: item.name,
                            address: item.address,
                            latitude: item.lat,
                            longitude: item.lon,
                            latitudeDelta: LATITUDE_DELTA*0.05,
                            longitudeDelta: LONGITUDE_DELTA*0.05,
                        })
                    )
                    }}>
                     <FontAwesome name="map-marker" size={35} color="#c0392b" />
                </FoodBtn>
                <FoodBtn onPress={()=>{
                    goToDetail();
                }}>
                     <FontAwesome name="info-circle" size={35} color="#2980b9" />
                </FoodBtn>
            </FoodBtnContainer>
            </>}

        </FoodListContainer>
        
    )
}

export default FoodComponent;
반응형
Comments