https://github.com/ZeroCho/food-delivery-app/blob/master/README.md

^태그

View : div랑 비슷

Text : span이랑 비슷

  하지만 안녕 안됨 문자열은 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옵션의 가로 세로는 반대로 적용된다

^버튼

, , , , 를 많이 씀 , 이 안드로이드, ios에서 똑같이 나타나서 이거 쓰는거 추천 ```typescript // 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** ```typescript useCallback(()=>{},[]) ``` useMemo 와 비슷 useMemo 는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용 []인 deps에 함수 안에서 사용하는 상태나 props가 있다면 넣어줘야지 최신값을 참조할 수 있음 useCallback은 async를 쓸 수 있는데 useEffect는 기본적으로는 못씀 #### **^TextInput** ```typescript <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** ```typescript import React from 'react'; import { TouchableWithoutFeedback, Keyboard, StyleProp, ViewStyle, KeyboardAvoidingView, Platform, } from 'react-native'; const DismissKeyboardView: React.FC<{style: StyleProp}> = ({ 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 라이브러리를 쓰는게 좋음 라이브러리 쓴 이후 코드 ```typescript 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}> = ({ 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의 경우 ```typescript declare module 'react-native-keyboard-aware-scrollview' { import * as React from 'react'; import {Constructor, ViewProps} from 'react-native'; class KeyboardAwareScrollViewComponent extends React.Component {} const KeyboardAwareScrollViewBase: KeyboardAwareScrollViewComponent & Constructor; class KeyboardAwareScrollView extends KeyboardAwareScrollViewComponent {} export {KeyboardAwareScrollView}; } ``` 이런식으로 만들어 주면 됨 #### **^redux** slice들이 모여서 reducer이 되고 reducer이 모여서 store이 됨 store을 App에서 <Provider store={store}로 감싸서 연결 ```typescript const isLoggedIng = useSelector((state: RootState) => state.user.email); ``` 이런식으로 불러옴 useSelector는 Provider안에서만 쓸 수 있음  현재 위의 코드가 밖에 있으므로 Provider안쪽 코드를 따로 파일화해서 거기서 선언해야함 ```typescript // 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(); ``` ```typescript // store/reducer.ts import {combineReducers} from 'redux'; import userSlice from '../slices/user'; const rootReducer = combineReducers({ user: userSlice.reducer, }); export type RootState = ReturnType; export default rootReducer; ``` ```typescript // 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; ``` ```typescript // 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) { state.orders.push(action.payload); }, acceptOrder(state, action: PayloadAction) { 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) { 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** 사용자가 짧은 시간에 회원가입을 여러번 누를 수 있으므로 안전장치는 많을 수록 좋음 ```typescript // 로딩이면 리턴 const onSubmit = useCallback(async () => { if(loading) return }) ``` ```typescript // 로딩이면 로딩뜨고 아니면 회원가입 글뜨게 <Pressable style={ canGoNext ? StyleSheet.compose(styles.loginButton, styles.loginButtonActive) : styles.loginButton } disabled={!canGoNext || loading} onPress={onSubmit}> {loading ? ( ) : ( <Text style={styles.loginButtonText}>회원가입</Text> )} </Pressable> ``` ```typescript // 이건 백단에서 post 요청에 토큰정보 넣어서 같은 토큰이 좀전에 요청있으면 안받아들이게끔 const response = await axios.post( '/user', {email, name, password}, { headers: { token: '유일한 토큰정보', }, }, ); ``` #### **^reactNativeConfig** ```typescript npm i react-native-config ``` local과 배포의 서버주소 분기를 하는법 ```typescript 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 쓰려면 ```typescript // android/app/proguard-rules.pro -keep class com.fooddeliveryapp.BuildConfig { *; } // 맨 아래에 추가 ``` ```typescript // 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 쓰려면 ```typescript // android/app/src/main/AndroidManifest.xml android:usesCleartextTraffic="true" // 추가 ``` #### **^저장 공간들** redux: app꺼지면 날아감 대신 불러올때 제일 빠름, 민감한거 여기 넣어도 됨 끄면 날아가기 때문에 async-storage : 보안에 민감하지 않은 값 encrypted-storage : 보안에 민감한 값 react-native-config : 보안에 민감하지 않은 값, 개발 환경별로 나누기 가능(개발용, 배포용) ```typescript await EncryptedStorage.setItem('키', '값') await EncryptedStorage.removeItem('키') const 값 = await EncryptedStorage.getItem('키') // asyncStorage도 마찬가지 ``` #### **^accessToken, refreshToken** accessToken은 짧으면 5분 길어야 1시간 따로 저장하는게 좋음 예를들어 엑세스토큰은 리덕스(웹에서는 메모리) 리프레쉬토큰은 encryptedStorage (웹에서는 로컬스토리지) 리프레쉬토큰이 탈취당한거 같으면 백에서 db에 저장된 리프레쉬토큰 삭제 **^socket.io** socket은 실시간으로 서버랑 통신하는거라 배터리 소모 및 서버 부하가 큼 그래서 푸쉬알림이 더 좋긴한데 이건 연습용으로 쓰는거 ```typescript npm i socket.io-client ``` socket.emit : 서버한테 데이터 보냄 socket.on : 서버한테서 데이터 받음 socket.off : 끄기 ```typescript 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]; }; ``` **^로그아웃** ```typescript 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]); ``` **^엑세스 토큰을 이용한 로그인 유지** ```typescript // 앱 실행 시 토큰 있으면 로그인하는 코드 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** ```typescript // /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: [], }; ``` ```typescript addOrder(state, action: PayloadAction) { state.orders.push(action.payload); }, // 원래 payload가 any 타입인데 PayloadAction 이것을 붙여 줌으로서 // Order타입을 넣어줌 ``` ```typescript useDispatch()만 써도되는데 ts의 경우 타입을 정해줘야해서 AppDispatch를 만들어 놓고 useDispatch() // 이렇게 쓰는데 // useDispatch를 쓸때마다 타입 붙여주는게 귀찮다면 export const useAppDispatch = () => useDispatch() // 이렇게 만들어 놓고 써도 됨 ``` **^FlatList** 는 스크롤이 안됨 는 스크롤이 되지만 모두 랜더링을 함 ```typescript // 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> ``` ```typescript // FlatList 예시 <FlatList data={orders} keyExtractor={item => item.orderId} renderItem={renderItem} /> ``` **^앱 이름 바꾸기** ```typescript 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로 분리하는게 좋음 (고차함수 안써도 되고 깔끔해짐)

카테고리:

업데이트: