BEB 과정의 마지막 프로젝트. 가장 오랜 시간과 애정❣️을 담은 프로젝트였다. 많은 것을 배우기도 했고..
프로젝트 끝나고 일주일 정도 푹 쉬었는데, 기억이 더 날아가기 전에 어서 회고를 작성해보려고 한다.
✏️ 역할 분담
- Front-end, Design/UX (내가 담당한 🙋🏻♀️)
클라이언트 웹사이트- Back-end
서버, DB- Smart Contract
solana, Rust
유저의 이동량을 추적하여 검증하고 보상으로 RETRIP 자체 발행 토큰인 RTRP를 지급하는 M2E(Move To Earn) 어플리케이션이다. 엄밀히는 걷는 이동에만 보상이 지급된다. 유저의 GPS를 추적하여 이동하는 속도를 기준으로 판단하는데, 평균 보행 속도보다 너무 느리거나 빠른 구간에서는 보상이 지급되지 않는다.
🌏 H3
지구 상의 모든 면적을 일정한 크기의 육각형으로 구분지어 구역을 나누는 오픈 소스 API이다.
우리 프로젝트에서는 한 변의 길이가 약 9.5m인 육각형을 단위로 사용하였고, 각 육각형을 하나의 '허니콘'으로 부르기로 했다.
Map에서 Start 버튼을 누르면, Move mode로 진입한다. 하나의 허니콘에서 유효한 걷기 이동이 발생하면 1개의 RTRP 토큰을 보상받을 수 있다. 이 때 이동하는 평균 속도로 유효성을 검사하는데, 너무 느리거나 빠른 이동은 걷기 이동으로 볼 수 없어 보상을 지급하지 않는다.
Photo NFT는 하나의 게시글, Feed로 생각할 수 있다. 모든 게시글은 블록체인 상에 NFT로 발행된다.
Bucket을 구매한 사용자는 Map에서 카메라 버튼을 눌러 사진을 찍고 해당 사진을 NFT로 발행할 수 있다. 이 때 이전에 찍은 사진을 가져올 수 없고 실시간으로 찍은 사진만 민팅이 가능하다. 현재의 순간을 추억으로 기록한다는 취지를 살리기 위함이다.
사진을 찍으면 자동으로 사진을 찍은 위치 정보와 그 때의 날씨 정보도 자동으로 업로드 된다. 모든 NFT들은 Map에 핀처럼 해당 위치에 꼽히게 된다. 다른 유저들은 자유롭게 서로의 NFT를 확인할 수 있다. 어떠한 NFT가 가치가 있다고 판단될 경우 유저끼리 사고 팔 수 있다. (아직 사고 파는 기능은 구현되어 있지 않다.)
기존 솔라나 지갑의 니모닉 코드를 입력하여 연결하거나 새로운 니모닉 코드를 발급하고,
로그인시 사용할 6자리 비밀번호를 지정한다.
지갑 연결시 등록한 6자리 비밀번호만으로 앱을 열 수 있다.
RTRP 보상을 받기 위해서는 걷기 보상 단위인 '허니콘'을 담기 위한 버킷을 구매해야 한다.
버킷 가격은 0.1 Sol 이다.
Map : Move mode, Explore mode
지도 상에 보이는 NFT들의 상세 정보도 확인할 수 있다.
Bucket을 구매한 유저는 Map에서 Photo NFT minting이 가능하다.
다른 유저들이 발행한 NFT들을 피드 형식으로 볼 수 있으며,
각 NFT의 상세 정보도 확인할 수 있다.
- My NFTs - 내가 발행한/소유한 Photo NFT 목록 확인
- My Wallet - SOL/RTRP 잔액 확인
- My Txs - 보상 지급, NFT 민팅 등 내가 참여한 거래 내역 확인
(내가 담당한 부분들에 대해서만 기술함)
⌨️ Stacks
- React-native
- Expo
- redux tool-kit
- React Navigation
- Styled-components
- GraphQL, Apollo
- Figma
- iOS/Android Simulator
프로젝트1,2에서도 Figma를 사용하긴 했는데 이번에 앱을 개발하면서 좀 더 제대로(?) 사용해보게 되었다. 스마트폰 앱의 경우 화면이 작기 때문에 한 화면에 담을 수 있는 정보량이 많지 않아 최대한 각 페이지를 간결하게 제작하는 것이 중요했다.
또한 페이지 구성, 각 페이지 간의 연결과 흐름이 유저 입장에서 자연스러운지 UX까지 고려해야 했기 때문에 단순히 디자인을 그려보는 것 이상으로 신경쓸게 많았던 것 같다.
아직 피그마로 각 요소들을 냅다 그리고만 있는데, 좀 더 익숙해지면 적절하게 그룹화를 해가면서 체계적으로 사용해볼 수 있을 것 같다.
react-native app은 이번 프로젝트를 통해 처음 개발해보게 되었는데, 처음치고 매우 빡시게(?) 경험할 수 있었다. 기본적으로 react와 거의 흡사해서 적응이 어렵지는 않았는데, CSS가 내 마음처럼 구현되지 않는 경우가 많아 고생한 것 같다.
있을 것 같은데? 싶은 라이브러리들은 대부분 이미 존재해서 가져다 쓰기만 하면 됐기 때문에 개발이 크게 어렵지는 않았다. 가끔 docs 설명이 미흡한 라이브러리들이 있어 헤매기는 했지만 냅다 하나하나 처음부터 개발하는 것보다 확실히 개발이 간단하고 시간도 적게 들었다.
또한 react-native의 복잡한 빌드 과정 등을 대신해주고 앱 개발을 훨씬 빠르고 편하게 할 수 있게 해주는 expo를 사용했다. 이는 우리 앱을 실제 서비스로 배포할 필요가 없었기 때문에 가능했다. 실 서비스용으로 개발하여 앱스토어나 플레이스토어에 올릴 예정이라면 바닐라 react-native-cli
로 개발해야 한다. 또한 편리한 만큼 native 차원의 개발이 어려운 제약사항이 많다.
"dependencies": {
"@expo-google-fonts/montserrat": "^0.2.2",
"@expo-google-fonts/slackey": "^0.2.2",
"@expo/vector-icons": "^13.0.0",
"@expo/webpack-config": "~0.16.2",
"@react-native-community/slider": "^4.2.2",
"@react-native-picker/picker": "^2.4.1",
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "^6.6.2",
"expo": "~45.0.0",
"expo-app-loading": "^2.0.0",
"expo-asset": "^8.5.0",
"expo-camera": "~12.2.0",
"expo-font": "^10.1.0",
"expo-location": "~14.2.2",
"expo-media-library": "~14.1.0",
"expo-random": "^12.2.0",
"expo-secure-store": "~11.2.0",
"expo-splash-screen": "~0.15.1",
"expo-status-bar": "~1.3.0",
"expo-system-ui": "^1.2.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-native": "0.68.2",
"react-native-confirmation-code-input": "^1.0.4",
"react-native-crypto-js": "^1.0.0",
"react-native-gesture-handler": "^2.4.2",
"react-native-get-random-values": "^1.8.0",
"react-native-keycode": "^1.1.2",
"react-native-maps": "^0.31.1",
"react-native-mime-types": "^2.3.0",
"react-native-modal": "^13.0.1",
"react-native-pager-view": "^5.4.15",
"react-native-stopwatch-timer": "^0.0.21",
"react-native-tab-view": "^3.1.1",
"react-native-url-polyfill": "^1.3.0",
"react-native-web": "0.17.7",
"react-redux": "^8.0.2",
"styled-components": "^5.3.5",
},
},
이번 프로젝트를 진행하면서 react-native/expo 관련하여 설치한 라이브러리만 추려보아도 이정도이다. 이만큼 미리 만들어진 라이브러리들이 많기 때문에 그 중 적절한 것을 잘 골라서(npm에서 주간 다운로드수가 많은 라이브러리 위주로 선택한다) 사용하면 생각보다 더 쉽게 앱 개발을 할 수 있다.
React Navigation은 native 전용은 아니지만, native에서 페이지간 이동을 구현하기 위해서는 필수적으로 사용해야 한다. 개인적으로 docs가 정말 잘 정리되어 있다고 생각한다.
일반적으로 tab
이나 stack
을 가장 많이 사용하는데 이번 프로젝트에는 하단 Nav Bar가 필요가 없었기 때문에 stack navigation 만으로 충분히 페이지간 이동을 구현할 수 있었다.
Stack.js에 네비게이션을 아래와 같이 정의해준다. 스택에 쌓아서 내가 왔다갔다 할 페이지들을 전부 집어넣어주면 된다.
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Intro from "../screens/Intro";
import ConnectWallet from "../screens/ConnectWallet";
import MapOrFeeds from "../screens/MapOrFeeds";
import Map from "../screens/Map";
import Feeds from "../screens/Feeds";
import Feed from "../screens/Feed";
import MintNFT from "../screens/MintNFT";
import MyPage from "../screens/MyPage";
import Setting from "../screens/Setting";
import TakePhoto from "../screens/TakePhoto";
import ExistedWallet from "../screens/wallet/ExistedWallet";
import NewWallet from "../screens/wallet/NewWallet";
import MakePassword from "../screens/MakePassword";
import BuyBucket from "../screens/BuyBucket";
import CheckMnemonic from "../screens/setting/CheckMnemonic";
import ChangePassword from "../screens/setting/ChangePassword";
import ResetWallet from "../screens/setting/ResetWallet";
const NavigateStack = createNativeStackNavigator();
const Stack = () => {
return (
<NavigateStack.Navigator
screenOptions={{
headerShown: false,
contentStyle: {
backgroundColor: "#C3BD2E",
},
}}
>
<NavigateStack.Screen name="Intro" component={Intro} />
<NavigateStack.Screen name="ConnectWallet" component={ConnectWallet} />
<NavigateStack.Screen name="ExistedWallet" component={ExistedWallet} />
<NavigateStack.Screen name="MakePassword" component={MakePassword} />
<NavigateStack.Screen name="NewWallet" component={NewWallet} />
<NavigateStack.Screen name="BuyBucket" component={BuyBucket} />
<NavigateStack.Screen name="MapOrFeeds" component={MapOrFeeds} />
<NavigateStack.Screen name="Map" component={Map} />
<NavigateStack.Screen name="Feeds" component={Feeds} />
<NavigateStack.Screen name="Feed" component={Feed} />
<NavigateStack.Screen name="MintNFT" component={MintNFT} />
<NavigateStack.Screen name="MyPage" component={MyPage} />
<NavigateStack.Screen name="Setting" component={Setting} />
<NavigateStack.Screen name="CheckMnemonic" component={CheckMnemonic} />
<NavigateStack.Screen name="ChangePassword" component={ChangePassword} />
<NavigateStack.Screen name="ResetWallet" component={ResetWallet} />
<NavigateStack.Screen name="TakePhoto" component={TakePhoto} />
</NavigateStack.Navigator>
);
};
export default Stack;
그리고 App.js에 다음과 같이 Stack
을 넣어주고 NavigationContainer
로 감싸기만 하면 된다.
import { NavigationContainer } from "@react-navigation/native";
import Stack from "./navigation/Stack";
export default function App() {
return (
<NavigationContainer>
<Stack />
</NavigationContainer>
);
}
엄청나게 리서치를 해본 결과, 불가능하다! (가능한데 방법을 못찾았을 수도)
우리 앱은 stack navigation으로 페이지들이 연결되어 있다. 그리고 특정 하나의 페이지 안에 작은 tab
을 넣고 싶었다. 자세하게는 My Page에서 중첩된 2개의 탭을 넣고 싶었다.
아래 사진은 최종적으로 원하는 것을 구현해낸 결과이다. My Page라는 하나의 페이지 안에 My NFTs - My Wallet - My Txs 를 왔다갔다 하는 Tab이 있고, My NFTs 탭 안에 Created - Collected 를 왔다갔다 하는 하위 Tab이 또 하나 있다.
나는 stack navigation 안에 tab navigation을 그 안에 또 tab navigation을 넣으면 될 줄 알았다. 하지만 그런 방식으로 구현해낼 수 없었다. 이걸 해결하는 데에 가장 오랜 시간이 걸렸던 것 같다.
결국엔 react-native-tab-view
라는 라이브러리를 적용해서 해결할 수 있었다. My Page 안에 Tab-view를 중첩으로 넣은 것이다. 이 Tab-view 관련해서도 다양한 라이브러리들이 있었는데 나는 가장 기본이 되는 라이브러리를 선택했고, 문제를 해결할 수 있었다.
My NFTs에는 내가 발행한(혹은 소유한) NFT들이 썸네일로 뜨고 있는데, 썸네일을 클릭하면 해당하는 NFT의 상세 정보 페이지로 이동되도록 구현하고 싶었다.
React navigation의 정말 편리한 점 중 하나가, stack
이나 tab
의 정의만 잘 해두면 해당 네비게이션에 포함된 모든 페이지들에 자동으로 navigation
이라는 props를 전달할 수 있다. 이 navigation
props로 원하는 페이지로 이동하거나(navigate
), 뒤로 가기(goBack
)를 아주 쉽게 구현할 수 있다.
const MyPage = ({ navigation: { goBack, navigate } }) => {
// ... //
const goToBuyBucket = () => {
navigate("BuyBucket", { fromSignUp: false });
};
// ... //
}
하지만 내가 누를 썸네일사진들은 tab-view component 안에 있고, 이 tab-view는 navigation
props를 받아올 수 없었다...! 여기서도 정말 많은 시간을 허비했는데, 결과적으로는 useNavigation
을 위해 손쉽게 해결할 수 있었다. 어이가 없을 정도(ㅋㅋㅋㅋ)
import { useNavigation } from "@react-navigation/native";
const MyNFTsCreated = () => {
// useNavigation으로 navigation을 하나 만들어주고,
const navigation = useNavigation();
return (
// ... //
{reverse.map((img, index) => {
return (
<TouchableOpacity
key={index}
// 아래와 같이 사용하기만 하면 됨....!!!
onPress={() => navigation.navigate("Feed", { feedId: img.id })}
>
<View
style={[
{ width: imgSize },
{ height: imgSize },
{ marginBottom: 2 },
index % 3 !== 0 ? { paddingLeft: 2 } : { paddingLeft: 0 },
]}
>
<Image
source={{ uri: img.imageUrl, headers: { Accept: "*/*" } }}
style={{ flex: 1 }}
/>
</View>
</TouchableOpacity>
);
})}
</View>
</ScrollView>
);
};
export default MyNFTsCreated;
스마트폰에 expo 앱을 설치하여 스마트폰으로 렌더링 결과를 확인해가며 개발했는데, PC 화면을 공유하며 협업할 때 너무 답답하게 느껴져서 결국 PC에 시뮬레이터를 설치해주었다. 상당히 번거롭고 귀찮은 과정이었는데 M1 Mac에 시뮬레이터를 설치하는 방법을 잘 적어주신 블로그 글(여기!)을 발견해서 편하게 진행할 수 있었다.
기획의 어려움
프로젝트 1,2는 주제가 정해진 클론코딩 프로젝트였기 때문에 기획에 오랜 시간이 걸리지 않았다. 하지만 이번 프로젝트는 온전히 우리의 아이디어로 앱을 기획해야 했기 때문에 기획에만 일주일 이상이 걸렸다. 개발/코딩은 명확하다. 정확한 결과가 나오기 때문이다. 하지만 기획은 각자가 생각하기 나름이기 때문에 아주 사소한 의사결정이더라도 오랜 협의가 필요하다. 아이디어를 구체화하고 또 논의를 통해 새로운 아이디어를 발견하는 과정이 재미있기도 했지만, 역시 나에게는 기획보다는 개발이 더 잘 맞는다는 생각이 확고해졌다.
컨디션 관리
이번 프로젝트를 하면서 밤을 새는 날이 많았는데(물론 그러고 낮에 많이 잠), 건강에 좋지 않은 것은 물론이고 여러모로 지양해야 할 습관이라고 생각된다. '이것만 해결하고 자야지' 하는 생각으로 해가 뜰 때까지 문제를 붙잡고 있고는 했다. 하지만 오히려 중간에 끊고 적당히 리프레시한 뒤 다시 문제를 볼 때 쉽게 해결할 수 있는 경우가 더 많았다. 개발자가 되어 현업에서 일을 할 때에도 정해진 근무 시간에 맞추어 일을 해야 할테니 맺고 끊음을 잘 해가며 시간을 쓰는 버릇을 들이는 게 좋겠다.
구현하고 싶었던 부분은 대부분 구현되었다. 앱의 구성과 디자인도 기획과 거의 흡사하게 완성되었다. 하지만 시간적 여유가 부족해 적용하지 못한 몇 가지가 있다. (클라이언트 한정)
FlatList
의 onEndReached
, onEndReachedThreshold
, ListFooterComponent
3가지 props를 사용하면 될 듯 하다. 어려워서는 아니고 시간이 없어서 구현하지 못했다 😭 추후에 꼭 완성해보는걸로..!크게 아쉬웠던 점은 우선 시간이 부족했던 것이다. 애정과 욕심을 갖고 진행한 프로젝트였기 때문에 시간이 더 주어진다면 실제 서비스 앱 처럼 더욱 완성도를 높여보고 싶다. 또한 react-native에 대한 기본 지식이 거의 없는 상태에서 개발을 시작했다는 점이다. 배우면서 진행해야 했기에 조금 서툴렀고 또 여러모로 비효율적인 코드가 많이 작성된 듯 하다. 다시 앱을 개발할 기회가 생긴다면 이번보다 훨씬 효율적으로 개발할 수 있을 것 같다.