지난 시간까지는 기획과 디자인을 발전시켰다면 이제 개발로 돌아와 리액트 네이티브로 실제 작업을 진행하기 위한 준비 과정을 진행해보려 한다.
먼저 React Native에 타입스크립트가 적용된 기본 템플릿을 내려받는다.
npx react-native init spoon --template react-native-template-typescript
주의점
필요한 설정들도 함께 완료되어야 마지막까지 에러가 발생하지 않는다. 예를 들어 다음과 같이 루비가 아예 없거나 버전이 맞지 않으면 에러가 발생한다.
error Your Ruby version is 2.6.10, but your Gemfile specified 2.7.5
업데이트는 다음과 같이 rvm을 이용해 진행할 수 있다.
rvm install 2.7.5
rvm use 2.7.5
안드로이드 스튜디오와 Xcode 또한 미리 설치해야 한다. 안드로이드 스튜디오는 추가로 JDK 다운로드 및 환경변수 설정이 필요하다.
React Native adb reverse ENOENT
export PATH=/Users/<your_computer_name>/Library/Android/sdk/platform-tools:$PATH
export ANDROID_HOME="/Users/<yourcomputername>/Library/Android/sdk"
export PATH=$ANDROID_HOME/emulator:$PATH
export PATH=$ANDROID_HOME/tools:$PATH
export PATH=$ANDROID_HOME/tools/bin:$PATH
export PATH=$ANDROID_HOME/platform-tools:$PATH
yarn start에 성공하면 메트로가 실행되는 것을 볼 수 있다.
yarn start로 메트로 서버를 실행시킨 상태에서 각각 yarn ios, yarn android를 통해 가상 기기를 실행시킬 수 있다. 보통 한 번에 실행되는 경우는 없으며 각 환경마다 세팅이 필요하다.
주요 발생 문제들
다음 명령어를 통해 eslint를 추가한다.
npm init @eslint/config
다음 과정을 통해 절대경로로 import 할 수 있다.
babel-plugin-module-resolver 추가
yarn add --dev babel-plugin-module-resolver
babel config 수정
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: [
[
'module-resolver',
{
root: ['./src'],
extensions: ['.ts', '.tsx', '.jsx', '.js', '.json'],
alias: {
'@': './src/',
},
},
],
],
};
tsconfig 수정
{
"extends": "@tsconfig/react-native/tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"typeRoots": ["src/types"]
}
주의점
eslint 관련 설정을 추가해 주어야 에러가 발생하지 않는다.
yarn add -D eslint-plugin-import
yarn add -D eslint-import-resolver-babel-module
eslintrc
'babel-module': {} // 추가
참고 - How to Setup Path Alias in a React Native TypeScript App
네비게이션 구현을 위해 @react-navigation, @react-navigation/native-stack 등을 추가한다. 그 뒤 APP에 라우트 구조를 추가한다.
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Login" component={Login} />
</Stack.Navigator>
</NavigationContainer>
)
}
recoil이나 redux 등을 추가한 후 관련 세팅을 진행한다. 여기선 recoil을 사용할 예정이다.
yarn add recoil
그 뒤 최상단에 RecoilRoot를 추가한다.
import React from 'react';
import {RecoilRoot} from 'recoil';
import {NavigationContainer} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
const App = () => {
return (
<RecoilRoot>
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
initialRouteName="Home">
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Login" component={Login} />
</Stack.Navigator>
</NavigationContainer>
</RecoilRoot>
)
}
이제 모든 세팅이 완료되었다. 다음으로 폴더구조를 설정해보자.
├── App.tsx // 최상단
├── src
| ├── assets
| | └── imgs
| ├── features
| | ├── login
| | | ├── components
| | | ├── hooks
| | | └── utils
| | └── ...
| ├── recoil
| ├── types
| └── screens
| ├── Home.tsx
| └── Login.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
개인적으로 선호하는 방식은 다음과 같이 스크린을 제외하면 페이지나 컴포넌트 등을 따로 분리하지 않고 구현하려는 feature 안에서 구분하는 방법이다. 이렇게 진행하면 나중에 page 등이 비대해져서 찾지 못하는 사태를 방지할 수 있고 feature 별로 구분해서 협업하면 conflict 방지에도 유리하다. 그러나 비슷한 컴포넌트가 각각 다른 feature에 중복으로 작성될 가능성이 생기므로 주기적으로 확인하여 합쳐주는 작업을 진행해야 한다. 이렇게 합쳐진 공통 컴포넌트는 보통 상단에 shared 폴더에 모아두고 추후 storybook으로 관리하기 편하도록 데이터와 UI를 분리하여 view 부분만 재사용 하게 된다.
이제 디자인을 토대로 각 기능을 구현하면 된다. 그러나 하나의 기능을 100% 완성하고 다음으로 넘어가면 그사이에 완성된 기능에서 변경 사항이 생길 수 있다. 최대한 이러한 재작업을 방지하기 위해 일단 뼈대가 되는 컴포넌트들을 구성하고 디자인 디테일은 최대한 나중에 구현하는 방식으로 진행하였다.