어서와, 개발은 처음이지?

React Native + Redux + Jest + Typescript + React Navigation 맛보기 본문

React/React Native

React Native + Redux + Jest + Typescript + React Navigation 맛보기

오지고지리고알파고포켓몬고 2019. 9. 4. 03:47



이번 글에서는 React Navigation, Redux, Typescript을 사용할 수 있도록 React Native 개발 환경을 구축해보겠습니다.

완성된 코드는 github에서 확인하실 수 있습니다.(보일러 플레이트 작업중이라 약간 상이할 수 있습니다. 참고용으로 확인해주세요.)


- 차례

0. 들어가기 전에

1. 설치환경

2. 프로젝트 생성

3. react-navigation 환경 구축

4. typescript 환경 구축

5. typescript + jest

6. redux 환경 구축

7. 마치며


0. 들어가기 전에


얼마전에 if kakao dev 컨퍼런스에 다녀왔는데, 대다수 서비스의 front end 스택이 react로 이루어져 있는 것을 알 수 있었습니다.

특히 react와 vue 중 react를 높게 평가하시는 개발자님들이 많은 것을 보고, react가 정말 흥하고 있구나 생각했습니다. 


대세 스택인 만큼, react는 velopert님 같은 선구자를 필두로 typescript, redux, test driven development 등 관련 글을 쉽게 찾아볼 수 있지만, react native에 대한 글은 상대적으로 부족하다고 느껴집니다.


컨퍼런스에서 강연을 들으며 typescript, redux 등의 스택을 react native 환경에 도입해야겠다는 필요성을 느꼈고,

혹시 저와 같은 생각을 가진 다른 개발자 분들이 시행착오를 겪지 않기를 바라는 마음으로 이번 글을 기획하고 공부하였습니다.



1. 설치환경


- 개발도구 : vscode

- react-native : 0.60.5

- react-navigation : 3.11.1

- typescript : 3.6.2

- redux : 4.0.4

- react-redux : 7.1.1


atomic design, ducks 패턴 모방



2. 프로젝트 생성


우선 SimpleApp이라는 이름으로 프로젝트를 생성합니다.

같은 react-native 버전을 사용하고 싶으시면 --version react-native@0.60.5 옵션을 추가하시면 됩니다.

$ react-native init SimpleApp
$ cd SimpleApp


3. react-navigation 환경 구축


이어서 react-navigation을 설치합니다. 자세한 설치 방법은 공식 문서를 참조하시기 바랍니다.


다음으로 navigation이 동작하는지 확인하기 위해 샘플 코드를 작성합니다.

이 글에서는 Atomic Design 패턴을 모방하여 디렉터리를 구성해보겠습니다.


아래 명령어를 터미널에 입력하여 src/screens/index.js와 src/screens/HomeScreen/index.js 파일을 생성합니다.

$ mkdir -p ./src/screens/HomeScreen
$ touch ./src/screens/HomeScreen/index.js
$ touch ./src/screens/index.js 


명령어 사용에 에러가 있으시면 개발 도구를 이용하여 아래 구조가 생성되도록 해주세요.

.
├── App.js
├── __tests__
├── android
├── app.json
├── babel.config.js
├── index.js
├── ios
├── metro.config.js
├── node_modules
├── package-lock.json
├── package.json
├── src
│    └── screens
│    ├── HomeScreen
│    │   └── index.js
│    └── index.js
└── yarn.lock


다음으로 코드를 입력해보겠습니다.


우선 홈 화면을 만들겠습니다.

src/screens/HomeScreen/index.js 파일을 아래와 같이 작성해주세요.

// src/screens/HomeScreen/index.js

import React, { Component } from 'react';
import {
    View,
    Text,
    TextInput
} from 'react-native';

export default class HomeScreen extends Component{
    render(){
        return (
            <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
                <Text>홈 화면</Text>
            </View>
        )
    }
}

<src/screens/HomeScreen/index.js>


다음은 생성한 화면을 기준으로 navigator를 생성하고 앱 컨테이너를 만들겠습니다.

 src/screens/index.js 파일을 아래와 같이 작성해주세요.

// src/screens/index.js

import React from 'react';
import { createStackNavigator, createBottomTabNavigator, createAppContainer } from 'react-navigation';

import HomeScreen from './HomeScreen';

const RootStack = createStackNavigator(
    {
        HomeScreen
    },
    {
        defaultNavigationOptions: ({navigation}) => ({
            title: 'Home',
        }),
        initialRouteName: 'HomeScreen'
    }
);

export default createAppContainer(RootStack);

<src/screens/index.js>


이제 App.js를 수정하여 react-navigation이 잘 작동하는지 확인해봅니다.

프로젝트 루트 경로의 App.js를 아래와 같이 수정해주세요.

import React from 'react';
import AppStack from './src/screens';


const App = () => {
  return (
    <AppStack />
  );
};

export default App;

<App.js>


수정이 완료되었다면 아래와 같은 화면을 보실 수 있습니다.

4. typescript 환경 구축


이어서 typescript 환경을 구축하겠습니다. react-native 공식 블로그를 참조하여 작성했습니다.


프로젝트 루트 경로에서 아래 명령을 차례대로 입력합니다.

$ npm install --save-dev typescript
$ npm install --save-dev react-native-typescript-transformer
$ npx tsc --init --pretty --jsx react
$ touch rn-cli.config.js
$ npm install --save-dev @types/react @types/react-native


정상적으로 설치를 마쳤다면 tsconfig.json 파일을 볼 수 있습니다.

이 파일을 열고, allowSyntheticDefaultImports 주석을 해제해줍니다.

{
  "compilerOptions": {
    // 생략
    "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    // 생략
  }
}

<tsconfig.json 일부>


다음은 rn-cli.config.js 파일을 아래와 같이 작성해주세요.(touch 명령으로 생성이 안된다면 직접 만들어주시면 됩니다.)

module.exports = {
    getTransformModulePath() {
      return require.resolve('react-native-typescript-transformer');
    },
    getSourceExts() {
      return ['ts', 'tsx'];
    },
};

<rn-cli.config.js>


(사실 위 파일은 react-native 0.58 이상 버전에서는 작성할 필요가 없다고 합니다.)


이제 typescript가 잘 동작하는지 보기위해 src/screen/HomeScreen/index.js 파일의 확장자를 .tsx로 변경하고 간단한 typescript 코드를 작성해보겠습니다.

HomeScreen/index를 아래와 같이 수정합니다.

// src/screens/HomeScreen/index.js -> tsx

import React, { Component } from 'react';
import {
    View,
    Text,
    TextInput
} from 'react-native';

interface IProps {

}

interface IState {
    text: string
}

export default class HomeScreen extends Component<IProps, IState>{
    constructor(props: IProps){
        super(props);
        this.state = {
            text: 100
        }
    }

    render(){
        return (
            <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
                <Text>{this.state.text}</Text>
            </View>
        )
    }
}

<src/screens/HomeScreen/index.tsx>


정상적으로 적용이 되었다면 vscode에서 아래와 같은 타입 에러를 확인하실 수 있습니다.



에러를 확인하셨다면 this.state의 text를 ""로 바꿔줍니다.




5. typescript + jest


이번에는 test 모듈인 jest에 typescript를 붙여보겠습니다.


우선 프로젝트 루트 경로에서 아래 명령어를 입력합니다.

$ npm install --save-dev ts-jest
$ npm install --save-dev @types/jest @types/react @types/react-native @types/react-test-renderer


다음은 package.json의 jest 프로퍼티에 아래 내용을 입력합니다.

  "jest": {
    "preset": "react-native",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js"
    ],
    "transform": {
      "^.+\\.(js)$": "<rootDir>/node_modules/babel-jest",
      "\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
    },
    "testRegex": "(/src/.*.(test|spec))\\.(ts|tsx|js)$",
    "testPathIgnorePatterns": [
      "\\.snap$",
      "<rootDir>/node_modules/"
    ],
    "cacheDirectory": ".jest/cache"
  }


이번 글에서는 src내의 test 코드만 테스트 하기위해 testRegex를 "(/src/.*.(test|spec))\\.(ts|tsx|js)$"로 변경하였습니다.

기본 권장사항은 "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$"입니다.


설정이 완료되었다면 jest가 정상적으로 동작하는지 테스트 코드로 사용할 src/screen/HomeScreen/index.test.ts 파일을 만들고 아래 내용을 입력합니다.

// src/screens/HomeScreen/index.test.ts

function sum(a: number, b: number): number{
  return a + b;
}

it('simple test', () => {
  expect(sum(10, "20")).toEqual(30);
});

<src/screen/HomeScreen/index.test.ts>


여기까지 잘 따라오셨다면, 이제 터미널에서 아래 명령어를 입력하면 테스팅이 되는 모습을 볼 수 있습니다.

$ npm test


테스팅 단계에서 타입 에러를 잡아내는 모습을 볼 수 있습니다.

sum 함수의 두번째 인자 "20"을 숫자로 바꿔주면 케이스를 통과하는 모습을 볼 수 있습니다.


6. redux 환경 구축


이번에는 redux 환경을 구축하겠습니다.

이 글에서는 ducks 패턴을 모방하여 코드를 작성해보겠습니다.


아래 명령어를 입력하여 redux를 설치합니다.

$ npm install --save redux react-redux
$ npm install --save-dev @types/redux @types/react-redux


아래 명령어를 입력하여 HomeScreen에서 사용할 store를 생성합니다.

$ mkdir -p ./src/stores
$ touch ./src/stores/index.ts
$ touch ./src/stores/HomeScreen.ts
$ touch ./src/stores/HomeScreen.test.ts


생성이 완료되었다면 아래와 같은 구조로 이루어집니다.

.
├── App.js
├── __tests__
├── android
├── app.json
├── babel.config.js
├── index.js
├── ios
├── metro.config.js
├── node_modules
├── package-lock.json
├── package.json
├── src
│    ├── screens
│    │   ├── HomeScreen
│    │   │   ├── index.test.ts
│    │   │   └── index.tsx
│    │   └── index.js
│    └── stores
│        ├── index.ts
│        ├── HomeScreen.test.ts
│        └── HomeScreen.ts
└── yarn.lock


우선 스토어 파일을 작성해봅니다.

src/stores/HomeScreen.ts 파일을 아래와 같이 작성합니다.

// 액션 타입 정의
const CHANGE_TEXT = 'home/CHANGE_TEXT';

interface changeText {
    type: typeof CHANGE_TEXT
    text: string
}

type ActionTypes = 
    | changeText

// 액션 생섬함수 정의
const changeText = (text: string = ""): ActionTypes => ({ type: CHANGE_TEXT, text });

export const actionCreators = {
    changeText
}

// 인터페이스
export interface IProps { // 컴포넌트 주입
    text: string
    changeText: Function
}

export interface IState{ 
    text: string
}

export const initialState: IState = {
    text: "",
}

// 리듀서
export default function HomeScreen(
    state = initialState, 
    action: ActionTypes | any
): IState {
    switch (action.type) {
        case CHANGE_TEXT:
            return {
                ...state,
                text: action.text,
            }; 
        default:
            return state;
    }
}

<src/stores/HomeScreen.ts>


간단하게 설명하자면 TextInput에서 입력받은 텍스트를 state로 사용하기 위한 역할을 수행하는 store입니다.

지금은 예제지만 redux-actions 등을 사용하여 더욱 생산성 있게 개발하실 수 있겠습니다.




컴포넌트에 붙이기 전에 test 케이스를 먼저 만들어보겠습니다.


src/stores/HomeScreen.test.ts 파일을 아래와 같이 작성합니다.

import HomeReducer, { initialState, actionCreators } from './HomeScreen';

describe('Store - HomeScreen',() => {
    describe('actions', () => {
        it('check create actions', () => {
            const expectedActions =[
                { type: 'home/CHANGE_TEXT', text: "" },
            ];
            const actions = [
                actionCreators.changeText()
            ];
            expect(actions).toEqual(expectedActions);
        });
    });

    describe('reducer', () => {
        let state = HomeReducer(undefined, {});
        it('should return the initialState', () => {
            expect(state).toEqual(initialState);
        });
        
        it('change text to "qwer"', () => {
            state = HomeReducer(state, actionCreators.changeText("qwer"));
            expect(state).toHaveProperty('text', "qwer");
        });

        it('change text to "1000"', () => {
            state = HomeReducer(state, actionCreators.changeText("1000"));
            expect(state).toHaveProperty('text', "1000");
        });
    })
});

<src/stores/HomeScreen.test.ts>


위 테스트 코드에서는 액션 생성 함수, 리듀서의 동작을 테스트합니다.

테스트 케이스는 프로세스를 구성해놓고 개발 해나감에 따라 추가하도록 합니다.

테스트를 통과했으니 이제 컴포넌트에 스토어를 붙여보겠습니다.


우선 src/stores/index.ts에 컴바인 리듀서를 작성합니다.

import { combineReducers } from 'redux';
import HomeScreen from './HomeScreen';

export default combineReducers({
    HomeScreen,
});

<src/stores/index.ts>


다음으로 App.js를 아래와 같이 수정합니다.

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './src/stores';
import AppStack from './src/screens';

const store = createStore(rootReducer);

const App = () => {
  return (
    <Provider store={store}>
      <AppStack />
    </Provider>
  );
};

export default App;

<App.js>


스토어를 생성하고 navigator를 Provider로 감싸 store를 주입합니다.


마지막으로 src/screens/HomeScreen/index.tsx을 아래와 같이 수정합니다.

// src/screens/HomeScreen/index.js -> tsx

import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
    View,
    Text,
    TextInput
} from 'react-native';
import { IProps, actionCreators } from '../../stores/HomeScreen';

interface Props extends IProps {

}

interface States {
    
}

class HomeScreen extends Component<Props, States>{
    constructor(props: Props){
        super(props);
    }

    _changeText = (text: string) => {
        this.props.changeText(text);
    }

    render(){
        return (
            <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
                <Text>{this.props.text}
                <TextInput style={{borderWidth: 1}} onChangeText={this._changeText.bind(this)} />
            </View>
        )
    }
}

// props 로 넣어줄 스토어 상태값
const mapStateToProps = (state) => ({
    text: state.HomeScreen.text,
});
  
// props 로 넣어줄 액션 생성함수
const mapDispatchToProps = dispatch => ({
    changeText: (text: string) => dispatch(actionCreators.changeText(text)),
});

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(HomeScreen);

<src/screens/HomeScreen/index.tsx>


크게 확인하셔야 할 부분은 주입한 스토어 상태값을 받아오기 위해 this.props.text 부분과 dispatch 부분입니다.

이제 TextInput을 변경하면 HomeScreen 스토어의 text가 변경되고 이는 <Text> 컴포넌트에 즉각 반영되는 모습을 볼 수 있습니다.


7. 마치며


이 글은 계속해서 업데이트 해 나갈 예정이며, 변경사항은 github에서 계속 확인하실 수 있겠습니다.


각 라이브러리/프레임워크의 아름다운 사용법 보다는, 각 스택을 프로젝트에서 사용하는 방법에 대한 참고용으로 작성한 글이오니 개선이 필요한 부분은 댓글로 남겨주시면 반영하겠습니다.(주저말고 참교육을 주세요!)


오늘 밤샘 공부는 여기까지ㅜㅜ 피곤함에 앞이 잘 안보여서 글에 하자가 있을 수 있습니다.. 에러/오타도 제보해주세요 :)




4 Comments
댓글쓰기 폼