복습
👉 내가 작성한 코드
import React from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
SafeAreaView,
} from "react-native";
export default function DetailPage() {
const tip = {
idx: 9,
category: "재테크",
title: "렌탈 서비스 금액 비교해보기",
image: "https://storage.googleapis.com/sparta-image.appspot.com/lecture/money1.png",
desc: "요즘은 정수기, 공기 청정기, 자동차나 장난감 등 다양한 대여서비스가 활발합니다. 사는 것보다 경제적이라고 생각해 렌탈 서비스를 이용하는 분들이 늘어나고 있는데요. 다만, 이런 렌탈 서비스 이용이 하나둘 늘어나다 보면 그 금액은 겉잡을 수 없이 불어나게 됩니다. 특히, 렌탈 서비스는 빌려주는 물건의 관리비용까지 포함된 것이기에 생각만큼 저렴하지 않습니다. 직접 관리하며 사용할 수 있는 물건이 있는지 살펴보고, 렌탈 서비스 항목에서 제외해보세요. 렌탈 비용과 구매 비용, 관리 비용을 여러모로 비교해보고 고민해보는 것이 좋습니다. ",
date: "2020.09.09",
};
const { title, image, desc } = tip;
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<Image
style={styles.img}
source={{
uri: image,
}}
/>
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.desc}>{desc}</Text>
<TouchableOpacity style={styles.button}>
<Text style={styles.textStyle}>팁 찜하기</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#000",
},
img: {
width: "100%",
height: "50%",
borderRadius: 20,
marginHorizontal: 5,
marginBottom: 30,
},
textContainer: {
marginHorizontal: 15,
},
title: {
color: "#fff",
fontSize: 30,
fontWeight: "bold",
textAlign: "center",
marginBottom: 10,
},
desc: {
fontSize: 20,
color: "#fff",
marginBottom: 20,
},
button: {
width: 130,
height: 50,
borderWidth: 2,
borderColor: "rgb(153, 37, 101)",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
alignSelf: "center",
},
textStyle: {
color: "#fff",
fontSize: 18,
},
});
- 여기에 버튼 눌렀을 때 팝업이 뜨는 것을 구현한다면 react-native 에서는 onPress={} 를 통해서 구현할 수 있다.
컴포넌트화
이전 주차에 만들었던 메인화면 MainPage.js 의 카드들을 컴포넌트화 해보자.
보통 컴포넌트들은 components 폴더에 모아서 관리한다.
components 폴더를 만들고 폴더 안에 Card.js 파일을 만들어보자.
Card.js
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
export default function Card({ content }) {
const { image, title, desc, date } = content;
return (
<View style={styles.card}>
<Image style={styles.cardImg} source={{ uri: image }} />
<View style={styles.textContainer}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>
{desc}
</Text>
<Text style={styles.cardDate}>{date}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
flex: 1,
flexDirection: "row",
marginHorizontal: 20,
paddingVertical: 15,
borderBottomWidth: 0.5,
borderBottomColor: "#eee",
},
cardImg: {
flex: 1,
width: "100%",
height: "100%",
borderRadius: 8,
marginRight: 10,
},
textContainer: {
flex: 2,
flexDirection: "column",
height: "100%",
},
cardTitle: {
fontSize: 22,
fontWeight: "bold",
},
cardDesc: {
fontSize: 18,
},
cardDate: {
fontSize: 16,
color: "gray",
},
});
MainPage.js
import React from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
SafeAreaView,
} from "react-native";
import data from "../data.json";
import Card from "../components/Card";
const mainImg =
"https://storage.googleapis.com/sparta-image.appspot.com/lecture/main.png";
const cardImg =
"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3";
export default function MainPage() {
const tip = data.tip;
return (
<SafeAreaView>
<ScrollView style={styles.container}>
{/* 타이틀 */}
<Text style={styles.title}>나만의 꿀팁</Text>
{/* 메인 이미지 */}
<Image source={{ uri: mainImg }} style={styles.mainImage} />
{/* 탭 버튼 목록 */}
<ScrollView
horizontal
style={styles.btnContainer}
indicatorStyle={"white"}
>
<TouchableOpacity style={styles.btn01}>
<Text style={styles.textStyle}>생활</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn02}>
<Text style={styles.textStyle}>재테크</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn03}>
<Text style={styles.textStyle}>반려견</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn04}>
<Text style={styles.textStyle}>꿀팁 찜</Text>
</TouchableOpacity>
</ScrollView>
{/* 카드 목록 */}
{tip.map((content, i) => {
return <Card content={content} key={i} />;
})}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
},
title: {
fontSize: 25,
marginTop: 20,
marginLeft: 20,
fontWeight: "bold",
},
mainImage: {
width: "90%",
height: 200,
borderRadius: 5,
alignSelf: "center",
margin: 10,
},
btnContainer: {
margin: 20,
height: 60,
},
btn01: {
backgroundColor: "rgb(246, 204, 113)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn02: {
backgroundColor: "rgb(241, 156, 131)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn03: {
backgroundColor: "rgb(178, 223, 208)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn04: {
backgroundColor: "rgb(238, 150, 180)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
textStyle: {
color: "#fff",
},
});
상태 관리하기
useEffect
보통 useEffect 는 데이터를 준비할 때 사용한다.
데이터를 준비한다는 것은 데이터를 서버로부터 혹은 어디선가로부터 받은 후 상태에 반영한다는 것을 뜻한다.
- 화면이 그려진다.
- useEffect 가 데이터를 준비한다.
- 상태 데이터가 업데이트 되었으니 화면이 다시 그려진다.
화면이 그려진 다음, 서버에게 필요한 데이터를 요청하여 받은 후, 화면을 다시 그릴 때 주로 사용된다!
실습: data.json 을 상태에 넣어 관리해보기
MainPage.js
import React, {useState, useEffect} from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
SafeAreaView,
} from "react-native";
import data from "../data.json";
import Card from "../components/Card";
const mainImg =
"https://storage.googleapis.com/sparta-image.appspot.com/lecture/main.png";
const cardImg =
"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3";
export default function MainPage() {
const [state, setState] = useState([]);
// 하단의 return 문이 실행되어 화면이 그려진 다음 실행되는 useEffect 함수
// 내부에서 data.json 으로 부터 가져온 데이터를 state 상태에 담고 있음
useEffect(() => {
setState(data);
}, [])
// data.json 데이터는 state 에 담기므로 상태에서 꺼내옴
let tip = state.tip;
return (
<SafeAreaView>
<ScrollView style={styles.container}>
{/* 타이틀 */}
<Text style={styles.title}>나만의 꿀팁</Text>
{/* 메인 이미지 */}
<Image source={{ uri: mainImg }} style={styles.mainImage} />
{/* 탭 버튼 목록 */}
<ScrollView
horizontal
style={styles.btnContainer}
indicatorStyle={"white"}
>
<TouchableOpacity style={styles.btn01}>
<Text style={styles.textStyle}>생활</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn02}>
<Text style={styles.textStyle}>재테크</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn03}>
<Text style={styles.textStyle}>반려견</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn04}>
<Text style={styles.textStyle}>꿀팁 찜</Text>
</TouchableOpacity>
</ScrollView>
{/* 카드 목록 */}
{tip.map((content, i) => {
return <Card content={content} key={i} />;
})}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
},
title: {
fontSize: 25,
marginTop: 20,
marginLeft: 20,
fontWeight: "bold",
},
mainImage: {
width: "90%",
height: 200,
borderRadius: 5,
alignSelf: "center",
margin: 10,
},
btnContainer: {
margin: 20,
height: 60,
},
btn01: {
backgroundColor: "rgb(246, 204, 113)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn02: {
backgroundColor: "rgb(241, 156, 131)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn03: {
backgroundColor: "rgb(178, 223, 208)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn04: {
backgroundColor: "rgb(238, 150, 180)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
textStyle: {
color: "#fff",
},
});
그러나 실행을 하면 다음과 같이 오류 화면이 뜬다.
🚨 오류 원인
- 화면이 그려진다.
- useEffect 가 데이터를 state 에 useState 를 이용하여 업데이트 한다.
- 상태(state)가 변경되었으니 화면이 다시 그려진다.
여기서 1번에서 오류가 난다.
화면이 가장 먼저 그려질 때, 우리가 Card 에 데이터를 넘기기 위해 state 에 tip 키 값에 접근하여 데이터를 꺼내 map 반복문을 돌리려고 했다. 하지만 state 에 데이터가 처음부터 담겨 있지 않다.
1. 화면이 그려진다 에서 return 구문에 해당하는 컴포넌트들이 일단 화면에 그린다. 하지만 tip.map 을 사용하고 있는데 이 tip 에는 state.tip 값이 들어있다. 이는 state 값에 해당하는데 useEffect가 실행되기 이전이기 때문에 아직 setState로 state에 data 가 들어가지 않았고, 결국엔 화면에 그려질때의 state 값은 useState([]) 초기값인 빈 배열이게 된다.
useEffect 를 거쳐야 state 에 데이터가 담기는데 아직 없는 상태이기 때문에 오류가 나게 되는 것이다.
>> 처음부터 데이터가 없어서, 꺼내올 수 없는 오류
👉 이런 오류는 데이터가 담긴 다음에 가져올 수 있도록 로딩화면을 만들어줌으로써 해결이 가능하다.
로딩화면 만들기
components 폴더에 Loading.js 파일을 만들어주자.
Loading.js
import React from 'react';
import {View,Text,StyleSheet} from 'react-native';
export default function Loading(){
return(<View style={styles.container}><Text style={styles.title}>준비중입니다...</Text></View>)
}
const styles = StyleSheet.create({
container: {
//앱의 배경 색
flex:1,
justifyContent:'center',
alignItems:'center',
backgroundColor: '#fdc453',
},
title: {
fontSize:20,
fontWeight:'700'
}
})
MainPage.js
import React, {useState, useEffect} from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
SafeAreaView,
} from "react-native";
import data from "../data.json";
import Card from "../components/Card";
import Loading from "../components/Loading";
const mainImg =
"https://storage.googleapis.com/sparta-image.appspot.com/lecture/main.png";
const cardImg =
"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3";
export default function MainPage() {
const [state, setState] = useState([]);
const [ready, setReady] = useState(true);
useEffect(() => {
setTimeout(()=>{
setState(data)
setReady(false)
}, 1000)
}, [])
let tip = state.tip;
// ready 가 true 상태일 때 Loading 페이지를 보여주고,
// setTimeout 에 의해 1초 뒤에 데이터가 담기고, ready 가 false 가 되면 이하 부분을 보여준다.
return ready ? <Loading /> : (
<SafeAreaView>
<ScrollView style={styles.container}>
{/* 타이틀 */}
<Text style={styles.title}>나만의 꿀팁</Text>
{/* 메인 이미지 */}
<Image source={{ uri: mainImg }} style={styles.mainImage} />
{/* 탭 버튼 목록 */}
<ScrollView
horizontal
style={styles.btnContainer}
indicatorStyle={"white"}
>
<TouchableOpacity style={styles.btn01}>
<Text style={styles.textStyle}>생활</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn02}>
<Text style={styles.textStyle}>재테크</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn03}>
<Text style={styles.textStyle}>반려견</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn04}>
<Text style={styles.textStyle}>꿀팁 찜</Text>
</TouchableOpacity>
</ScrollView>
{/* 카드 목록 */}
{tip.map((content, i) => {
return <Card content={content} key={i} />;
})}
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
},
title: {
fontSize: 25,
marginTop: 20,
marginLeft: 20,
fontWeight: "bold",
},
mainImage: {
width: "90%",
height: 200,
borderRadius: 5,
alignSelf: "center",
margin: 10,
},
btnContainer: {
margin: 20,
height: 60,
},
btn01: {
backgroundColor: "rgb(246, 204, 113)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn02: {
backgroundColor: "rgb(241, 156, 131)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn03: {
backgroundColor: "rgb(178, 223, 208)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn04: {
backgroundColor: "rgb(238, 150, 180)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
textStyle: {
color: "#fff",
},
});
state 를 이용한 카테고리 만들기
카테고리 기능을 위해선 먼저 카테고리 상태가 필요하다.
카테고리에 맞는 꿀팁들을 지속적으로 저장하고 관리할 상태 그릇으로 사용하기 위해서!
그리고 함수가 필요하다.
각 카테고리 버튼에 연결할 함수로, 카테고리에 따라 카테고리 상태 데이터를 새롭게 구성해 주는 기능을 한다.
MainPage.js
import React, {useState, useEffect} from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
SafeAreaView,
} from "react-native";
import data from "../data.json";
import Card from "../components/Card";
import Loading from "../components/Loading";
const mainImg =
"https://storage.googleapis.com/sparta-image.appspot.com/lecture/main.png";
const cardImg =
"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3";
export default function MainPage() {
const [state, setState] = useState([]); // data.json 꿀팁 전체 데이터
const [ready, setReady] = useState(true); // 로딩 상태 관리
const [cateState, setCateState] = useState([]); // 카테고리 분류 관리
useEffect(() => {
setTimeout(()=>{
setState(data.tip)
setCateState(data.tip)
setReady(false)
}, 1000)
}, [])
const category = (cate) => {
if (cate == '전체보기') {
// 전체보기면 원래 꿀팁 데이터 전체를 담고 있는 상태 값으로 다시 초기화
setCateState(state)
} else {
// category 함수 인자로 받아온 cate 를 data.json 에서 찾아서 해당 카테고리 데이터를 보여줌.
setCateState(state.filter((d) => {
return d.category == cate
}))
}
}
let tip = state.tip;
return ready ? <Loading /> : (
<SafeAreaView>
<ScrollView style={styles.container}>
{/* 타이틀 */}
<Text style={styles.title}>나만의 꿀팁</Text>
{/* 메인 이미지 */}
<Image source={{ uri: mainImg }} style={styles.mainImage} />
{/* 탭 버튼 목록 */}
<ScrollView
horizontal
style={styles.btnContainer}
indicatorStyle={"white"}
>
<TouchableOpacity style={styles.btnAll} onPress={()=>{category('전체보기')}}>
<Text style={styles.textStyle}>전체보기</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn01} onPress={()=>{category('생활')}}>
<Text style={styles.textStyle}>생활</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn02} onPress={()=>{category('재테크')}}>
<Text style={styles.textStyle}>재테크</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn03} onPress={()=>{category('반려견')}}>
<Text style={styles.textStyle}>반려견</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn04} onPress={()=>{category('꿀팁 찜')}}>
<Text style={styles.textStyle}>꿀팁 찜</Text>
</TouchableOpacity>
</ScrollView>
{/* 카드 목록 */}
{/* tip.map 이 아닌 카테고리 상태별로 보여주도록 cateState로 설정 /*}
{cateState.map((content, i) => {
return <Card content={content} key={i} />;
})}
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
},
title: {
fontSize: 25,
marginTop: 20,
marginLeft: 20,
fontWeight: "bold",
},
mainImage: {
width: "90%",
height: 200,
borderRadius: 5,
alignSelf: "center",
margin: 10,
},
btnContainer: {
margin: 20,
height: 60,
},
btnAll: {
backgroundColor: "#20b2aa",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn01: {
backgroundColor: "rgb(246, 204, 113)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn02: {
backgroundColor: "rgb(241, 156, 131)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn03: {
backgroundColor: "rgb(178, 223, 208)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn04: {
backgroundColor: "rgb(238, 150, 180)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
textStyle: {
color: "#fff",
},
});
재테크를 눌렀을 때 해당 컨텐츠만 보이는 모습!
상태바 관리
Expo 상태바 설치
🛠 공식문서
expo install expo-status-bar
컴포넌트마다 다르게 적용할 수도 있고, 앱 전체에 공통적으로 적용할 수도 있다.
import { StatusBar } from 'expo-status-bar';
export default function MainPage() {
...
return ready ? <Loading/> : (
<ScrollView style={styles.container}>
<StatusBar style="light" />
...
)
}
👉 style이 light / black 일 때 비교해보기
네비게이터
네비게이터는 앱에 웹사이트를 이용하듯, 컴포넌트들을 페이지화 시켜주고, 해당 페이지끼리 이동을 가능하게 해주는 라이브러리 이다.
이 라이브러리도 Expo에서 지원하고 있는 도구로써, 앱을 만들기위해 사용중인 Expo와 잘 맞는다.
페이지 구성은 보통 pages 폴더 안에 생성해둔 컴포넌트로 구성한다.
지금까지 pages 폴더 안에 만든 페이지는 MainPage, AboutPage, DetailPage 3가지의 페이지가 있다.
리액트 네이티브에서 페이지 네비게이션을 구현하기란 조금 까다롭다.
그래서 필요한 것만 가져와 설치하고, 적용해 나갈 계획이다.
아래 명령어들은 네비게이션을 사용하기 위해 필요한 기본 라이브러리들이다.
곧 알아볼 스택 네비게이션과 탭 네비게이션 기능은 추가적으로 라이브러리를 또 설치해주어야 한다.
네비게이션 설치
yarn add @react-navigation/native
네비게이션 추가 설치코드
expo install react-native-screens react-native-safe-area-context react-native-gesture-handler
스택 네비게이션
스택 네비게이션은 컴포넌트에 페이지 기능울 부여해주고 컴포넌트에서 컴포넌트로 이동, 즉 페이지 이동을 가능하게 해준다.
컴포넌트를 페이지화 시키는 스택 네비게이션은 다음과 같다.
우리가 페이지처럼 만든 컴포넌트를 정말 페이지처럼 사용할 수 있게끔 페이지로 컴포넌트를 감싸 페이지로 만들어준다.
이렇게 만든 여러 페이지들을 책갈피 기능을 하는 스택 네비게이터에 모두 등록시켜서, 언제든지 이 페이지 이동이 가능하게끔 해준다.
👉 페이지는 Stack.Screen 이라 부르며, 책갈피는 Stack.Navigator 라 부른다.
설치
createStackNavigator 사용해보기
yarn add @react-navigation/stack
적용
navigation 폴더 하나를 만들고 StackNavigator.js 파일을 만들어 준다.
그리고 StackNavigator 안에 다음 코드를 넣는다.
import React from 'react';
//설치한 스택 네비게이션 라이브러리를 가져옵니다
import { createStackNavigator } from '@react-navigation/stack';
//페이지로 만든 컴포넌트들을 불러옵니다
import DetailPage from '../pages/DetailPage';
import MainPage from '../pages/MainPage';
//스택 네비게이션 라이브러리가 제공해주는 여러 기능이 담겨있는 객체를 사용합니다
//그래서 이렇게 항상 상단에 선언하고 시작하는게 규칙입니다!
const Stack = createStackNavigator();
const StackNavigator = () =>{
return (
//컴포넌트들을 페이지처럼 여기게끔 해주는 기능을 하는 네비게이터 태그를 선언합니다.
//위에서 선언한 const Stack = createStackNavigator(); Stack 변수에 들어있는 태그를 꺼내 사용합니다.
//Stack.Navigator 태그 내부엔 페이지(화면)를 스타일링 할 수 있는 다양한 옵션들이 담겨 있습니다.
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "black",
borderBottomColor: "black",
shadowColor: "black",
height:100
},
headerTintColor: "#FFFFFF",
headerBackTitleVisible: false
}}
>
{/* 컴포넌트를 페이지로 만들어주는 엘리먼트에 끼워 넣습니다. 이 자체로 이제 페이지 기능을 합니다*/}
<Stack.Screen name="MainPage" component={MainPage}/>
<Stack.Screen name="DetailPage" component={DetailPage}/>
</Stack.Navigator>
)
}
export default StackNavigator;
App.js
import React from 'react';
import { StatusBar } from 'expo-status-bar';
// 이제 컴포넌트를 불러오지 않고 네비게이션 도구들을 불러오면 된다.
import {NavigationContainer} from '@react-navigation/native';
import StackNavigator from './navigation/StackNavigator';
export default function App() {
console.disableYelloBox = true;
return (
<NavigationContainer>
<StatusBar style='black'/>
<StackNavigator />
</NavigationContainer>
);
}
페이지 헤더 스타일
모든 페이지 상단에 매번 공통적인 정보/스타일을 적용해주고 싶을 때 screenOptions 를 이용할 수 있다.
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: "white",
borderBottomColor: "white",
shadowColor: "white",
height:100
},
//헤더의 텍스트를 왼쪾에 둘지 가운데에 둘지를 결정
headerTitleAlign:'left',
headerTintColor: "#000",
headerBackTitleVisible: false
}}
>
{/* 컴포넌트를 페이지로 만들어주는 엘리먼트에 끼워 넣습니다. 이 자체로 이제 페이지 기능을 합니다*/}
<Stack.Screen name="MainPage" component={MainPage}/>
<Stack.Screen name="DetailPage" component={DetailPage}/>
</Stack.Navigator>
페이지 이동하기
MainPage.js 에서 카드 버튼(Card.js)을 누르면 꿀팁 상세 페이지(DetailPage.js)로 이동한다.
Stack.screen 에 등록된 모든 페이지 컴포넌트들은 navigation 과 route 라는 딕셔너리(객체)를 속성으로 넘겨받아 사용할 수 있다.
navigation 객체가 가지고 있는 함수
- setOptions: 해당 페이지의 제목을 설정할 수 있다.
navigation.setOptions({
title:'나만의 꿀팁'
})
- navigate: StackNavigator 에서 Stack.screen 에서 name 속성으로 정해준 이름을 지정해주면 해당 페이지로 이동하는 함수
navigation.navigate("DetailPage")
- 이 때 데이터도 같이 넘겨주고 싶다면?
첫번째 인자로 name 을 전달해주고, 두번째 인자로 딕셔너리 데이터를 전달해주면, DetailPage 에서 두번째 인자로 전달된 딕셔너리 데이터를 route 딕셔너리로 받을 수 있다.
navigation.navigate("DetailPage",{
title: title
})
전달 받은 데이터를 받는 형식은 route 딕셔너리로 받게 된다.
//전달받은 데이터를 받는 route 딕셔너리
//비구조 할당 방식으로 route에 params 객체 키로 연결되어 전달되는 데이터를 꺼내 사용
//navigate 함수로 전달되는 딕셔너리 데이터는 다음과 같은 모습이기 때문입니다.
{
route : {
params :{
title:title
}
}
}
const { title} = route.params;
카드 버튼을 누르면 페이지가 이동되게 구현해야 하므로 Card.js 에 페이지 이동 기능을 달아보자!
⭐ [실습] 데이터 없이 페이지 이동하기
navigation.navigate("DetailPage")
MainPage.js
MainPage 에 navigation 과 route 인자를 전달받는다. 이는 어디서 넘겨준 것일까?
👉 Stack.Screen 으로 부터 넘겨받은 navigation 과 route
: StackNavigator.js 에서 Stack.Screen 을 통해 컴포넌트를 연결시켜 페이지화 시켜주었고,
페이지화를 당하면 두 가지 딕셔너리를 사용할 수 있게 된다. 그것이 바로 navigation 과 route 이다.
이 때 카드 버튼을 누르면 페이지가 이동되어야 하는데, Stack.Screen 에 의해 페이지화 되지 않은 Card.js 는 navigation을 사용할 수 없다. 따라서 MainPage.js 에서 카드 컴포넌트에게 navigation 을 넘겨줌으로써 페이지 이동 기능을 사용할 수있도록, 네비게이션 도구를 쥐어 준다.
import React, {useState, useEffect} from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
ScrollView,
SafeAreaView,
} from "react-native";
import { StatusBar } from "expo-status-bar";
import data from "../data.json";
import Card from "../components/Card";
import Loading from "../components/Loading";
const mainImg =
"https://storage.googleapis.com/sparta-image.appspot.com/lecture/main.png";
const cardImg =
"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3";
// 페이지 이동을 구현
// Stack.Screen 으로부터 MainPage 는 navigation과 route 를 인자로 전달받는다.
export default function MainPage({navigation, route}) {
const [state, setState] = useState([]);
const [ready, setReady] = useState(true);
const [cateState, setCateState] = useState([]);
useEffect(() => {
setTimeout(()=>{
// 헤더의 타이틀 변경
navigation.setOptions({
title: '나만의 꿀팁'
})
setState(data.tip)
setCateState(data.tip)
setReady(false)
}, 1000)
}, [])
const category = (cate) => {
if (cate == '전체보기') {
setCateState(state)
} else {
setCateState(state.filter((d) => {
return d.category == cate
}))
}
}
let tip = state.tip;
return ready ? <Loading /> : (
<SafeAreaView>
<ScrollView style={styles.container}>
<StatusBar style="light" />
{/* 타이틀 */}
{/* <Text style={styles.title}>나만의 꿀팁</Text> */}
{/* 메인 이미지 */}
<Image source={{ uri: mainImg }} style={styles.mainImage} />
{/* 탭 버튼 목록 */}
<ScrollView
horizontal
style={styles.btnContainer}
indicatorStyle={"white"}
>
<TouchableOpacity style={styles.btnAll} onPress={()=>{category('전체보기')}}>
<Text style={styles.textStyle}>전체보기</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn01} onPress={()=>{category('생활')}}>
<Text style={styles.textStyle}>생활</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn02} onPress={()=>{category('재테크')}}>
<Text style={styles.textStyle}>재테크</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn03} onPress={()=>{category('반려견')}}>
<Text style={styles.textStyle}>반려견</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn04} onPress={()=>{category('꿀팁 찜')}}>
<Text style={styles.textStyle}>꿀팁 찜</Text>
</TouchableOpacity>
</ScrollView>
{/* 카드 목록 */}
{/* 카드 컴포넌트 눌렀을 때 페이지가 이동될 수 있도록 navigation 을 넘겨준다.*/}
{cateState.map((content, i) => {
return <Card content={content} key={i} navigation={navigation} />;
})}
</ScrollView>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#fff",
},
title: {
fontSize: 25,
marginTop: 20,
marginLeft: 20,
fontWeight: "bold",
},
mainImage: {
width: "90%",
height: 200,
borderRadius: 5,
alignSelf: "center",
margin: 10,
},
btnContainer: {
margin: 20,
height: 60,
},
btnAll: {
backgroundColor: "#20b2aa",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn01: {
backgroundColor: "rgb(246, 204, 113)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn02: {
backgroundColor: "rgb(241, 156, 131)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn03: {
backgroundColor: "rgb(178, 223, 208)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
btn04: {
backgroundColor: "rgb(238, 150, 180)",
width: 100,
borderRadius: 5,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
},
textStyle: {
color: "#fff",
},
});
Card.js
import React from "react";
import { View, Text, Image, StyleSheet, TouchableOpacity } from "react-native";
// MainPage 로 부터 navigation 을 전달 받았다.
export default function Card({ content, navigation }) {
const { image, title, desc, date } = content;
return (
// 카드 자체가 버튼 역할로써 누르게 되면 상세페이지로 넘어가게끔 View→TouchableOpacity 를 사용
// 카드를 눌렀을 때 페이지가 이동되도록 navigation 함수 연결
<TouchableOpacity style={styles.card} onPress={()=>
{navigation.navigate('DetailPage')}}>
<Image style={styles.cardImg} source={{ uri: image }} />
<View style={styles.textContainer}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>
{desc}
</Text>
<Text style={styles.cardDate}>{date}</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
flex: 1,
flexDirection: "row",
marginHorizontal: 20,
paddingVertical: 15,
borderBottomWidth: 0.5,
borderBottomColor: "#eee",
},
cardImg: {
flex: 1,
width: "100%",
height: "100%",
borderRadius: 8,
marginRight: 10,
},
textContainer: {
flex: 2,
flexDirection: "column",
height: "100%",
},
cardTitle: {
fontSize: 22,
fontWeight: "bold",
},
cardDesc: {
fontSize: 18,
},
cardDate: {
fontSize: 16,
color: "gray",
},
});
⭐ [실습] 데이터 가지고 페이지 이동하기
지금은 어떠한 카드를 눌러도 상세페이지(DetailPage)에서 동일한 데이터를 보고 있다.
tip 데이터가 고정되어 있으므로..!
이제는 Card 에서 DetailPage 로 이동할 때, MainPage 로 부터 넘겨 받은 content 도 넘겨줘보자!
Card.js
import React from "react";
import { View, Text, Image, StyleSheet, TouchableOpacity } from "react-native";
// MainPage 로 부터 navigation 을 전달 받았다.
export default function Card({ content, navigation }) {
const { image, title, desc, date } = content;
return (
// 카드 자체가 버튼 역할로써 누르게 되면 상세페이지로 넘어가게끔 View→TouchableOpacity 를 사용
// 카드를 눌렀을 때 페이지가 이동되도록 navigation 함수 연결
// 두번째 인자로 넘겨줄 데이터인 content 를 넣어준다.
<TouchableOpacity style={styles.card} onPress={()=>
{navigation.navigate('DetailPage', content)}}>
<Image style={styles.cardImg} source={{ uri: image }} />
<View style={styles.textContainer}>
<Text style={styles.cardTitle}>{title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>
{desc}
</Text>
<Text style={styles.cardDate}>{date}</Text>
</View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
card: {
flex: 1,
flexDirection: "row",
marginHorizontal: 20,
paddingVertical: 15,
borderBottomWidth: 0.5,
borderBottomColor: "#eee",
},
cardImg: {
flex: 1,
width: "100%",
height: "100%",
borderRadius: 8,
marginRight: 10,
},
textContainer: {
flex: 2,
flexDirection: "column",
height: "100%",
},
cardTitle: {
fontSize: 22,
fontWeight: "bold",
},
cardDesc: {
fontSize: 18,
},
cardDate: {
fontSize: 16,
color: "gray",
},
});
DetailPage.js
import React, { useEffect, useState } from "react";
import {
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
SafeAreaView,
ScrollView
} from "react-native";
export default function DetailPage({navigation, route}) {
// 기존에 DetailPage 에 고정된 데이터는 useState의 기본값으로 넣어주었다.
// 이는 화면에 그려질 때 tip 에 데이터가 없어서 오류가 뜨는 것을 방지할 수 있다!
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",
});
const { title, image, desc } = tip;
useEffect(()=> {
console.log(route)
// Card.js 에서 navigation.navigate 함수를 쓸 때 두번째 인자로 content 를 넘겨주었다.
// content 는 딕셔너리 자체였으므로 route.params에 고대로 들어온다.
// 즉, route.params 는 content 이다.
navigation.setOptions({
title: route.params.title,
// StackNavigator 에서 작성했던 옵션을 다시 수정할 수도 있다.
headerStyle: {
backgroundColor: '#000',
shadowColor: '#000',
},
headerTintColor: '#fff',
})
setTip(route.params)
}, [])
return (
<SafeAreaView style={styles.container}>
<ScrollView>
<Image
style={styles.img}
source={{
uri: image,
}}
/>
<View style={styles.textContainer}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.desc}>{desc}</Text>
<TouchableOpacity style={styles.button}>
<Text style={styles.textStyle}>팁 찜하기</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#000",
},
img: {
width: "95%",
height: "100%",
alignSelf: "center",
borderRadius: 20,
marginTop: 10,
marginBottom: 30,
},
textContainer: {
marginHorizontal: 15,
},
title: {
color: "#fff",
fontSize: 30,
fontWeight: "bold",
textAlign: "center",
marginBottom: 10,
},
desc: {
fontSize: 20,
color: "#fff",
marginBottom: 20,
},
button: {
width: 130,
height: 50,
borderWidth: 2,
borderColor: "rgb(153, 37, 101)",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
alignSelf: "center",
},
textStyle: {
color: "#fff",
fontSize: 18,
},
});
console.log(route) 시 담겨져 있는 값 확인
🚨 여기도 마찬가지로 useState 를 통해 데이터 상태를 관리해주고 있고, useEffect 에서 화면이 그려진 후에 setTip 으로 데이터가 담길 수 있도록 작성되어 있다.
예전에 MainPage 구현할 때 처럼 state 에 담긴 데이터가 없는 상태에서 화면을 그리려고 하면 오류가 뜨지 않나? 라고 생각할 수 있다. 그러면 이것도 로딩화면을 만들어주어야 하나 생각이 들 수 있다.
이 때 우리가 이전에 만들었던 고정된 데이터 값을 useState의 기본값으로 넣어주면 오류가 나지 않는다.
기본 데이터가 한번 그려지고 → useEffect 가 실행되어 setTip 으로 tip 에 데이터가 들어가고 → 변경된 값으로 다시 화면이 그려지는 과정이 이루어지게 된다!
Share 공유하기
Share 기능은 설치 할 라이브러리 없이, react-native 에서 기본적으로 제공해주는 공유 기능을 사용한다.
import { Share } from "react-native";
const share = () => {
Share.share({
message: `${tip.title} \n\n ${tip.desc} \n\n ${tip.image}`,
});
}
...
<TouchableOpacity style={styles.button} onPress={()=>{share()}}><Text>공유하기</Text></TouchableOpacity>
👉 공유 모습
외부 링크 클릭 이벤트
앱에서 외부 링크를 여는 방법에 대해 알아보자.
버튼을 누르면 버튼에 연결시킨 기능을 통해 외부 링크를 핸드폰에 있는 브라우저로 열어본다.
설치
expo 에서 제공해주는 도구를 설치한 다음, 해당 도구를 상단에 가져와 준비해야 한다.
expo install expo-linking
import * as Linking from 'expo-linking';
적용
아래 처럼 함수에 작성해서 함수를 버튼에 연결해주면 된다.
const link = () => {
Linking.openURL('https://spartacodingclub.kr')
}
...
<TouchableOpacity style={styles.button} onPress={()=>{link()}}><Text>외부 링크</Text></TouchableOpacity>