본문 바로가기

React Native 초기 설정

3월부터 착수했는데 이래저래 바빠서 정리하고 있지 못했던 React Native 맨땅에 헤딩한 경험을 기록한다.

* 기억하면 좋을 설정들만을 기록하여 아예 처음부터 설정하는 건 아님

 

 


목차

1. 디버깅 Setup

2. styled-components 설정

3. react-navigation 설정 (4.x)

4. Root import 설정


 

 

개발 환경

 

1. 디버깅 Setup

참고 URL. React Native Docs


feat. 안드로이드 개발자 고수 친구👍

 

1. Jdk 설치

 

2. 환경 변수에  맞게 설정 (나는 zsh 쓰고 있어서 ~/.zshrc)

export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools

 

3. .zshrc 재시작

> source ~/.zshrc

 

4. 안드로이드 스튜디오에서 Sdk Manager에서 맞는 SDK tools 설치

- 필자는 10, 11 설치

5. Keystore 생성 (Build > Generate Signed Bundle or APK)

- 정상적으로 설치되었다면 설정한 경로에 debug.keystore 파일이 생성됨

- 참고 URL ([Android] Android Studio Keystore 생성하기)

 

6. app/build.gradle에서 생성한 keystore 정보 입력

 

keystoreAlias 확인

keytool -v -list -keystore ${keystore 파일 위치}

build.gradle의 signingConfigs 설정

signingConfigs{
    debug {
        storeFile file(SIGNED_STORE_FILE)
        storePassword SIGNED_STORE_PASSWORD
        keyAlias SIGNED_KEY_ALIAS
        keyPassword SIGNED_KEY_PASSWORD
    }
}

여기서 SIGNED_STORE_FILE 등 변수 처리는 gradle.properties에서 함

SIGNED_STORE_FILE={keystore 파일명}
SIGNED_STORE_PASSWORD={설정한 비밀번호}
SIGNED_KEY_ALIAS={keyAlias}
SIGNED_KEY_PASSWORD={설정한 비밀번호}

- 참고 URL (Android Studio Gradle 에서 빌드시 Signed Key 설정하기)

 

7. USB 디버깅 연결한 adb devices 디바이스 확인

adb는 안드로이드 스튜디오가 설치되면서 자동으로 설치되었다.

> adb devices
List of devices attached
emulator-5554	device

8. 디버깅 시작

USB 연결한 실제 기기가 있으면 기기로 디버깅이 시작되고, 없으면 emulator로 실행된다.

npx react-native start
npx react-native run-android

 

디버깅이 완료되면 다음처럼 뜨는 화면을 볼 수 있다.

디버깅 실행 예시

 

9. 디버깅 실행 후 단말기를 흔들거나, 에뮬레이터의 경우 아래 커맨드를 입력하면

> adb shell input keyevent 82

다음처럼 목록이 뜨고 두번째 버튼 Debugging을 클릭한다.

(필자 화면에는 지금 이미 Debugging을 눌러서 Stop Debugging이라고 뜬다.)

 

이제 http://localhost:8081/debugger-ui/ 에 접속하여 크롬 콘솔로 에러를 볼 수 있다.

 

2. styled-components 설정

어떻게 스타일을 정의할까 논의하다 styled-components를 사용해보기로 하였다.

- 참고 URL (Styled-components ThemeProvider를 활용한 스타일 환경 구축)

 

먼저 디자이너에게 공통 스타일 요소 정의를 부탁해서 이를 theme.js로 모듈화했다.

const addPx = (size) => `${size}px`

const fontSizes = {
    xxs: addPx(10),
    xs: addPx(11),
    small: addPx(13),
    base: addPx(14),
    lg: addPx(16),
    xl: addPx(18),
    xxl: addPx(24),
    xxxl: addPx(32),
}

// ...중략...

const theme = {
    fontSizes,
    fonts,
    paddings,
    margins,
    interval,
    verticalInterval,
    fontWeight,
}

/**
 * @returns {ReturnType<theme>} theme
 */
export default theme

App.js에서 ThemeProvider의 theme property로 theme 객체를 정의한다.

필자는 향후 다크모드 개발을 위해 background-color, color등 요소는 lightTheme으로 나누었다.

const App = () => {
    const AppContainer = createAppContainer(AppNavigator)

    const light = {
        ...theme,
        ...lightTheme,
    }

    return (
        <ThemeProvider theme={light}>
            <RootProvider>
                <AppContainer />
            </RootProvider>
        </ThemeProvider>
    )
}

 

이제 컴포넌트에서 theme을 전달받아 사용할 수 있다.

import styled from 'styled-components'

const TextBox = styled.View`
    margin: ${({theme}) => `0 ${theme.margins.xxxxl}`};
`

const Example = () => {
    return (
        <TextBox></TextBox>
    )
}

 

위 예제에서 볼 수 있듯이 react-native의 코어 컴포넌트들을 styled의 props로 받아서 사용할 수 있다.

 

3. react-navigation 설정 (4.x)

react native의 Routing과 navigation을 위해 react-navigation 라이브러리를 사용한다.

현재는 4.x버전을 사용하였는데 5.x로 향후 migration 예정이다.

 

createSwitchNavigator로 switch할 navigatior 리스트를 설정한다. SwtichNavigator는 각 navigator들간의 이동이 있으면 routes를 초기화한다.

The purpose of SwitchNavigator is to only ever show one screen at a time. By default, it does not handle back actions and it resets routes to their default state when you switch away. This is the exact behavior that we want from the authentication flow.
const AppNavigator = createSwitchNavigator(
    {
        loading: Loading,
        auth: AuthNavigator,
        home: HomeNavigator,
    },
    {
        initialRouteName: 'loading',
    },
)

 

initialRouteName은 앱을 켜자마자 이동할 RouteName이다. 보통 스플래쉬 이미지를 설정한다.

후에 소개하겠지만 AuthNavigator와 HomeNavigator는 스크린의 스택을 쌓는 작업을 만드는 createStackNavigator로 정의한 인스턴스이다.

더보기

React Navigation을 스택(Stack)을 사용한다. 즉, 스크린의 이동이 곧 스택에 스크린을 push하는 작업이다.

후입선출(last in, first out)로 동작하는 스택의 특징대로 현재 스크린에서 뒤로가기하면 바로 이전 화면으로 이동한다.

즉, 스택에서 현재 스크린이 pop된다.

 

이제 위에서 만든 AppNavigator를 담을 AppContainer를 생성한다.

const AppContainer = createAppContainer(AppNavigator)

 

완성된 코드는 다음과 같다.

import React from 'react'
import {createAppContainer, createSwitchNavigator} from 'react-navigation'

const AppNavigator = createSwitchNavigator(
    {
        loading: Loading,
        auth: AuthNavigator,
        home: HomeNavigator,
    },
    {
        initialRouteName: 'loading',
    },
)

const App = () => {
    const AppContainer = createAppContainer(AppNavigator)

    const light = {
        ...theme,
        ...lightTheme,
    }

    return (
        <ThemeProvider theme={light}>
            <RootProvider>
                <AppContainer />
            </RootProvider>
        </ThemeProvider>
    )
}

 

이제 SwitchNavigator의 하위 navigator인 AuthNavigator는 StackNavigator로 스크린을 쌓는 작업을 하는 인스턴스로 만든다.

/* eslint-disable import/no-named-as-default-member */
/**
 * @flow strict-local
 */
import {createStackNavigator} from 'react-navigation-stack'

import Login from './Login'
import Signup from './Signup'
import EmailForm from './EmailForm'
import NameForm from './NameForm'
import Agreement from './Agreement'

const AuthNavigator = createStackNavigator(
    {
        login: {
            screen: Login,
            navigationOptions: () => ({
                header: null,
            }),
        },
        signUp: {
            screen: Signup,
            navigationOptions: () => ({
                header: null,
            }),
        },
    },
    {
        initialRouteName: 'login',
    },
)

export default AuthNavigator

 

각 navigator의 screen으로 준 컴포넌트는 props로 navigation을 사용할 수 있다.

const Login = ({navigation}) => {
    return <></>
}

  

이제 특정 시점에 화면을 이동하고 싶을 땐 navigator.navigate(`${RouteName}`)을 호출한다.

const Login = ({navigation}) => {

    const goMain = () => navigation.navigate('home')

    return (
        <Container>
            <FooterButton text="시작하기" onPress={openLoginLayer} />
        </Container>
    )
}

 

(중요) React-navigation Lifecycle

(참고) React-navigation LifeCycle


react-navigation은 React.js의 라이프사이클을 따르지 않는다.

screen이 이동되었다고 해서 이전 컴포넌트를 unmount하지 않는다.

 

5.x 버전에서는 useFocusEffect 훅을 제공하는 등 함수형 컴포넌트 작성에 더욱 적합한 방법을 제공하나 필자는 현재 4.x 버전을 사용하고 있어 navigation.addListener를 사용한다.

useEffect(() => {
    const willBlurSubscription = navigation.addListener('willBlur', () => {
        closeFn()
    })
    return () => willBlurSubscription.remove()
}, [])

 

지금은 기본적인 기능들만 사용하고 있지만 Docs를 읽어보니 더 많은 기능을 제공하는 듯해서 5.x로 migragion할 때 improve해야겠다.

 

4. Root import 설정

현재 필자가 사용하는 directory 구조는 다음이다.

screens/는 컴포넌트 설계에 따라 더 깊은 directory 구조를 가진다.

상대경로로 import한 파일들의 경로를 나타내면 ../../../처럼 작성해야 하여 파일의 정확한 경로를 파악하기 갈수록 어려워진다.

 

따라서 alias 설정을 통해 root import를 설정하였다.

 

1. babel-plugin-root-import를 install한다.

> npm i --save-dev babel-plugin-root-import

2. babel.config.js(혹은 .babelrc)의 plugins에 babel-plugin-root-import를 추가한다.

 

그리고, 원하는 prefix와 prefix가 가리키는 경로를 작성해준다.

{
    rootPathPrefix: '$',
    rootPathSuffix: 'app',
}

 

필자가 설정한 alias는 다음이다.

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    plugins: [
        'babel-plugin-styled-components',
        [
            'babel-plugin-root-import',
            {
                paths: [
                    {
                        rootPathPrefix: '$',
                        rootPathSuffix: 'app',
                    },
                    {
                        rootPathPrefix: '$context',
                        rootPathSuffix: 'app/context/index.js',
                    },
                    {
                        rootPathPrefix: '$utils',
                        rootPathSuffix: 'app/utils',
                    },
                    {
                        rootPathPrefix: '$screens',
                        rootPathSuffix: 'app/screens',
                    },
                    {
                        rootPathPrefix: '$stores',
                        rootPathSuffix: 'app/stores',
                    },
                ],
            },
        ],
    ],
}

 

3. 그런데 이전에 설정한 eslint에서는 alias 설정이 반영이 되지 않아서 Error로 표시한다. 따라서 eslint용 root-import 설정또한 추가해준다.

> npm i --save-dev eslint-import-resolver-root-import

eslintrc.js의 settings property로 설정한다.

settings: {
    'import/resolver': {
        'root-import': {
            rootPathPrefix: '$',
            rootPathSuffix: 'app',
            extensions: ['.js', '.android.js', '.ios.js'],
        },
    },
},

 

4. vscode또한 alias를 인식할 수 있도록 jsconfig.json의 path 설정을 한다.

Typescript를 사용하는 경우, tsconfig.json에 설정한다.

{
    "compilerOptions": {
        "baseUrl": "app",
        "paths": {
            "$/*": ["*"],
            "$context": ["context/index.js"],
            "$utils/": ["utils/"],
            "$screens/*": ["screens/*"],
            "$stores/*": ["stores/*"]
        }
    },
    "exclude": ["node_modules"]
}

 

이제 파일에서 절대경로로 사용할 수 있다.

 

주의 1. plugins[1] may only be a two-tuple or three-tuple 이슈

 

처음에 alias 설정이 몇 개 없었을 때에 babel.config.js의 plugins을 이렇게 작성했었다.

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    plugins: [
        'babel-plugin-styled-components',
        [
            'babel-plugin-root-import',
            {
            	rootPathPrefix: '$',
            	rootPathSuffix: 'app',
            },
            {
            	rootPathPrefix: '$context',
            	rootPathSuffix: 'app/context/index.js',
            },
        ],
    ],
}

 

그런데 어느순간 추가하다가 plugins[1] may only be a two-tuple or three-tuple 디버깅 에러가 발생했다.

(아마 이전부터 에러였을텐데 캐싱되어서 뒤늦게 로그가 뜬 거 같다.)

 

solution을 검색하다가 비슷한 오류를 겪은 글에서 options을 배열로 설정해야 한다는 글을 보았다.

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    plugins: [
        'babel-plugin-styled-components',
        [
            'babel-plugin-root-import', 
            [
                {
                    rootPathPrefix: '$',
                    rootPathSuffix: 'app',
                },
                {
                    rootPathPrefix: '$context',
                    rootPathSuffix: 'app/context/index.js',
                },
            ]
        ],
    ],
}

 

위 오류는 해결된듯했으나 두번째 오류를 보았다 -_-;

 

주의 2. plugins[1][1] must be an object, false, or undefined 이슈

 

위에서 option으로 설정한 배열이 object거나 false, undefined여야 한다는 에러를 받으면 배열이 아닌 paths를 property로 하는 Object로 설정해야 한다.

 

참고 URL. Error: .plugins[0][1] must be an object, false, or undefined (Multiple custom prefixes)

module.exports = {
    presets: ['module:metro-react-native-babel-preset'],
    plugins: [
        'babel-plugin-styled-components',
        [
            'babel-plugin-root-import',
            {
                paths: [
                    {
                        rootPathPrefix: '$',
                        rootPathSuffix: 'app',
                    },
                    {
                        rootPathPrefix: '$context',
                        rootPathSuffix: 'app/context/index.js',
                    },
                    {
                        rootPathPrefix: '$utils',
                        rootPathSuffix: 'app/utils',
                    },
                    {
                        rootPathPrefix: '$screens',
                        rootPathSuffix: 'app/screens',
                    },
                    {
                        rootPathPrefix: '$stores',
                        rootPathSuffix: 'app/stores',
                    },
                ],
            },
        ],
    ],
}

 


전체 소스코드 저장소

ClimbFight/climbfighting-mobile


TODO

1. theme.js 요소가 자동입력이 안되어서 불편함
2. react-navigation 5.x migration
3. 블로그에 회원가입 양식 구현로직 정리하면서 리팩터링