코딩배우기

React Native React Navigation Redux Reduxpersist with Typescript (+Jest)

탁이 2021. 9. 20. 11:18

보일러 플레이트를 사용하면 좋겠지만, 리덕스에 익숙하기 때문에 정리하는 의미에서 아래 순서대로 설치를 해보도록 하겠습니다.

1. React Native (0.65) with Typescript

RN 템플렛을 사용할 경우

npx react-native init MyApp --template react-native-template-typescript

expo bare work flow 를 사용할 경우

expo init --template expo-template-bare-typescript --name MyApp

2. React Navigation

설치

yarn add @react-navigation/native

디펜던시 설치

yarn add react-native-screens react-native-safe-area-context

ios경우
npx pod-install ios

안드로이드 경우
실행되도록 android/app/src/main/java/<your package name>/MainActivity.java 에 아래 추가

import android.os.Bundle;

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(null);
}

스택내비게이션 만들기 위해 라이브러리 설치

yarn add @react-navigation/native-stack

파라미터를 타입스크립트를 이용해서 전달하는 스크린 코딩 Code screens with typed parameters to pass.

import * as React from 'react';
import { Button, View, Text } from 'react-native';

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NativeStackScreenProps } from '@react-navigation/native-stack';

type RootStackParamList = {
  Home: undefined;
  Count: { count: number };
};

const RootStack = createNativeStackNavigator<RootStackParamList>();

type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
type CountScreenProps = NativeStackScreenProps<RootStackParamList, 'Count'>;

function HomeScreen({ route, navigation }: HomeScreenProps) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Count"
        onPress={() => navigation.navigate('Count',
          {
            count: 7,
          })}
      />
    </View>
  );
}

function CountScreen({ route, navigation }: CountScreenProps) {
  const { count } = route.params;
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Count Screen</Text>
      <Text>카운터: {JSON.stringify(count)}</Text>
      <Button
        title="Go Home"
        onPress={() => navigation.navigate('Home')}
      />
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <RootStack.Navigator initialRouteName="Home">
        <RootStack.Screen name="Home" component={HomeScreen} />
        <RootStack.Screen name="Count" component={CountScreen} />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

export default App;

Redux 

install

 yarn add react-redux redux

스토어 폴더에 redux.ts 를 작성

import { Action, combineReducers, createStore } from "redux";

//액션 타입
export const ActionTypes = {
    increment: 'INCREMENT',
    decrement: 'DECREMENT',
} as const

//스테이트 타입
export type Count = {
    value: number
}

interface incrementAction extends Action {
    type: typeof ActionTypes.increment,
}

interface decrementAction extends Action {
    type: typeof ActionTypes.decrement,
}

export type CountActionTypes = incrementAction | decrementAction

//액션 크리에이터 작성 - 실제로 액션을 리턴하는 함수
export const increment = (): CountActionTypes => {
    return {
        type: ActionTypes.increment
    }
}

export const decrement = (): CountActionTypes => {
    return {
        type: ActionTypes.decrement
    }
}

const initialState: Count = {
    value: 0,
}

//리듀서
export const CountReducer = (state = initialState, action: CountActionTypes): Count => {
    switch (action.type) {
        case ActionTypes.increment:
            return { ...state, value: state.value + 1 }
        case ActionTypes.decrement:
            return { ...state, value: state.value - 1 }
    }
    return state;
}

const RootReducer = combineReducers({
    count: CountReducer,
})

export type RootState = ReturnType<typeof RootReducer>

const store = createStore(RootReducer)
export default store

index.js 에 App 을 프로바이더로 감싸기

import { AppRegistry } from 'react-native';
import React from 'react';

import App from './App';
import { name as appName } from './app.json';

import { Provider } from 'react-redux';
import store from './store/redux';

const ReduxApp = () => (
    <Provider store={store}>
        <App />
    </Provider>
)

AppRegistry.registerComponent(appName, () => ReduxApp);

App.tsx 화일에 추가하여 리덕스 스테이트 변경해 보기 useDispatch 덕분에 많이 편해졌다.

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * Generated with the TypeScript template
 * https://github.com/react-native-community/react-native-template-typescript
 *
 * @format
 */

import * as React from 'react';
import { Button, View, Text } from 'react-native';

import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NativeStackScreenProps } from '@react-navigation/native-stack';

import { useDispatch, useSelector } from 'react-redux';
import store, { decrement, increment, RootState } from './store/redux';

type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Count: undefined;
};

const RootStack = createNativeStackNavigator<RootStackParamList>();

type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
type ProfileScreenProps = NativeStackScreenProps<RootStackParamList, 'Profile'>;
type CountScreenProps = NativeStackScreenProps<RootStackParamList, 'Count'>;

function HomeScreen({ route, navigation }: HomeScreenProps) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
      <Text>
        <Button
          title="Go to Profile"
          onPress={() => navigation.navigate('Profile',
            {
              userId: '나야나',
            })}
        />
        <Button
          title="Go to Count"
          onPress={() => navigation.navigate('Count')}
        />
      </Text>
    </View>
  );
}

function ProfileScreen({ route, navigation }: ProfileScreenProps) {
  const { userId } = route.params;
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Profile Screen</Text>
      <Text>userId: {JSON.stringify(userId)}</Text>
      <Button
        title="Go Home"
        onPress={() => navigation.navigate('Home')}
      />
    </View>
  );
}

function CountScreen({ route, navigation }: CountScreenProps) {

  const countState = useSelector((state: RootState) => state.count)
  const dispatch = useDispatch()

  const OnIncrement = () => {
    dispatch(increment())
  }

  const OnDecrement = () => {
    dispatch(decrement())
  }
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Count Screen</Text>
      <Text>카운터2: {countState.value}</Text>
      <Text>
        <Button
          title="위로"
          onPress={OnIncrement}
        />
        <Button
          title="아래로"
          onPress={OnDecrement}
        />
      </Text>
      <Button
        title="Go Home"
        onPress={() => navigation.navigate('Home')}
      />
      <Text>{JSON.stringify(store.getState())}</Text>
    </View>
  );
}

const Stack = createNativeStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <RootStack.Navigator initialRouteName="Home">
        <RootStack.Screen name="Home" component={HomeScreen} />
        <RootStack.Screen name="Profile" component={ProfileScreen} />
        <RootStack.Screen name="Count" component={CountScreen} />
      </RootStack.Navigator>
    </NavigationContainer>
  );
}

export default App;

redux-persist 인스톨

먼저 리덕스의 스테이트를 저장하기 위해서는 redux-persist 라이브러리가 필요합니다.

yarn add redux-persist

AsyncStorage 인스톨

이번에 지속데이터를 저장할 공간으로 사용할 AsyncStorage 를 인스톨 합니다. 6.0 이전까지는 포함되어 있었으나, 6.0 이후 버전을 사용할 때는 커뮤니티 판을 사용.

yarn add @react-native-community/async-storage 

*아이폰의 경우 ios 폴더에서 npx pod-install ios 을 실행해 줍니다.

index.js화일에서 아래와 같이 적용합니다.

/**
 * @format
 */

import { AppRegistry } from 'react-native';
import React from 'react';
import App from './App';
import { name as appName } from './app.json';
import { Provider } from 'react-redux';
import { store } from './store/redux';

import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";

const persistor = persistStore(store);

const ReduxApp = () => (
    <Provider store={store}>
        <PersistGate loading={null} persistor={persistor}>
            <App />
        </PersistGate>
    </Provider>
)

AppRegistry.registerComponent(appName, () => ReduxApp);

 

redux.ts 

import { Action, combineReducers, createStore } from "redux";
import AsyncStorage from '@react-native-community/async-storage';
import { persistStore, persistReducer } from "redux-persist";


//액션 타입
export const ActionTypes = {
    increment: 'INCREMENT',
    decrement: 'DECREMENT',
} as const

//스테이트 타입
export type Count = {
    value: number
}

interface incrementAction extends Action {
    type: typeof ActionTypes.increment,
}

interface decrementAction extends Action {
    type: typeof ActionTypes.decrement,
}

export type CountActionTypes = incrementAction | decrementAction

//액션 크리에이터 작성 - 실제로 액션을 리턴하는 함수
export const increment = (): CountActionTypes => {
    return {
        type: ActionTypes.increment
    }
}

export const decrement = (): CountActionTypes => {
    return {
        type: ActionTypes.decrement
    }
}

const initialState: Count = {
    value: 0,
}

//리듀서
export const CountReducer = (state = initialState, action: CountActionTypes): Count => {
    switch (action.type) {
        case ActionTypes.increment:
            return { ...state, value: state.value + 1 }
        case ActionTypes.decrement:
            return { ...state, value: state.value - 1 }
    }
    return state;
}

const RootReducer = combineReducers({
    count: CountReducer,
})

export type RootState = ReturnType<typeof RootReducer>

// const store = createStore(RootReducer)
// export default store

const persistConfig = {
    key: "root",
    storage: AsyncStorage,
};

const persistedReducer = persistReducer(persistConfig, RootReducer);

// store.js
export const store = createStore(
    persistedReducer,
    // composeWithDevTools()
);

// // store.js
// export const store = createStore(
//     reducers,


// )
export const persistor = persistStore(store);