<스파르타 App개발> 4주차 개발일지 (完)

이번 주차에는 우리가 만든 앱과 서버의 통신에 대해 다루는데

 


이전 웹개발 종합반에서의 기억을 되새겨보면..

 

Python library인 Flask서버를 구축하였고

구축한 서버를 AWS EC2 클라우드 컴퓨터에서 구동하였다. 

 

이후 

 

client side : 1) 페이지가 로딩되면, 2) 서버에 "정보들을 보여줘"라고 요청을 보낸다. (조회할래! Read, 즉 GET 요청)

server side : 3) 요청을 받으면 DB에서 현재 저장된 정보들을 불러와, 4) client에 넘겨주면

client side : 5) 받은 정보를 웹페이지에 뿌려서 보게된다. 

 

이런 방법으로 핑퐁을 하면서 클라이언트와 서버가 통신을 주고받았다. 


이번 앱개발 종합반에서는 복잡한 서버 구축과 DB까지 제공해주는 google의 서비스인 Firebase를 통해 이를 대체하였다. 

 

 

1. 앱과 서버

앱에 모든 정보를 다 때려 넣는다면? 앱이 무거워지고, 느려지며, 불필요한 정보까지 client가 가지고 있는 문제가 발생한다. 

따라서 앱에서도 대부분의 데이터는 서버에 넣어 두고, 앱이 요청할 때만 전달해주는 방식을 사용한다. 

 

즉, 앱이 요청(Request)을 하면 서버는 응답(Response)을 하게 된다. 

이때 당연히 서버가 정한 규칙에 따라 요청을 해야 하며, 서버가 정한 이 규칙을 API(Application Programming Interface)라고 한다. 

 

API

이러한 규칙은 서버가 제공하는 도메인일 수도 있고

www.sparta.com/getdata ←- 데이터 조회 API
www.sparta.com/setData ←- 데이터 저장 API

 

서버가 미리 만들어둔 함수를 사용하기도 한다. 

db.ref('/like/').on('value') ←- 데이터 조회 API
db.ref('/like/').set(new_like); <-- 데이터 저장 API

 

이때 서버에서는 DB의 종류에 따라 다르겠지만 JSON 형식으로 데이터를 전달하고

보통 React native로 만든 App에서는

  • 앱 화면이 렌더링된 후 useEffect 함수가 실행될 때 데이터가 준비되거나,
  • 사용자가 요청하는 버튼을 누르는 등의 액션을 취할 때 데이터를 보내주게 된다. 

 

 

날씨 API 연습 (도메인 형식으로 요청하기! axios)

앱에서 현재 날씨를 제공하는 서버에 정보를 요청하고, 받아온 정보를 앱 메인 화면에 띄워주는 방식으로 연습을 해보자. 

날씨 제공 무료 API인 openweathermap api를 사용한다. 

 

1) 어떻게 요청할까?

해당 API의 정보를 요청할 때의 규칙을 알아보려면? 공식 문서를 참조하자. 

 

https://openweathermap.org/api

 

Weather API - OpenWeatherMap

Please, sign up to use our fast and easy-to-work weather APIs. As a start to use OpenWeather products, we recommend our One Call API 3.0. For more functionality, please consider our products, which are included in professional collections.

openweathermap.org

요청 방식은 다음과 같다. 

즉 우리에게 필요한 것은, 현재 앱이 실행되는 위치의 위도(latitude), 경도(longitude), 그리고 API 접속에 필요한 API key이다. API key는 수업에서 제공하니 넘어가고, 위도와 경도는 어떻게 구할까?

 

2) 필요한 정보를 구해보자

앱이 실행되는 기기의 위도와 경도 데이터를 구해주는 것은 expo가 제공한다. 바로 expo-location 이다. 

https://docs.expo.dev/versions/latest/sdk/location/

 

Location - Expo Documentation

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

docs.expo.dev

 

expo-location을 설치하고

expo install expo-location
import * as Location from "expo-location";

  const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestForegroundPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
      console.log(locationData)
    } catch (error) {
      //혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
      Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

getLocation 함수를 구성한다. 

 

이때 처음보는 async, await 구문과 try, catch 구문이 있는데 간단히 살펴보자. 


Async await

위의 예시처럼 함수 앞에 async를 붙이고, 함수 실행부에 await을 붙이면

이제 await가 붙은 코드들은 순서 없이 실행된다. 

 

즉 Location.request ---- 로 사용자의 위치사용허가를 얻는 부분과

const locationData 로 현재의 location을 얻어서 해당 변수에 넣는 부분이

꼭 코드 윗줄부터가 아니라 각자 실행되는 것이다. 

 

이 경우를 예를 들자면 사용자의 허용버튼 터치가 늦어질 경우, 미리 데이터를 받아오는 것이 앱의 빠른 구동에 효율적이므로 async await 문을 사용한 것이다. 

 

 

Try catch

Try catch 문은 에러가 발생할 것을 대비하여 사용하는 문법이다. 

try 문 내의 코드를 실행하되, 에러가 발생하면 이 에러를 catch 내의 error라는 변수에 담고, catch 문을 실행하게 된다. 


이렇게 expo-location을 이용하여 locationData에 현재의 위치데이터를 담았다. 

locationData의 형태를 확인해보고, 위도와 경도는 각각

const latitude = locationData['coords']['latitude']
const longitude = locationData['coords']['longitude']

형태로 선언하였다. 

 

 

3) 정보를 요청하자

openweathermap API는 도메인형태로 요청(request)을 받는다.

이때 사용하는 javascript 패키지가 axios이다. 

yarn을 이용하여 이를 설치하고

yarn add axios

axios.get( URL ) 

import axios from "axios"

const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestForegroundPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
      console.log(locationData)
      console.log(locationData['coords']['latitude'])
      console.log(locationData['coords']['longitude'])
      const latitude = locationData['coords']['latitude']
      const longitude = locationData['coords']['longitude']
      const API_KEY = "cfc258c75e1da2149c33daffd07a911d";
      const result = await axios.get(
        `http://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=
${API_KEY}&units=metric`
      );

      console.log(result)

    } catch (error) {
      //혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
      Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

위와 같이 JSON 형태의 데이터를 result에 담아 반환한다. 

반환하는 데이터의 형태 또한 공식문서에 나와 있으며, 우리가 필요한 데이터인 온도와 날씨 또한 간편하게 확인할 수 있다. 

const temp = result.data.main.temp; 
const condition = result.data.weather[0].main

이 데이터를 앱에 어떻게 반영할까?

 

4) 정보를 반영하자

이 또한 실시간으로 변화하는 데이터이므로 상태변수로 정의해야 한다.

 

const [weather, setWeather] = useState({
    temp : 0,
    condition : ''
  })

useEffect(() => {
	setTimeout(() => {
    	getLocation()
    }, 1000)

const getLocation = async () => {
	// 어쩌구 저쩌구 데이터 얻어 오는 부분
    const temp = result.data.main.temp; 
    const condition = result.data.weather[0].main
    setWeather({temp, condition})

이렇게 하면 화면 렌더링 후 useEffect가 실행되면서 상태변수가 변하고, 다시 해당 정보를 담아 렌더링한다. 

 

이렇게 앱이 API와 어떻게 상호작용하는지 domain 방식으로 정보를 요청받는 api인

openweathermap API와 axios를 이용하여 실습해 보았다. 

 

 

 

2. 서버리스, Firebase

위의 날씨API 처럼 우리도 서버를 만들고, DB를 구축하고 API를 만들면

우리가 만든 app에서 API와 통신하여 데이터를 주고 받을 수 있다. 

다만, 너무 번거롭다. 

 

 

이를 해결해주는 것이 서버리스이며, 여러 서비스들이 있으나 우리는 구글의 Firebase를 이용한다. 

파이어베이스에 가입하고 프로젝트를 생성한 후, 우리의 앱과 연결하면 끝난다. 

자체 DB를 제공하기 때문에 아주 간편하다. 

 

앱과 Firebase를 연결하는 방법

파이어베이스 프로젝트를 생성하고, 

우리의 app 프로젝트는 Js를 활용하여 웹 앱으로 인식하므로, 웹 앱에 파이어베이스를 추가한다. 

 

 

앱에서는 expo 파이어베이스 도구를 설치하고, 

expo install firebase

프로젝트 경로에 firebaseConfig.js 파일을 생성한다. 

파일의 내용은 강의에서는 제공해주고 있으나, firebase 웹사이트와는 모양이 다르다. 

무엇을 사용하던지 상관은 없다.

 

 

Firebase에서 제공하는 storage

크게 두가지 형식의 스토리지를 사용하는데, 

간단히 JSON 형식의 파일은 realtime DB에 저장할 수 있고

기타 파일(이미지 등)file stroage에 저장할 수 있다. 

 

storage에 저장한 이미지 파일 등은 제공하는 url을 이용하여 불러와서 앱에 사용할 수 있다. 

 

JSON 형태의 realtime DB에는 마찬가지로 접근 규칙을 알아야 한다. 

(단, firebaseConfig에 realtime DB 정보가 없는 경우가 많기 때문에 따로 추가해주어야 한다.)

 

 

Realtime Database와의 통신 (CRUD)

모든 데이터 읽기

firebase_db.ref('/tip').once('value').then((snapshot) => {
   let tip = snapshot.val();
})

 

*** 이때 파이어베이스는 데이터를 .cjs 파일 형태로 되돌려 주는데

이런 cjs 파일을 읽을 수 없는 경우

프로젝트 경로에 metro.config.js 파일을 만들고 

module.exports = {
    transformer: {
      getTransformOptions: async () => ({
        transform: {
          experimentalImportSupport: false,
          inlineRequires: false,
        },
      }),
    },
    resolver: {
      sourceExts: ['jsx', 'js', 'ts', 'tsx', 'cjs', 'fs'],
    },
  };

해당 내용을 추가해야 읽을 수 있다.

 

특정 데이터 읽기

매번 데이터를 통으로 불러와야 한다면 속도도 느려지고 너무 무겁다. 

따라서 index를 이용하여 필요한 데이터만 불러오는 것이 권장된다. 

 

현재 꿀팁 앱에서 카드를 누르면 DetailPage로 연결시켜 보여주는데

//MainPage로 부터 navigation 속성을 전달받아 Card 컴포넌트 안에서 사용
export default function Card({content,navigation}){
    return(
        //카드 자체가 버튼역할로써 누르게되면 상세페이지로 넘어가게끔 TouchableOpacity를 사용
        <TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage',content)}}>
            <Image style={styles.cardImage} source={{uri:content.image}}/>
            <View style={styles.cardText}>
                <Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
                <Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
                <Text style={styles.cardDate}>{content.date}</Text>
            </View>
        </TouchableOpacity>
    )
}

이때 MainPage.js에서 Card.js로 넘겨준 'content'를 통해 카드를 보여주고 있는데

이를 클릭하면(onPress) DetailPage로 또다시 content 전체를 넘겨주어 화면을 그려주게 된다. 

 

이렇게 전체 데이터가 왔다갔다 하지 않고, index만 넘겨주면 

DetailPage 자체에서 index 번호를 이용하여 서버와 통신해서 데이터를 가져오는 방식으로 바꿔보려 한다. 

 

왜??

  • 이 데이터들도 실시간으로 바뀔 수 있기 때문에!!
  • 큰 데이터들이 매번 이동하면 앱 퍼포먼스가 저하되기 때문에!!!

 

따라서 다음과 같이 바꿔보자. 

1) Card -> DetailPage로 이동할 때

<TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage',{idx:content.idx})}}>

idx 키 값에 content.idx만 넘겨주었다. 

 

2) DetailPage 렌더링

    useEffect(()=>{
        console.log(route)
        navigation.setOptions({
            title:route.params.title,
            headerStyle: {
                backgroundColor: '#000',
                shadowColor: "#000",
            },
            headerTintColor: "#fff",
        })
        //넘어온 데이터는 route.params에 들어 있습니다.
        const { idx } = route.params;
        firebase_db.ref('/tip/'+idx).once('value').then((snapshot) => {
            let tip = snapshot.val();
            setTip(tip)
        });
    },[])

Card -> DetailPage로 넘어온 데이터는 route.params에 들어 있으므로

idx라는 변수에 route.params(즉, content.idx)를 구조분해할당하여 받아서

firebase에 요청하여 tip을 setTip하여 화면을 그려주면 된다. 

 

 

데이터 쓰기

꿀팁 앱에는 꿀팁 찜 기능이 있다. 

즉, 상세 페이지에서 찜한 꿀팁만을 꿀팁 찜 페이지에서 보여주는 기능이다. 

 

이를 구현하려면..

팁 찜하기를 누르면,

누가 + 어떠한 꿀팁을 

저장했는지 확인하여 이를 포함한 데이터를 DB에 저장해주어야 한다. 

 

1) 누가??

기기의 unique ID를 확인하는 도구를 expo에서 제공한다. 

expo install expo-application
import * as Application from 'expo-application';
import { Platform } from 'react-native'
const isIOS = Platform.OS === 'ios';


let uniqueId;
if(isIOS){
  let iosId = await Application.getIosIdForVendorAsync();
  uniqueId = iosId
}else{
  uniqueId = Application.androidId
}

console.log(uniqueId)

 

2) 어떤 꿀팁을??

DetailPage의 꿀팁 찜하기 버튼을 누를 때

<TouchableOpacity style={styles.button} onPress={()=>like()}>
	<Text style={styles.buttonText}>팁 찜하기</Text>
</TouchableOpacity>

like 함수가 실행되는데

현재 그려지는 페이지는 tip의 데이터를 바탕으로 그려지므로, tip과 uniqueID를 DB에 보내주면 된다. 

    const like = async () => {
        
        // like 방 안에
        // 특정 사용자 방안에
        // 특정 찜 데이터 아이디 방안에
        // 특정 찜 데이터 몽땅 저장!
        // 찜 데이터 방 > 사용자 방 > 어떤 찜인지 아이디
        let userUniqueId;
        if(isIOS){
        let iosId = await Application.getIosIdForVendorAsync();
            userUniqueId = iosId
        }else{
            userUniqueId = await Application.androidId
        }

        console.log(userUniqueId)
	       firebase_db.ref('/like/'+userUniqueId+'/'+ tip.idx).set(tip,function(error){
             console.log(error)
             Alert.alert("찜 완료!")
         });
    }

 

Firebase의 Realtime DB의 

'/like/userID/tip의 idx번호   주소에 tip 데이터를 저장하였다. 

 

 

3) 찜 데이터 모아서 보여주기 (LikePage)

이제 사용자별로(uniqueID) 찜한 데이터를 모아두었으니

찜한 꿀팁만 모아서 보여주는 기능을 완성해야 한다. 

기존 LikePage에 데이터를 대충 넣어두었는데, 이를 서버에서 가져와서 보여주는 방식으로 고쳐주면 된다. 

 

firebase_db.ref('/like'+userUniqueId).once('value').then((snapshot) => {
          console.log("파이어베이스에서 데이터 가져왔습니다!!")
          let tip = snapshot.val();
})



return (
        <ScrollView style={styles.container}>
           {
               tip.map((content,i)=>{
                   // LikeCard에서 꿀팀 상태 데이터(==tip)과 꿀팁 상태 데이터를 변경하기 위한
                   // 상태 변경 함수(== setTip)을 건네준다.
                   //즉 자기 자신이 아닌, 자식 컴포넌트에서도 부모의 상태를 변경할 수 있다.
                   return(<LikeCard key={i} content={content} navigation={navigation} tip={tip} setTip={setTip}/>)
               })
           }
        </ScrollView>
    )
}

 

문제점

만약 찜해둔 데이터가 없다면? tip이 비어있게 되므로 tip.map을 돌릴 때 에러가 나게 된다. 

 

따라서 다음과 같이 tip 데이터의 길이가 true일 때만 ready 상태변수를 바꿔주는 식으로 고쳐주면 된다. 

또한 tip은 { }딕셔너리 내의 {}딕셔너리기 때문에 tip.length가 아닌

Object.values(tip).length를 사용하면 된다. 

 

 

데이터 삭제하

위와 같은 LikePage에서 찜 해제 버튼을 누르면 DB에서 해당 정보가 삭제되고

화면에서도 사라지게 만들어 보자. 

<TouchableOpacity style={styles.button} onPress={()=>remove(content.idx)}>
	<Text style={styles.buttonText}>찜 해제</Text>
</TouchableOpacity>

찜 해제 버튼에 remove()함수를 연결해주고, content index를 넘겨준다. 

 

const remove = async (cidx) => {
      let userUniqueId;
      if(isIOS){
      let iosId = await Application.getIosIdForVendorAsync();
          userUniqueId = iosId
      }else{
          userUniqueId = await Application.androidId
      }
      firebase_db.ref('/like/'+userUniqueId+'/'+cidx).remove().then(function(){
        Alert.alert("삭제 완료");
        let result = tip.filter((data,i)=>{
          return data.idx !== cidx
        })
        console.log(result)
        setTip(result)
      })

firebase_db.ref().remove() 메서드로

해당 컨텐츠를 삭제하고

tip은 LikePage로부터 넘어온 상태변수이다.

result 변수를 선언하여 

핵심!!!!!

"LikePage에서 LikeCard를 호출할 때 넘겨받은 데이터인 tip"

중 idx가 cidx(지울 content의 idx)와 같지 않은 데이터들만 filter하여 이를 result에 담고

setTip을 이용하여 result를 새로운 tip 값으로 넣어주면

 

LikePage의 상태변수인 tip이 변화하였으므로..

LikePage의 useEffect가 실행되면서

새로 가져온 데이터로 LikePage가 렌더링되게 된다. 

 

 

 

 

 

3. 마무리

이렇게 꿀팁 앱을 4주간 만들어 보았다. 

앱을 간단히 소개하면 다음과 같다. 

 

현재 날씨를 실시간으로 보여주며

소개 페이지를 누르면 소개 페이지로 넘어간다. 

 

 

 

카테고리 기능이 있어서

카테고리 버튼을 누를 경우 해당 카테고리의 내용만을 보여준다. 

 

 

 

카드를 터치하면 카드의 상세 페이지로 넘어가게 되며

상세 페이지에서 팁 찜하기 버튼을 누르면 

팝업과 함께 저장되며, 메인페이지에서 꿀팁 찜 버튼으로 이동할 수 있는 꿀팁 찜 페이지에서 이를 확인할 수 있다. 

 

찜 해제 버튼을 누르면 삭제 완료 팝업과 함께 데이터가 사라진다. 

 

팁 공유하기 버튼의 경우 실제로 공유 기능을 제공한다. 

 

 

이렇게 4주간 간단한 어플리케이션을 제작해보았다. 

단순한 기능만 담고 있지만, 이를 응용한다면 꽤 그럴듯하고 유용한 앱을 만들 수 있을 것으로 기대된다.

 

5주차 수업도 있으나, 광고를 붙이거나 앱을 배포하는 과정에 대해서만 다루고 있어 블로그에는 따로 정리하지 않기로 한다.

 

 

 

끝!

댓글()