https://github.com/ZeroCho/food-delivery-app/blob/master/README.md
^태그
View : div랑 비슷
Text : span이랑 비슷
하지만 <View>안녕</View> 안됨 문자열은 Text안에 있어야함
SafeAreaView : 노치같은거 때문에 위쪽 이거로주고 죽은공간으로 만들어서 그 아래부터 화면처리
StatusBar : 배터리잔량같은 상태창 아래부터 화면처리하기 위해서 react-native-status-bar-height 라이브러리로 높이 구함
ScrollView : 스크롤 만듦 근데 안에 많으면 성능문제있어서 FlatList씀
^StyleSheet
styleComponent보다 StyleSheet를 권장
const styles = StyleSheet.create({
inputWrapper: {
padding: 20,
},
})
이런식으로 씀
StyleSheet내부에서는 조건문을 못써서 조건문을 쓰고싶다면 어쩔 수 없이 위쪽에 인라인형태로 삼항연산자를 씀
그리고 위쪽에서
style={[styles.abc,{color:isDarkMode ? Colors.white : Colors.balck}]}
이런식으로 배열형태로 쓸 수 있는데 먼저꺼가 적용되고 뒤에꺼로 덮여씌워짐
배열대신에 StyleSheet.compose 메소드를 써도 됨
borderBottomWidth: StyleSheet.hairlineWidth // 숫자를 써도되지만 이것을 쓰면 눈에는 보이는 가장 얇은 선 생성
^리액트 네비게이션
리액트 네비게이션을 쓸땐
import {NavigationContainer} from '@react-navigation/native';
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{title: '제목'}}
/>
<Stack.Screen name="Details" component={DetailsScreen} />
{/*<Stack.Screen name="Details">*/}
{/* {props => <DetailsScreen {...props} />}*/}
{/*</Stack.Screen>*/}
</Stack.Navigator>
</NavigationContainer>
);
}
Stack.Screen은 원래 navigation과 route를 props로 필수로 넘겨야함 그래서 ...props로 스프레드연산자로 준거임
리액트네비게이션에서는 내부에 SafeAreaView가 포함되어있음
navigation.push로 쌓기 가능
navigation.goBack : 이전 페이지로
Tab.Screen하면 하단에 탭이 생김
Stack.Screen같은건 map이 로딩이 오래걸려서 계속 새로 로딩시키는것 보다 한번 로딩시켜놓고 다른거를 위에 쌓는 용도로 좋음
Tab.Group은 Tab.Screen을 하나로 묶는건데 children이 하나만 있어야한다고 에러뜨는 경우에 쓰면 됨
// 네비게이션을 프룹스드릴링을 안하기 위해서
const navigation = useNavigation<NavigationProp<LoggedInParamList>>();
// 이렇게 하기도 함
^flex(플렉스)
flex : 세로를 각자 비율로 나눔
justifyContent : '옵션'
옵션이 flex-start, center, flex-end가 있음. 가로가 아닌 세로에 대한 정렬
alignItems : 가로에 대한 정렬
style={{flexDirection : 'row'}}
를 주면 해당 스타일을 가지고 있는 태그의 하위 태그들이 가로 정렬된다
이렇게 row 정렬을 하면 하위의 justifyContent와 alignItems옵션의 가로 세로는 반대로 적용된다
^버튼
<Button> 태그가 있긴한데 버튼 태그대신에
<TouchableHighlight>, <TouchableOpacity>, <TouchableWithoutFeadback>, <TouchableNativeFeadback>, <Pressable>를 많이 씀
<Pressable>, <TouchableWithoutFeadback>이 안드로이드, ios에서 똑같이 나타나서 이거 쓰는거 추천
// Button 태그
<Button
title="Go to Details"
onPress={() => {
/* 1. Navigate to the Details route with params */
navigation.navigate('Details', {
itemId: 86,
otherParam: 'anything you want here',
});
}}
/>
// Pressable 태그
<Pressable
onPress={onClick}
style={{padding: 15, backgroundColor: 'red'}}>
<Text style={{color: 'white'}}>글자색 하얀색</Text>
</Pressable>
padding 같은거는 웹처럼 : 20 40 50 30 이런식으로 축약해서 못쓰고
paddingTop, paddingBottom처럼 다 따로 써야함. 그런데 paddingVertical, paddingHorizontal은 묶어서 쓸 수 있음
^useCallback
useCallback(()=>{},[])
useMemo 와 비슷
useMemo 는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용
[]인 deps에 함수 안에서 사용하는 상태나 props가 있다면 넣어줘야지 최신값을 참조할 수 있음
useCallback은 async를 쓸 수 있는데 useEffect는 기본적으로는 못씀
^TextInput
<TextInput
style={styles.textInput}
placeholder="이메일 입력해"
value={email}
onChangeText={onChangeEmail}
importantForAutofill="yes"
autoComplete="email"
textContentType="emailAddress"
keyboardType="email-address" // 키보드 형태가 이메일 쓰기 쉬운 형태
returnKeyType="next" // 키보드의 엔터버튼 아이콘 변경
onSubmitEditing={() => {
passwordRef.current?.focus();
}}
blurOnSubmit={false} // 키보드 내려가는거 방지
ref={emailRef}
/>
^DismissKeyboardView
import React from 'react';
import {
TouchableWithoutFeedback,
Keyboard,
StyleProp,
ViewStyle,
KeyboardAvoidingView,
Platform,
} from 'react-native';
const DismissKeyboardView: React.FC<{style: StyleProp<ViewStyle>}> = ({
children,
...props
}) => (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<KeyboardAvoidingView
{...props}
style={props.style}
behavior={Platform.OS === 'android' ? 'position' : 'padding'}>
{children}
</KeyboardAvoidingView>
</TouchableWithoutFeedback>
);
export default DismissKeyboardView;
Keyboard.dismiss : 공백 클릭시 키보드 내려가는거
accesible={false} : 시각장애인을 위한 스크린리더기가 버튼을 인지하는데 현재 이 버튼은 우리의 편의를 위한 버튼이지 기능이 없는 버튼인데 스크린리더기가 인식을 해버리므로 false를 줌
KeyboardAvoidingView가 안좋아서 KeyboardAwareScrollView 라이브러리를 쓰는게 좋음
라이브러리 쓴 이후 코드
import React from 'react';
import {
TouchableWithoutFeedback,
Keyboard,
StyleProp,
ViewStyle,
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
const DismissKeyboardView: React.FC<{style: StyleProp<ViewStyle>}> = ({
children,
...props
}) => (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<KeyboardAwareScrollView {...props} style={props.style}>
{children}
</KeyboardAwareScrollView>
</TouchableWithoutFeedback>
);
export default DismissKeyboardView;
^Type
RN의 라이브러리가 옛날꺼라서 ts지원이 안된다면
npm i @types/<라이브러리이름>
을 해보고 찾지 못한다면 내가 만들어야함
types 폴더에 <라이브러리이름>.d.ts파일을 만들고
react-native-keyboard-aware-scrollview의 경우
declare module 'react-native-keyboard-aware-scrollview' {
import * as React from 'react';
import {Constructor, ViewProps} from 'react-native';
class KeyboardAwareScrollViewComponent extends React.Component<ViewProps> {}
const KeyboardAwareScrollViewBase: KeyboardAwareScrollViewComponent &
Constructor<any>;
class KeyboardAwareScrollView extends KeyboardAwareScrollViewComponent {}
export {KeyboardAwareScrollView};
}
이런식으로 만들어 주면 됨
^redux
slice들이 모여서 reducer이 되고 reducer이 모여서 store이 됨
store을 App에서 <Provider store={store}로 감싸서 연결
const isLoggedIng = useSelector((state: RootState) => state.user.email);
이런식으로 불러옴
useSelector는 Provider안에서만 쓸 수 있음 현재 위의 코드가 밖에 있으므로
Provider안쪽 코드를 따로 파일화해서 거기서 선언해야함
// store/index.ts
import {configureStore} from '@reduxjs/toolkit';
import {useDispatch} from 'react-redux';
import rootReducer from './reducer';
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => { // 미들웨어는 flipper 연결때문에
if (__DEV__) {
const createDebugger = require('redux-flipper').default;
return getDefaultMiddleware().concat(createDebugger());
}
return getDefaultMiddleware();
},
});
export default store;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
// store/reducer.ts
import {combineReducers} from 'redux';
import userSlice from '../slices/user';
const rootReducer = combineReducers({
user: userSlice.reducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
// slices/user.ts
import {createSlice} from '@reduxjs/toolkit';
const initialState = {
name: '',
email: '',
accessToken: '',
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser(state, action) {
state.email = action.payload.email;
state.name = action.payload.name;
state.accessToken = action.payload.accessToken;
},
},
extraReducers: builder => {},
});
export default userSlice;
// slices/order.ts
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
export interface Order {
orderId: string;
start: {
latitude: number;
longitude: number;
};
end: {
latitude: number;
longitude: number;
};
price: number;
}
interface InitialState {
orders: Order[];
deliveries: Order[];
}
const initialState: InitialState = {
orders: [],
deliveries: [],
};
const orderSlice = createSlice({
name: 'order',
initialState,
reducers: {
addOrder(state, action: PayloadAction<Order>) {
state.orders.push(action.payload);
},
acceptOrder(state, action: PayloadAction<string>) {
const index = state.orders.findIndex(v => v.orderId === action.payload);
if (index > -1) {
state.deliveries.push(state.orders[index]);
state.orders.splice(index, 1);
}
},
rejectOrder(state, action: PayloadAction<string>) {
const index = state.orders.findIndex(v => v.orderId === action.payload);
if (index > -1) {
state.orders.splice(index, 1);
}
const delivery = state.deliveries.findIndex(
v => v.orderId === action.payload,
);
if (delivery > -1) {
state.deliveries.splice(delivery, 1);
}
},
},
extraReducers: builder => {},
});
export default orderSlice;
^loading
사용자가 짧은 시간에 회원가입을 여러번 누를 수 있으므로 안전장치는 많을 수록 좋음
// 로딩이면 리턴
const onSubmit = useCallback(async () => {
if(loading) return
})
// 로딩이면 로딩뜨고 아니면 회원가입 글뜨게
<Pressable
style={
canGoNext
? StyleSheet.compose(styles.loginButton, styles.loginButtonActive)
: styles.loginButton
}
disabled={!canGoNext || loading}
onPress={onSubmit}>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.loginButtonText}>회원가입</Text>
)}
</Pressable>
// 이건 백단에서 post 요청에 토큰정보 넣어서 같은 토큰이 좀전에 요청있으면 안받아들이게끔
const response = await axios.post(
'/user',
{email, name, password},
{
headers: {
token: '유일한 토큰정보',
},
},
);
^reactNativeConfig
npm i react-native-config
local과 배포의 서버주소 분기를 하는법
1. env
`${process.env.NODE_ENV === 'production' ? '실서버 주소' : 'localhost:3105'}/user`
2. __DEV__
`${__DEV__ ? 'localhost:3105' : '실서버 주소'}/user`
3. react-native-config
`${Config.API_URL}/user` // .env파일에 API_URL=http://<내 ip주소>:80
// 나는 port번호를 3105가 아닌 80으로 하니까 됐음
안드로이드는 Config 쓰려면
// android/app/proguard-rules.pro
-keep class com.fooddeliveryapp.BuildConfig { *; } // 맨 아래에 추가
// android/app/build.gradle
// 맨 위에 추가
apply plugin: "com.android.application"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
// defaultConfig에 추가
defaultConfig {
...
resValue "string", "build_config_package", "com.fooddeliveryapp"
}
안드로이드는 http 쓰려면
// android/app/src/main/AndroidManifest.xml
android:usesCleartextTraffic="true" // 추가
^저장 공간들
redux: app꺼지면 날아감 대신 불러올때 제일 빠름, 민감한거 여기 넣어도 됨 끄면 날아가기 때문에
async-storage : 보안에 민감하지 않은 값
encrypted-storage : 보안에 민감한 값
react-native-config : 보안에 민감하지 않은 값, 개발 환경별로 나누기 가능(개발용, 배포용)
await EncryptedStorage.setItem('키', '값')
await EncryptedStorage.removeItem('키')
const 값 = await EncryptedStorage.getItem('키')
// asyncStorage도 마찬가지
^accessToken, refreshToken
accessToken은 짧으면 5분 길어야 1시간
따로 저장하는게 좋음
예를들어 엑세스토큰은 리덕스(웹에서는 메모리)
리프레쉬토큰은 encryptedStorage (웹에서는 로컬스토리지)
리프레쉬토큰이 탈취당한거 같으면 백에서 db에 저장된 리프레쉬토큰 삭제
^socket.io
socket은 실시간으로 서버랑 통신하는거라 배터리 소모 및 서버 부하가 큼
그래서 푸쉬알림이 더 좋긴한데 이건 연습용으로 쓰는거
npm i socket.io-client
socket.emit : 서버한테 데이터 보냄
socket.on : 서버한테서 데이터 받음
socket.off : 끄기
let socket: Socket | undefined;
const useSocket = (): [Socket | undefined, () => void] => {
const isLoggedIn = useSelector((state: RootState) => !!state.user.email);
const disconnect = useCallback(() => {
if (socket && !isLoggedIn) {
console.log(socket && !isLoggedIn, '웹소켓 연결을 해제합니다.');
socket.disconnect();
socket = undefined;
}
}, [isLoggedIn]);
if (!socket && isLoggedIn) {
console.log(!socket && isLoggedIn, '웹소켓 연결을 진행합니다.');
socket = SocketIOClient(`${Config.API_URL}`, {
transports: ['websocket'],
});
}
return [socket, disconnect];
};
^로그아웃
const onLogout = useCallback(async () => {
try {
//서버쪽 로그아웃
await axios.post(
`${Config.API_URL}/logout`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
);
Alert.alert('알림', '로그아웃 되었습니다.');
// 프론트쪽 로그아웃
dispatch(
userSlice.actions.setUser({
name: '',
email: '',
accessToken: '',
}),
);
await EncryptedStorage.removeItem('refreshToken');
} catch (error) {
const errorResponse = (error as AxiosError).response;
console.error(errorResponse);
}
}, [accessToken, dispatch]);
^엑세스 토큰을 이용한 로그인 유지
// 앱 실행 시 토큰 있으면 로그인하는 코드
useEffect(() => {
const getTokenAndRefresh = async () => {
try {
const token = await EncryptedStorage.getItem('refreshToken');
if (!token) {
return;
}
const response = await axios.post(
`${Config.API_URL}/refreshToken`,
{},
{
headers: {
authorization: `Bearer ${token}`,
},
},
);
dispatch(
userSlice.actions.setUser({
name: response.data.data.name,
email: response.data.data.email,
accessToken: response.data.data.accessToken,
}),
);
} catch (error) {
console.error(error);
if ((error as AxiosError).response?.data.code === 'expired') {
Alert.alert('알림', '다시 로그인 해주세요.');
}
} finally {
// TODO : 스플래시 스크린 없애기
}
};
getTokenAndRefresh();
}, [dispatch]);
^ts
// /slices/order.ts
export interface Order { // interface는 객체에 대한 타입 지정할때
orderId: string;
start: {
latitude: number;
longitude: number;
};
end: {
latitude: number;
longitude: number;
};
price: number;
}
interface InitialState { // 위에서 지정한 Order에 대한 타입을 넣어줌
orders: Order[];
deliveries: Order[];
}
const initialState: InitialState = { // 변수에다가 타입 지정할때는 : ~~ 하고 쓰면 됨
orders: [],
deliveries: [],
};
addOrder(state, action: PayloadAction<Order>) {
state.orders.push(action.payload);
},
// 원래 payload가 any 타입인데 PayloadAction<Order> 이것을 붙여 줌으로서
// Order타입을 넣어줌
useDispatch()만 써도되는데 ts의 경우 타입을 정해줘야해서
AppDispatch를 만들어 놓고
useDispatch<AppDispatch>() // 이렇게 쓰는데
// useDispatch를 쓸때마다 타입 붙여주는게 귀찮다면
export const useAppDispatch = () => useDispatch<AppDispatch>() // 이렇게 만들어 놓고 써도 됨
^FlatList
<View>는 스크롤이 안됨
<ScrollView>는 스크롤이 되지만 모두 랜더링을 함
<FlatList>
// ScrollView 예시
<ScrollView style={{backgroundColor: 'yellow'}}>
{orders.map(item => (
<View key={item.orderId} style={Styles.orderContainer}>
<Pressable onPress={toggleDetail} style={styles.info}>
<Text style={styles.eachInfo}>
{item.price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')}원}
</Text>
</Pressable>
</View>
))}
</ScrollView>
// FlatList 예시
<FlatList
data={orders}
keyExtractor={item => item.orderId}
renderItem={renderItem}
/>
^앱 이름 바꾸기
android > app > src > main > java > com > fooddeliveryapp을
com에 fullfish 폴더를 만들고 fooddeliveryapp폴더를 넣는다
src > debug > java > com > fooddeliveryapp도 마찬가지
그리고 com.fooddeliveryapp경로를 다
com.fullfish.fooddeliveryapp으로 바꾼다
^기타
str.replace(/\B(?=(\d{3})+(?!\d))/g, ',') 숫자 3자리마다 콤마찍는 정규표현식
for문처럼 반복대상이 되는 애들은 components로 분리하는게 좋음 (고차함수 안써도 되고 깔끔해짐)
'RN' 카테고리의 다른 글
riteSql 사용 (0) | 2023.12.04 |
---|---|
구글드라이브에 업로드 (0) | 2023.12.04 |
구글 로그인 (0) | 2023.12.01 |
RN & expo 배포 (0) | 2023.11.30 |
react-native-webview 웹뷰 흰페이지 나올때 해결법 (ssl ignore) (0) | 2023.11.30 |