🔨 작업 순서
0) react navigation과 AsysncStorage 라이브러리 추가
1) Main.tsx에 최상위 네비게이터 배치
2) Intro 화면 컴포넌트 제작
3) login 관련 3개 화면 컴포넌트 제작 [screen_login 폴더]
4) Main화면 관련 4개 컴포넌트 제작 [screen_main 폴더]
https://reactnavigation.org/docs/getting-started
1.2.1) 스택
1.2.2) 바텀 탭
https://react-native-async-storage.github.io/async-storage/docs/install/
//1. 최상위 스택 네비게이터의 화면 리스트 타입 지정
export type RootScreenList = {
Intro : undefined,
LoginNav : undefined,
MainNav : undefined, //undefined = putExtra
}
// 2. LoginNav Stack Navigator 화면 리스트 지정
export type LoginNavScreenList = {
Login : undefined,
SignUp : undefined,
ResetPassWd : undefined,
//메인 Nav화면으로 전환할 수 있도록 등록
MainNav : undefined,
}
// 앱 제작의 주요 작업 순서
// 0) react navigation과 AsysncStorage 라이브러리 추가
// 1) Main.tsx에 최상위 네비게이터 배치
// 2) Intro 화면 컴포넌트 제작
// 3) login 관련 3개 화면 컴포넌트 제작 [screen_login 폴더]
// 4) Main화면 관련 4개 컴포넌트 제작 [screen_main 폴더]
////////////////////////////////////////////////////////////////
//1.
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
// 2.
// 타입 스크립트에서는 네비게이터에 지정할 스크린 화면 리스트 타입을 미리 만들어야함
// types.tsx 파일을 만들어 관리 할 것, 필요한 곳에서 임포트 해서 사용하기
// 한 곳에 모아 보기 편리하게 하기 위해
import { RootScreenList } from "./types";
// 3. 앱 전체 화면들을 전환 할 수 있는 최 상위 stack navigtor 객체 생성 & 타입 지정
const rootStack = createStackNavigator<RootScreenList>()
// 4. 최상위 네비게이터에 의해 전환될 페이지(screen or navigator) 사용하기 위한 임포트
import Intro from "./Intro";
import LoginNav from "./navigators/LoginNav";
import MainNav from "./navigators/MainNav";
//네비게이션 콘테이너를 가진 최상위 root 컴포넌트 제작 - 앱의 시작 컴포넌트(index.js에서 설정)
export default function Main() : JSX.Element {
return (
<NavigationContainer>
<rootStack.Navigator screenOptions={{headerShown : false}}>
<rootStack.Screen name="Intro" component={Intro}></rootStack.Screen>
<rootStack.Screen name="LoginNav" component={LoginNav}></rootStack.Screen>
<rootStack.Screen name="MainNav" component={MainNav}></rootStack.Screen>
</rootStack.Navigator>
</NavigationContainer>
)
}
import React from "react";
import { View, Text, StyleSheet, Button, ActivityIndicator } from "react-native";
// ActivityIndicator - 화면지시자 , 빙빙 도는 모양, 프로그레스바
import AsyncStorage from '@react-native-async-storage/async-storage'
// 2. Stack Navigator의 screen으로 등록된 컴포넌트에서 전달받은 props의 타입 지정
import { StackScreenProps } from "@react-navigation/stack"
import { RootScreenList } from "./types";
//그냥 Props만 임포트 하면 안되고 이 프로프가 스택에 연결된 3 화면중 누구것인지 명시해야함!!!
type IntroProps = StackScreenProps<RootScreenList , 'Intro'>
//function component
// 1. 네비게이션에 등록하면 파라미터로 props {navigation, route} 객체 받음
export default function Intro(props : IntroProps):JSX.Element {
// 2. 로그인한 적이 있는지 검사한 후 결과에 따라 로그인 Nav 또는 메인 Nav로 이동
AsyncStorage.getItem('email').then((value)=>{
if(value) props.navigation.replace('MainNav') //replace는 현재 화면 스택에 안 쌓고 감
else props.navigation.replace('LoginNav')
})
return (
// 화면 전환 테스트 목록으로 보여질 임시 화면 제작
// <View style={style.root}>
// <Text >인트로</Text>
// <Button title="로그인 페이지 이동"
// onPress={()=>props.navigation.navigate("LoginNav")}></Button>
// <Button title="메인 페이지 이동"
// onPress={()=>props.navigation.navigate("MainNav")}></Button>
// </View>
// 3. AsyncStorage가 비동기 방식으로 읽어오는 사이에 잠깐 보여질 수 있는 로딩 화면
<View style={style.root}>
<ActivityIndicator></ActivityIndicator>
</View>
)
}
const style = StyleSheet.create({
root : {
padding : 16,
flex : 1,
justifyContent : 'center',
alignItems : 'center',
}
})
import React from "react";
//LoginNav의 Stack Navigator 객체 생성
import {createStackNavigator} from '@react-navigation/stack'
//이 로그인 네브에서 전환 할 화면 스크린들 type지정
import { LoginNavScreenList } from "../types";
const Stack = createStackNavigator<LoginNavScreenList>()
//네비게이터가 보여줄 화면 컴포넌트들 import
import SignUp from "../screen_login/SignUp";
import ResetPassWd from "../screen_login/ResetPassWd";
import Login from "../screen_login/Login";
export default function LoginNav() : JSX.Element{
return (
<Stack.Navigator
screenOptions={{headerShown : false,}}
>
<Stack.Screen name="Login" component={Login}></Stack.Screen>
<Stack.Screen name="SignUp" component={SignUp}></Stack.Screen>
<Stack.Screen name="ResetPassWd" component={ResetPassWd}></Stack.Screen>
</Stack.Navigator>
)
}
여러 페이지에서 공통으로 쓸 것을 미리 다른 파일로 만들어 어디서든 쓸 수 있도록 한다
같은 모양을 쓰지만 각각의 input마다 달라야하는 placeholder 같은 것을 나만의 프로퍼티로 만들어 넘겨 받는다 (props type 설정)
import React from 'react'
import {View, TextInput, StyleSheet} from 'react-native'
//props type [TextInput 컴포넌트의 각 속성들을 전달받기 위한 타입]
type Props = {
placeholder : string | undefined,
secureTextEntry? : boolean | undefined, //nullable로 만들어서 꼭 안써도 되게 함
onChangeText? : (text:string)=>void | undefined
}
export default function inputComponent(props : Props):JSX.Element{
return (
<View style={style.container}>
<TextInput
placeholder={props.placeholder} //컴포넌트를 사용하는 곳에서 힌트에 대한 property를 전달받아야한다
secureTextEntry = {props.secureTextEntry} //텍스트 시큐어 기능
onChangeText={props.onChangeText} //글씨 바뀔때마다 반응하는 넘
placeholderTextColor='#C3C2C8'
style={style.input}></TextInput>
</View>
)
}
const style= StyleSheet.create({
container:{
width:'100%',
height: 40,
paddingLeft:16,
paddingRight:16,
borderWidth:1,
borderColor:'#D3D3D3',
borderRadius: 4,
backgroundColor: '#FAFAFA',
marginTop:8,
marginBottom:8,
},
input:{
flex:1, //TextInput의 높이를 container높이 40에 맞게
color: '#292929',
},
})
import React from 'react'
import { View, Text, StyleSheet, Button } from 'react-native'
//공통으로 사용하는 컴포넌트 improt
import InputComponent from '../components/inputComponent'
// 네비게이션이터에서 네비게이션 받아오기 위한 작업
import { StackScreenProps } from '@react-navigation/stack'
import { LoginNavScreenList } from '../types' //어떤 화면인데?
type LoginProps= StackScreenProps<LoginNavScreenList,'Login'> //타입지정
export default function Login(props : LoginProps) : JSX.Element { //props 객체 {navigation, route}
//우선 테스트 목적 화면
// return (
// <View style={style.root}>
// <Text>로그인</Text>
// </View>
// )
return (
<View style={style.root}>
{/* 두 영역으로 구성 : 로그인 콘텐츠 영역, 아래쪽에 회사 or 앱 이름 표시 영역 */}
<View style={style.content}>
{/* 1.1 로고 */}
<Text style={style.logo}>Movie</Text>
{/* 1.2 이메일과 비밀번호 입력 박스 제작 */}
{/* 자주쓰는 컴포넌트를 따로 빼서 제작 - 재사용을 위해 */}
{/* TextInput은 로그인,회원가입, 비밀번호 재설정 화면에서도 모두 사용
사용빈도가 높아, 이를 일일이 스타일 하기 번거로우니 별도의 CustomCoponent로 제작하여 재사용
*/}
<InputComponent placeholder='이메일'></InputComponent>
<InputComponent placeholder='비밀번호' secureTextEntry={true}></InputComponent>
{/* 내가 만드는 속성 (placeholder) */}
{/* 1.3 비밀번호 재설정 */}
<Text style={style.resetPW} onPress={()=>props.navigation.navigate('ResetPassWd')}>비밀번호 재설정</Text>
{/* 1.4 로그인 버튼 만들기 */}
<View style={{width : '100%', marginBottom : 24}}>
<Button title='login'></Button>
</View>
{/* 1.5 회원가입 안내 글씨 */}
<Text style={style.singup}>계정이 없으신가요?
<Text style={style.singupLink} onPress={()=>props.navigation.navigate('SignUp')}> 가입하기</Text>
</Text>
</View>
<View style={style.footer}>
<Text style={style.footercopyright}>Movie info app by SJ</Text>
</View>
</View>
)
}
const style = StyleSheet.create({
root : {
flex :1,
backgroundColor : 'FEFFFF'
},
content : {
flex :1,
justifyContent : 'center',
alignItems : 'center',
padding : 32,
},
footer : {
borderTopWidth : 1,
borderTopColor : '#D3D3D3',
padding : 8,
},
logo : {
color :'#292929',
fontSize : 40,
fontWeight : 'bold',
marginBottom : 32,
},
resetPW : {
width : '100%',
color : '#3796EF',
textAlign : 'right',
marginTop : 8,
marginBottom : 16,
marginRight : 8,
},
singup : {
color : '#929292',
},
singupLink : {
color : '#3796EF',
},
footercopyright : {
textAlign : 'center',
padding : 8,
}
})
import React from 'react'
import { TouchableOpacity, Text, StyleSheet } from 'react-native'
// type 타입만드는 또 다른 방법
interface Props{
label:string,
selected?: boolean | undefined,
onPress?: ()=>void | undefined,
}
export default function tabComponent(props:Props):JSX.Element{
//탭의 선택여부에 따라 글씨색상 달라야 함
let color= props.selected? '#292929' : '#929292'
//탭의 선택여부에 따라 아래경계선의 색상도 변경
style.container.borderBottomColor= color
return (
// 탭 터치에 따른 처리 메소드를 지정하기위해
<TouchableOpacity style={style.container} onPress={props.onPress}>
{/* 탭에 보여질 글씨는 props로 전달받기 */}
<Text style={{color:color}}>{props.label}</Text>
</TouchableOpacity>
)
}
const style= StyleSheet.create({
container:{
flex:1, //탭이 놓여질때 다른 탭과의 flex값을 같게 하여 가로 너비를 균등하게 하기 위함
borderBottomWidth:2,
borderBottomColor:'#929292',
paddingBottom:8,
alignItems:'center',
justifyContent:'center',
}
})
- 화면 제작 중 자바스크립트 영역에서 if문 사용 불가하다
- 조건문 처리를 논리연산자 특징을 사용하여 한다
&& (논리 연산자) = 앞 조건이 false면 뒤 조건 안봄
& (비트 연산자) = 앞 조건이 false여도 뒤도 쳐다봄
논리연산자의 특징을 이용하여 처리 앞 조건 값이 true일 때만 뒤 실행문이 실행하도록 함
import React, {useState} from 'react'
import { View, Text, Button, StyleSheet } from 'react-native'
// [2]공통 사용 컴포넌트 임포트
import TabComponent from '../components/tabComponent'
import InputComponent from '../components/inputComponent'
// [1]네비게이션이터에서 네비게이션 받아오기 위한 작업
import { StackScreenProps } from '@react-navigation/stack'
import { LoginNavScreenList } from '../types' //어떤 화면인데?
type SignUpProps = StackScreenProps<LoginNavScreenList ,"SignUp"> //타입지정
export default function SignUp(props : SignUpProps) : JSX.Element { //props 객체 {navigation, route}
// //우선 테스트 목적 화면
// return (
// <View style={style.root}>
// <Text>SignUp</Text>
// </View>
// )
// tab 작업에서 사용할 state 변수 생성
const [tabs, setTabs] = useState(['전화번호', '이메일']) //탭 라벨을 string배열로 만들기
const [tabIndex, setTabIndex] = useState(0) //현재 선택된 탭 번호
// 완료 버튼 클릭 시 실행하는 메소드
const Signup = () => {
// 원래는 서버에 정보 보내는 작업 코드 작성///
//전송작업 끝나면 회원가입화면 종료 및 이전 로그인화면으로 이동
props.navigation.goBack()
}
return (
<View style={style.root}>
{/* Content, footer영역으로 구성 */}
<View style={style.content}>
{/* [3] 1.1 전화번호와 이메일 중 원하는 정보로 회원가입할 수 있도록 탭으로 구성 */}
<View style={style.tabContainer}>
{/* 탭 텀포넌트는 RN에 없음.. 그래서 Custom component로 제작 */}
{/* <TabComponent label='전화번호' selected={true}></TabComponent>
<TabComponent label='이메일' selected={false}></TabComponent> */}
{/* 탭 라벨의 개수만큼 탭 컴포넌트를 만들기 위해 map() 메소드 이용 */}
{
tabs.map( (value, index)=>{
return <TabComponent
label={value}
key={index}
selected = { index == tabIndex }
onPress= {()=>setTabIndex(index)}
></TabComponent>
} )
}
</View>
{/* 1.2 정보 입력 */}
<InputComponent placeholder={tabs[tabIndex]}></InputComponent>
{/* 1.3 이메일 탭일때는 비밀번호 입력 컴포넌트가 추가로 존재 해야됨 */}
{
//if(tabIndex == 1){} //JSX {}안에서는 if문법 불가능 연산자는 가능
// && (논리 연산자) = 앞 조건이 false면 뒤 조건 안봄
// & (비트 연산자) = 앞 조건이 false여도 뒤도 쳐다봄
//논리연산자의 특징을 이용하여 처리 앞 조건 값이 true일 때만 뒤 실행문이 실행됨
tabIndex == 1 && <InputComponent placeholder='비밀번호' secureTextEntry={true}></InputComponent>
}
{/* 1.4 전화번호 탭일 때는 [다음] 버튼 */}
{
tabIndex === 0 && <View style={{width:'100%', margin : 16,}}><Button title='다음' onPress={()=>setTabIndex(1)}></Button></View>
}
{/* 1.5 이메일 탭일 때 [완료] 버튼 */}
{
tabIndex === 1 && <View style={{width:'100%', margin : 16,}}><Button title='완료' onPress={()=>Signup()}></Button></View>
}
{/* 1.6 전화번호 탭일 때 입력에 대한 이유를 안내하는 메세지 표시 */}
{
tabIndex === 0 && <Text style={style.telMsg}>Movie APP의 업데이트 내용을 SMS로 수신할 수 있으며, 언제든지 수신 취소 가능합니다</Text>
}
</View>
<View style={style.footer}>
<Text style={style.footerMsg}>이미 계정이 있으신가요? <Text style={style.footerGoBack} onPress={()=>props.navigation.goBack()}>로그인</Text></Text>
</View>
</View>
)
}
const style = StyleSheet.create({
root : {
flex :1,
backgroundColor : '#FEFFFF'
},
content : {
flex :1,
alignItems : 'center',
padding : 32,
},
telMsg : {
textAlign : 'center',
fontSize : 12,
color : '#929292',
marginLeft :8,
marginRight : 8,
},
footer : {
borderTopWidth : 1,
borderTopColor : '#D3D3D3',
padding : 8,
},
footerMsg : {
textAlign : 'center',
color : '#929292',
},
footerGoBack : {
color : '#3796EF',
},
tabContainer : {
flexDirection : 'row',
marginBottom : 16,
}
})
import React, {useState} from 'react'
import { View, Text, Button, Alert, Image, StyleSheet } from 'react-native'
//공통사용 컴포넌트 임포트
import TabComponent from '../components/tabComponent'
import InputComponent from '../components/inputComponent'
// 네비게이션이터에서 네비게이션 받아오기 위한 작업
import { StackScreenProps } from '@react-navigation/stack'
import { LoginNavScreenList } from '../types' //어떤 화면인데?
type ResetPassWdProps = StackScreenProps<LoginNavScreenList ,"ResetPassWd"> //타입지정
export default function ResetPassWd(props : ResetPassWdProps) : JSX.Element { //props 객체 {navigation, route}
//우선 테스트 목적 화면
// return (
// <View style={style.root}>
// <Text>ResetPassWd</Text>
// </View>
// )
// 탭에 따른 화면 구성을 위한 state 변수들
const [tabs, setTabs] = useState<string[]>(['이메일', '전화번호']) //탭이름
const [tabIndex, setTabIndex] = useState<number>(0) //현재 탭 번호
//탭 선택에 따른 안내 메세지
const message = [
'이메일을 입력하면 임시 비밀번호를 보내드립니다',
'전화번호를 입력하면 임시 비밀번호를 보내드립니다',
]
//2. 비밀번호 재설정 화면
return (
<View style={style.root}>
{/* 1. 콘텐츠 영역 */}
<View style={style.content}>
{/* 1.1 자물쇠 모양의 이미지 표시 영역 */}
<View style={style.lockImgcontainer}>
<Image source={require('../Images/lock.png')}></Image>
</View>
{/* 1.2 타이틀 안내문구 표시 */}
<Text style={style.title}>로그인에 문제가 있나요</Text>
{/* 1.3 이메일 또는 전화번호 탭 선택에 대한 안내문구 */}
<Text style={style.msg}> {message[tabIndex]} </Text>
{/* 1.4 탭 만들기 - 공통 컴포넌트 사용하여 */}
<View style={style.tabContainer}>
{
tabs.map( (value, index) => {
return <TabComponent label={tabs[index]} selected={index==tabIndex} onPress={()=>setTabIndex(index)} key={index}></TabComponent>
})
}
</View>
{/* 1.5 정보 입력 박스 */}
<InputComponent placeholder={tabs[tabIndex]}></InputComponent>
{/* 1.6 전송 버튼 */}
<View style={{width:'100%', margin:16}}>
<Button title='전송' onPress={()=> Alert.alert('임시비밀 번호 발송', '로그인 후 정보수정을 통해 안전한 비밀번호로 변경 하세요') }></Button>
</View>
</View>
{/* 2. footer 영역 */}
<View style={style.footer}>
<Text style={style.footerMsg}>로그인 화면으로 돌아가기 <Text style={style.footerGoBack} onPress={()=>props.navigation.goBack()}>클릭</Text></Text>
</View>
</View>
)
}
const style = StyleSheet.create({
root : {
flex :1,
padding:16,
backgroundColor : '#FEFFFF'
},
content : {
flex : 1,
width : '100%',
alignItems : 'center',
padding : 16,
},
lockImgcontainer : {
padding : 24,
borderWidth : 2,
borderColor : '#292929',
borderRadius : 100,
},
title : {
fontSize : 16,
marginBottom : 16,
marginTop : 16,
},
msg : {
textAlign : 'center',
marginBottom : 14,
color : '#292929',
},
tabContainer : {
flexDirection : 'row',
marginBottom : 16,
},
footer : {
borderTopWidth : 1,
borderTopColor : '#D3D3D3',
padding : 8,
},
footerMsg : {
textAlign : 'center',
color : '#929292',
},
footerGoBack : {
color : '#3796EF',
},
})