내일배움단 App3.4
[앱개발 종합반] 3주 4일차 개발일지(3주 6-7강, 숙제)
📱TIL App.304
1. Expo의 다양한 앱 기능 알아보기
1) 페이지 내용 공유하기
import { Share } from "react-native";
내용 공유 기능은 react-native
에서 기본적으로 제공해 주므로, 코드 상단에 위 코드를 작성해주면 된다. 공유 기능을 사용하기 위해 DetailPage에 ‘팁 공유하기’ 버튼을 추가해 준다.
// DetailPage.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Image, ScrollView, TouchableOpacity, Alert, Share } from 'react-native';
export default function DetailPage({navigation,route}) {
const [tip, setTip] = useState({
"idx":9,
"category":"재테크",
"title":"렌탈 서비스 금액 비교해보기",
"image": "https://storage.googleapis.com/sparta-image.appspot.com/lecture/money1.png",
"desc":"요즘은 정수기, ... ",
"date":"2020.09.09"
})
useEffect(()=>{
console.log(route)
navigation.setOptions({
title:route.params.title,
headerStyle: {
backgroundColor: '#000',
shadowColor: "#000",
},
headerTintColor: "#fff",
})
setTip(route.params)
},[])
const popup = () => {
Alert.alert("팝업!!")
}
const share = () => {
Share.share({
message:`${tip.title} \n\n ${tip.desc} \n\n ${tip.image}`,
});
}
return (
<ScrollView style={styles.container}>
<Image style={styles.image} source={{uri:tip.image}}/>
<View style={styles.textContainer}>
<Text style={styles.title}>{tip.title}</Text>
<Text style={styles.desc}>{tip.desc}</Text>
<View style={styles.buttonGroup}>
<TouchableOpacity style={styles.button} onPress={()=>popup()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={()=>share()}><Text style={styles.buttonText}>팁 공유하기</Text></TouchableOpacity>
</View>
</View>
</ScrollView>
)
}
코드를 살펴보면, 코드 최상단에 Share
도구를 가져온 것, share
함수를 설정한 것, ‘팁 공유하기’ 버튼에 onPress
속성으로 share
함수를 연결한 것을 볼 수 있다. 실제로 버튼을 눌러 카톡에 공유할 때, 링크가 아닌 꿀팁 제목, 설명, 이미지 주소가 텍스트로 전달된다. 이는 share
함수의 message
가 전달된 것이라 볼 수 있다. 이처럼, message
key 값에는 공유하고 싶은 내용을 입력한다. 우리가 자주 보는, 썸네일과 인터넷 주소를 공유하는 방식은 다음에 다루도록 한다.


2) 외부 링크 클릭 이벤트
expo install expo-linking
앱에서 휴대폰에 설치된 웹 브라우저로 외부 링크를 여는 방법을 알아보자. ‘이벤트’란 함수 기능을 뜻한다. 외부 링크 도구는 expo-linking
이라는 도구를 위 코드를 터미널에 입력해 설치해야 한다.
// DetailPage.js
import * as Linking from 'expo-linking';
export default function DetailPage({navigation,route}) {
const link = () => {
Linking.openURL("https://spartacodingclub.kr")
}
return(
...
<View style={styles.buttonGroup}>
<TouchableOpacity style={styles.button} onPress={()=>popup()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={()=>share()}><Text style={styles.buttonText}>팁 공유하기</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={()=>link()}><Text style={styles.buttonText}>외부 링크</Text></TouchableOpacity>
</View>
)
}
도구 설치 후, 설치한 도구는 코드의 최상단에 import
해서 사용한다. 불러온 모습을 보면 * as Linking
이라는 생소한 모습인데, 이는 expo-linking
을 모두 가지고 오는데, 가지고 올 때 그 도구의 이름을 ‘Linking’으로 설정한다는 뜻이다. Linking
을 불러온 이후에는 위 코드처럼 Linking
의 openURL
함수를 사용해서 연결하고자 하는 링크를 입력할 수 있다. 버튼을 눌러 함수가 실행되면 입력한 URL이 휴대폰의 기본 브라우저에서 열린다.
3주차 숙제(1) AboutPage.js 페이지화/버튼 추가하기
1) 결과 이미지


2) 코드 설계노트
MainPage에 <TouchableOpacity>
태그로 버튼을 추가하고, 버튼의 onPress
속성으로 함수를 달아야겠다. 함수는 navigation.navigate
를 사용해 AboutPage.js
파일로 연결되도록 해야지. 그러려면 StackNavigator.js
에 AboutPage.js
파일을 Stack.Screen
으로 연결시켜야겠다. Stack.Navigator
라는 책갈피에 이동할 페이지를 꽂아두는 느낌으로! 그보다 먼저 사용할 페이지를 StackNavigator.js
파일에 import
해와야 한다.
‘MainPage’에서의 버튼, 그리고 버튼을 누르면 ‘AboutPage’로 이동하는 것까지는 구현했다. 다음으로 ‘AboutPage’의 헤더 스타일을 바꾸려고 했는데 스스로 해보려니 조금 어려웠다. DetailPage.js
파일에서는 useEffect
함수 안에 navigation.setOptions
가 사용되었는데, AboutPage.js
파일에서 상태 관리를 하는 것은 아닌 것 같았다. StackNavigator.js
파일에서의 name
속성을 “소개 페이지”로 바꾸는 방법도 유효했지만, setOptions
함수 형태처럼 헤더 스타일을 바꾸는 다른 방법이 있을까 싶어 공식 문서를 찾아봤다.
예시 코드를 보니, StackNavigator.js
파일의 <Stack.Screen>
태그에 options
속성으로 title
, headerStyle
, headerTintColor
등 여러 속성을 지정할 수 있었다. 새로 데이터를 받아오는 것이 아니라 페이지 자체의 헤더 스타일을 변경하고 싶었기 때문에, 태그에 options
속성을 사용했다. 흰색으로 설정되어 있던 borderBottomColor
와 shadowColor
속성도 배경색과 같은 색으로 바꾸어 주었다. MainPage에서 검은색으로 설정된 앱 상태 바가 AboutPage에서는 잘 보이지 않아서, AboutPage.js
파일에 StatusBar
를 불러온 다음 태그를 넣고 light
로 설정해 주었다.
3) 나의 코드
// StackNavigator.js
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import MainPage from '../pages/MainPage';
import DetailPage from '../pages/DetailPage';
import AboutPage from '../pages/AboutPage';
const Stack = createStackNavigator();
const StackNavigator = () => {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "white",
borderBottomColor: "white",
shadowColor: "white",
height:100
},
headerTitleAlign:'left',
headerTitleStyle: {
fontSize: 18,
paddingLeft: 5
},
headerTintColor: "#000",
headerBackTitleVisible: false
}}
>
<Stack.Screen name="MainPage" component={MainPage}/>
<Stack.Screen name="DetailPage" component={DetailPage}/>
<Stack.Screen name="AboutPage" component={AboutPage} options={{
title: "소개 페이지",
headerStyle: {
backgroundColor: "#001064",
borderBottomColor: "#001064",
shadowColor: "#001064",
},
headerTintColor: "#fff"
}}/>
</Stack.Navigator>
)
}
export default StackNavigator;
// MainPage.js
import React, { useState, useEffect } from 'react';
import main from '../assets/main.png';
import { StyleSheet, Text, View, Image, TouchableOpacity, ScrollView } from 'react-native';
import data from '../data.json';
import Card from '../components/Card';
import Loading from '../components/Loading';
import { StatusBar } from 'expo-status-bar';
export default function MainPage({navigation,route}) {
console.disableYellowBox = true;
const [state,setState] = useState([])
const [cateState,setCateState] = useState([])
const [ready,setReady] = useState(true)
useEffect(()=>{
setTimeout(()=>{
navigation.setOptions({
title:'나만의 꿀팁'
})
let tip = data.tip;
setState(tip)
setCateState(tip)
setReady(false)
},1000)
},[])
const category = (cate) => {
if (cate == "전체보기") {
setCateState(state)
} else {
setCateState (state.filter((d)=>{
return d.category == cate
}))
}
}
let todayWeather = 10 + 17;
let todayCondition = "흐림"
return ready ? <Loading/> : (
<ScrollView style={styles.container}>
<StatusBar style="black" />
<Text style={styles.weather}>오늘의 날씨: {todayWeather + '°C ' + todayCondition} </Text>
<TouchableOpacity style={styles.aboutButton} onPress={() => {
navigation.navigate('AboutPage')}}><Text style={styles.middleButtonText}>소개 페이지</Text></TouchableOpacity>
<Image style={styles.mainImage} source={main}/>
<ScrollView style={styles.middleContainer} horizontal indicatorStyle={"white"}>
<TouchableOpacity style={styles.middleButtonAll} onPress={()=>{category('전체보기')}}><Text style={styles.middleButtonTextAll}>전체보기</Text></TouchableOpacity>
<TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>
<TouchableOpacity style={styles.middleButton02} onPress={()=>{category('재테크')}}><Text style={styles.middleButtonText}>재테크</Text></TouchableOpacity>
<TouchableOpacity style={styles.middleButton03} onPress={()=>{category('반려견')}}><Text style={styles.middleButtonText}>반려견</Text></TouchableOpacity>
<TouchableOpacity style={styles.middleButton04} onPress={()=>{category('꿀팁 찜')}}><Text style={styles.middleButtonText}>꿀팁 찜</Text></TouchableOpacity>
</ScrollView>
<View style={styles.cardContainer}>
{
cateState.map((content,i)=>{
return (<Card content={content} key={i} navigation={navigation}/>)
})
}
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#fff',
},
weather:{
alignSelf:"flex-end",
paddingRight:20
},
aboutButton: {
width:100,
height:45,
padding:10,
backgroundColor:"lightpink",
borderRadius:15,
marginTop: 10,
marginRight: 20,
alignSelf: "flex-end",
justifyContent: "center",
alignItems: "center"
},
mainImage: {
width:'90%',
height:200,
borderRadius:10,
marginTop:20,
alignSelf:"center"
},
middleContainer:{
marginTop:20,
marginLeft:10,
height:60
},
middleButtonAll: {
width:100,
height:50,
padding:15,
backgroundColor:"#20b2aa",
borderRadius:15,
margin:7,
justifyContent: "center",
alignItems: "center"
},
middleButton01: {
width:100,
height:50,
padding:15,
backgroundColor:"#fdc453",
borderColor:"deeppink",
borderRadius:15,
margin:7,
justifyContent: "center",
alignItems: "center"
},
middleButton02: {
width:100,
height:50,
padding:15,
backgroundColor:"#fe8d6f",
borderRadius:15,
margin:7,
justifyContent: "center",
alignItems: "center"
},
middleButton03: {
width:100,
height:50,
padding:15,
backgroundColor:"#9adbc5",
borderRadius:15,
margin:7,
justifyContent: "center",
alignItems: "center"
},
middleButton04: {
width:100,
height:50,
padding:15,
backgroundColor:"#f886a8",
borderRadius:15,
margin:7,
justifyContent: "center",
alignItems: "center"
},
middleButtonText: {
color:"#fff",
fontWeight:"700"
},
middleButtonTextAll: {
color:"#fff",
fontWeight:"700"
},
cardContainer: {
marginTop:10,
marginLeft:10
}
});
// AboutPage.js
import React from 'react';
import { StyleSheet, Text, View, Image, TouchableOpacity } from 'react-native';
import about from '../assets/about.png'
import { StatusBar } from 'expo-status-bar';
export default function AboutPage() {
console.disableYellowBox = true;
return (
<View style={styles.container}>
<StatusBar style="light" />
<View style={styles.titleContainer}>
<Text style={styles.titleText}>안녕하세요!{'\n'}스파르타코딩 앱 개발반에{'\n'}오신 것을 환영합니다.</Text>
</View>
<View style={styles.innerContainer}>
<Image
source={about}
resizeMode={"cover"}
style={styles.imageStyle}
/>
<Text style={styles.subText}>많은 내용을{'\n'}간결하게 담아내려{'\n'}노력했습니다!</Text>
<Text style={styles.descText}>꼭 완주하셔서{'\n'}여러분 것으로{'\n'}만들어가시길 바랍니다.</Text>
<TouchableOpacity style={styles.button}><Text style={styles.buttonText}>Instagram</Text></TouchableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
backgroundColor: "#001064"
},
titleContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center"
},
titleText: {
color: "#fff",
fontSize: 30,
fontWeight: "bold",
textAlign: "center",
marginTop: 5
},
innerContainer: {
flex: 3,
width: "82%",
borderRadius: 30,
marginBottom: 50,
backgroundColor: "#fff",
justifyContent: "center",
alignItems: "center"
},
imageStyle: {
width: 150,
height: 150,
borderRadius: 30,
marginBottom: 10
},
subText: {
fontSize: 21,
fontWeight: "bold",
textAlign: "center",
marginVertical: 15
},
descText: {
fontWeight: "bold",
fontSize: 16,
textAlign: "center",
marginVertical: 10
},
button: {
width: 110,
height: 50,
backgroundColor: "orange",
borderRadius: 15,
justifyContent: "center",
alignItems: "center",
marginTop: 15
},
buttonText: {
color: "white",
fontWeight: "600"
}
})
4) 해설 코드
// AboutPage.js
export default function AboutPage({navigation,route}){
useEffect(()=>{
navigation.setOptions({
title:"소개 페이지",
headerStyle: {
backgroundColor: '#1F266A',
shadowColor: "#1F266A",
},
headerTintColor: "#fff",
})
},[])
}
해설 코드를 보고 내가 헷갈렸던 부분을 알아차렸다. useEffect
는 상태와 아무 관련이 없는 별개의 영역이다. useEffect
는 단지 화면이 그려지고 난 후 가장 먼저 실행되는 함수일 뿐이다. 그 함수에 상태값을 변경하는 함수가 들어있을 뿐, 둘을 엮어서 생각한 것이 문제였다. 그래서 나는 StackNavigator.js
파일의 페이지 태그에서 options
속성을 활용했지만, useEffect
로 해당 페이지 파일 내에서 setOptions
함수를 사용할 수 있다. navigation
을 사용해야 하므로 AboutPage
함수 parameter로 {navigation, route}
를 준 것도 체크하자.
3주차 숙제(2) 꿀팁 찜 화면 만들기
두 번째 숙제는, 제일 끝에 위치한 ‘꿀팁 찜’ 버튼을 눌렀을 때, ‘찜’한 팁들을 모아보는 페이지를 구현하는 것이다. 실제 데이터를 받아오는 것은 배우지 않았기 때문에, 데이터 값을 받아서 화면을 구성해보자. 또, Card.js
와 똑같은 코드를 사용하지만, ‘찜 페이지’에서만 사용될 LikeCard.js
component 파일도 만들어보자.
1) 결과 이미지


2) 코드 설계
새로운 페이지를 만들어야 하므로 LikePage.js
파일을 pages
폴더에 생성한다. 다음으로 해당 페이지를 StackNavigator
폴더에 import
, <Stack.Screen>
태그에 연결한 후 MainPage.js
에서 ‘꿀팁 찜’ 버튼에 onPress
로 navigate
함수를 연결한다. Card.js
코드와 같은 코드인 LikeCard.js
파일을 component
폴더에 생성하고, 위의 기본 데이터가 카드 리스트에 사용될 수 있도록 한다.
3) 나의 코드
// MainPage.js
<TouchableOpacity style={styles.middleButton04} onPress={() => {
navigation.navigate('LikePage')}}>
<Text style={styles.middleButtonText}>꿀팁 찜</Text>
</TouchableOpacity>
// StackNavigator.js
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import MainPage from '../pages/MainPage';
import DetailPage from '../pages/DetailPage';
import AboutPage from '../pages/AboutPage';
import LikePage from '../pages/LikePage';
const Stack = createStackNavigator();
const StackNavigator = () => {
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "white",
borderBottomColor: "white",
shadowColor: "white",
height:100
},
headerTitleAlign:'left',
headerTitleStyle: {
fontSize: 18,
paddingLeft: 5
},
headerTintColor: "#000",
headerBackTitleVisible: false
}}
>
<Stack.Screen name="MainPage" component={MainPage}/>
<Stack.Screen name="DetailPage" component={DetailPage}/>
<Stack.Screen name="AboutPage" component={AboutPage} options={{
title: "소개 페이지",
headerStyle: {
backgroundColor: "#001064",
borderBottomColor: "#001064",
shadowColor: "#001064",
},
headerTintColor: "#fff"
}}/>
<Stack.Screen name="LikePage" component={LikePage} options={{title: "꿀팁 찜"}}/>
</Stack.Navigator>
)
}
export default StackNavigator;
// LikeCard.js
import React from 'react';
import {View, Image, Text, StyleSheet, TouchableOpacity} from 'react-native'
export default function LikeCard({content,navigation}){
return(
<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>
)
}
const styles = StyleSheet.create({
card:{
flex:1,
flexDirection:"row",
margin:10,
borderBottomWidth:0.5,
borderBottomColor:"#eee",
paddingBottom:10
},
cardImage: {
flex:1,
width:100,
height:100,
borderRadius:10,
},
cardText: {
flex:2,
flexDirection:"column",
marginLeft:10,
},
cardTitle: {
fontSize:20,
fontWeight:"700"
},
cardDesc: {
fontSize:15
},
cardDate: {
fontSize:10,
color:"#A6A6A6",
}
});
// LikePage.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, ScrollView } from 'react-native';
import LikeCard from '../components/LikeCard';
export default function LikePage({navigation}) {
const [tip, setTip] = useState([{
"idx":3,
"category":"재테크",
"title":"잠자는 내 돈을 찾아라",
"image": "https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fmoney1.png?alt=media&token=491096e7-0b57-40a3-991b-b984193f8018",
"desc":"‘새는 돈’에는 미처 몰랐던 ...",
"date":"2020.09.09"
},
{
"idx":4,
"category":"재테크",
"title":"할인행사, 한정할인판매 문구의 함정 탈출!",
"image": "https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fmoney2.png?alt=media&token=9c9df304-16e8-4a6f-8ae4-1d3f9ad58134",
"desc":"‘안 사면 100% 할인’이라는 ...",
"date":"2020.09.09"
}])
return (
<ScrollView style={styles.container}>
<View style={styles.cardContainer}>
{
tip.map((content,i)=>{
return (<LikeCard content={content} key={i} navigation={navigation}/>)
})
}
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
cardContainer: {
marginTop:10,
marginLeft:10
}
})
4) 🤔 QnA
생각과는 달리 구현에 꽤나 애를 먹었는데, 각 태그의 속성과 네비게이터에 대한 이해가 부족했던 때문인 것 같다. 헷갈렸던 부분과 다시 기억할 부분을 정리해보자.
✅ 파일 이름과 파일의 함수 이름은 같아야 한다.
☑️ 처음엔 아래 코드처럼 반복문 없이 <LikeCard>
태그만 사용하려고 했었다. 고유의 key
값은 반복문에서 필수적이었으므로 key
속성은 제외했다. 그런데 화면에 헤더만 뜨고 데이터는 나오지 않았다.
// LikePage.js
// 수정 전
<ScrollView style={styles.container}>
<View style={styles.cardContainer}>
<LikeCard content={content} navigation={navigation}/>
</View>
</ScrollView>
// 수정 후
<ScrollView style={styles.container}>
<View style={styles.cardContainer}>
{
tip.map((content,i)=>{
return (<LikeCard content={content} key={i} navigation={navigation}/>)
})
}
</View>
</ScrollView>
✅ 생각해보니, 하나 이상의 데이터를 같은 형식으로 출력하려면 반복문을 사용해야 했다. 반복문을 사용하되, 반복문을 돌릴 array를 tip
으로 설정하기만 하면 해결되는 문제였다. 사실 처음에 useState
parameter, 즉 tip
변수의 초기값을 리스트(array)가 아닌 딕셔너리(object) 형태로 잘못 본 것이 큰 것 같다. 무의식중에 ‘딕셔너리를 반복문으로 어떻게 돌리지…?’라는 생각으로 반복문을 사용하지 않은 것 같다. 앞으로 데이터를 사용할 때는 map
method로 반복문을 이용하면서 반복문 돌릴 array를 잘 체크하자!
☑️ LikeCard.js
파일에서 LikeCard
함수의 parameter로 {tip, navigation}
을 주려고 했었다.
✅ parameter로 주어지는 것은 전달받은 속성의 이름이다. Card.js
파일도 동일한 방식으로, MainPage.js
에서 반복문을 돌릴 때 content
속성을 전달받아 사용했었다. 아래 코드로 개념을 다시 정리해보자.
// MainPage.js
// cateState array의 각 원소를 content라는 변수명으로 설정하고 같은 이름의 속성/속성값 전달
<View style={styles.cardContainer}>
{
cateState.map((content,i)=>{
return (<Card content={content} key={i} navigation={navigation}/>)
})
}
</View>
// Card.js
// MainPage.js의 <Card> 태그에서 지정한 'content' 속성을 parameter로 받아 사용
// 'content' 속성은 `cateState`의 각 element값(얘 이름도 content)을 담고 있음
export default function Card({content,navigation}){
return(
<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>
...
)}
5) 해설 코드
// LikePage.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, ScrollView } from 'react-native';
import LikeCard from '../components/LikeCard';
import Card from '../components/Card';
export default function LikePage({navigation, route}){
Card.js
component를 불러오는 것, 그리고 LikePage
함수 parameter로 route
를 넘겨주는 것 모두 추후 데이터를 불러올 때 필요한 포인트다. 코드를 짤 때 이용되지 않아서 쓰지는 않았지만, 앞으로 쓰일 데이터니까 미리 입력해두자!
Leave a comment