RN

RN 공부

fullfish 2023. 5. 21. 23:30

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