1. 역할과 기여
- 외부 SDK와 API를 연동하여 서비스 내에 존재하는 유저들의 NFT를 사고 팔 수 있는 마켓플레이스를 개발한다.
- 클라이언트와 서버 모두 작업하며 웹 퍼블리싱 파일은 초기 구조만 전달 받아 React 기반 nextJs로 재구현한다.
- 추가 기획에 따른 기능들은 퍼블리싱 없이 nextJs에서 직접 구현한다.
2. 온체인 마켓 플레이스 소개
1. 개발 배경
서비스 내에 유저들은 NFT를 소유하고 있다. 해당 NFT를 블록체인상에서 자유롭게 구매/판매 할 수 있는 공간이 필요했고 이를 통해 거래 수수료로 부가적인 수익을 얻을 수 있었기에 개발을 시작하였다. 특히 과거에 KIFT라는 온체인마켓 플레이스를 개발해본 경험이 있었으며 블록체인과 지갑 그리고 거래 방식에 대해 잘 알고 있었기에 개발에 더욱 적극적으로 참여하였다.
3. 거래 방식
우선 사내에서 만든 블록체인 지갑이나 토큰은 없었고 NFT는 클레이튼에 올라가있었으며 스펜딩지갑과 온체인지갑을 넘나드는 경우에는 해치랩스의 face-wallet을 사용하며 서명을 하게되며 처리요청을 BORA API를 통해 블록에 추가하여 처리했었다.
<판매>
1. 유저가 판매할 NFT를 선택한다.
2. 해당 NFT를 BORA에 판매 요청 한다.
3. 해당 NFT를 BORA가 만든 거래 컨트랙트에 권한을 주기 위해 face-wallet으로 서명을 받는다.
4. 컨트랙트가 권한을 가지고 있다가 다른 유저가 해당 nft를 구매하고 구매가격 만큼 재화를 입금받는다면 컨트랙트가 NFT를 빼서 구매자에게 전송한다.
<구매>
1. 유저가 구매할 NFT를 선택한다.
2. 해당 NFT를 BORA에 구매 요청한다.
3. 해당 NFT의 가격 만큼 토큰을 거래컨트랙트에게 통해 출금하기 위해 face-wallet으로 서명을 받는다.
4. 컨트랙트가 토큰을 빼서 유저에게 발송하고 NFT를 입금 받는다.
4. 필요한 주요 기능
- 거래를 위한 BORA에 API 연결
- face-wallet SDK 사용하여 서명, 지갑로그인, 홈창 연결
- 판매 및 보유 중인 nft 무한 스크롤
- 판매중인 nft 필터 기능 및 정렬 기능
- 반응형 웹
5. 개발 방향 및 사용 기술 ( 맡은 역할 중심 )
타입스크립트를 베이스로 NextJs를 사용했다. react기반으로 개발하려고 했으나 당시 팀인원이 부족하여 모두가 풀스택으로 개발을 하고 있었고 이왕 시작하는거 다같이 nextJs로 개발해보는건 어떠냐는 의견이 나왔다. 블록체인으로 개발된 서비스들은 대부분 동작도, 업데이트도 느리다. SSR을 쓴다면 그나마 개선된 것처럼 보이지 않을까, 실제 NFT 마켓플레이스들이 nextJs로 개발을 많이 하고 있는 추세였다. 그래서 react는 기본적으로 익히고 있으니 nextJs로 개발을 시작했다.
메타마스크를 사용하던것을 fw로 바꾸게 되니 키관리를 더욱 쉽게 할 수 있게 되었고 핸드폰이나 이메일만으로도 키를 복구 할 수 있음이 장점이였다. 이 과정에서 fw는 어떻게 키를 관리하게 되는지, 유저에게 모든 키관리 책임을 부여하는 메타마스크랑은 어떻게 다른지 궁금하게 되었고 팀원들은 블록체인이 생소했기에 fw 서명방식과 NFT 거래 방식에 대해 알려주기 위해서 누구보다 fw를 잘 알고 쓸줄 알아야했다. 그래서 fw에 대해 자세히 찾아보았고 게속해서 해치랩스 팀과 소통해가면서 해치랩스측에서도 알지 못했던 버그를 찾아가며 개발을 해나아갔다.
그렇다면 fw(face-wallet)란? 메타마스크는 데스크탑에서 이용할 시 크롬익스텐션으로 설치하고 실행 시 지갑 주소를 받게 되며 지갑주소 별 프라이빗키는 암호화하여 Local에 저장된다. 이후 컨트랙트가 연동된 웹에서 트랜잭션을 날리게 되면 메타마스크가 해당 트랙잭션에 프라이빗키로 서명을 이어주게 된다. 여기서 페이스월렛과 차이점은 페이스월렛은 별도의 설치 없이 서비스 도메인 별로 발급한 apikey와 블록체인 네트워크 정보를 가지고 face 객체를 만들어 이를 ethers라는 라이브러리의 Providers에 주입하여 프로바이더를 만들게 되고 만든 프로바이더로 트랜잭션이나 블록체인 메소드를 요청하게 되면 2개의 쉐어(사용자 웹 브라우저와 페이스월렛 서버)를 모아 신뢰 할 수 있는 컴퓨팅 환경(AWS)에서 복호화하여 키를 얻어 서명을 하게 된다.
일전에 개발했던 KIFT 중심으로 자신있었던 react로 개발을 시작하였다. 클라이언트에서 face-wallet SDK와 BORA에 요청시 필요한 값들은 백엔드엔드에서 생성하고 가져와 해당 키를 다시 파라미터스토어로 와 환경변수로 복호화하도록 하였다.
클라이언트는 초기 구조는 퍼블리싱 팀에서 html과 css형태로 전달해주었다. 하지만 react로 만들려고 하니 jQuery 형태로 작성해준 부분들을 모두 react에서 함수기능으로 다시 구현해야 했고 시간을 최소화하기 위해 antd와 디자인컴포넌트를 유용하게 사용하였다. 사실상 어떤기능인지 파악한다음 디자인 컴포넌트로 새로 구현한거나 마찬가지였다.
상태관리를 하기 위해 처음에는 redux를 고민했나 구현 기능들이 가볍고 거래와 가진 아이템보기가 전부라 가벼운 상태관리를 찾다가 zustand라는 상태관리를 접하게 되었고 사용해보니 큰 러닝커브없이 다룰 수 있는 라이브러리였다. 당시 개발팀 인력이 부족하였고 모두가 풀스택으로 개발하고 있었기에 서버개발자들도 클라이언트 개발을 접해야 했고 누구나 쉽게 이해할 수 있는 라이브러리를 찾는게 주 목적이라 사용하게 됐다.
베이스: node, typeScript
클라이언트: react, nextJs
서버: nestJs
상태관리: zustand
SDK: faceWallet
external: bora
7. 개발 회고
Keep & Problem:
-외부 API를 연동하였고 이상거래가 감지된다.
:fw와 BORA API를 번갈아가면서 구매와 판매로직을 구현하는데 있어 가장 중요한 부분은 유저들이 토큰을 지불했다면 원했던 nft를 정상적으로 구매하게 해야한다. 하지만 자체 거래 컨트랙트도, API도 없었고 블록체인 특성상 데이터를 되돌릴수 없기에 외부 api와 거래시스템에 의존할 수 밖에 없었다.
그래서 동시간대에 다른 유저가 같은 nft를 구매한다면 한 유저는 토큰이 지불됐지만 nft를 받지 못하게 되며 이를 거래 컨트랙트를 개발한 쪽에서 매일 스케줄링을 돌려 이상거래 된 유저에게 자동환불 되도록 시스템을 구현하였다. 이 과정이 가능한 이유는 거래컨트랙트가 유저들의 토큰을 중간에서 이동시키는 역할을 해주기 때문이다 이상거래된 토큰을 권한이 있는 컨트랙트가 갖고 있기에 되돌려줌과 동시에 거래 수수료 일부도 차감하여 지급하는 것이 가능하다.
나는 유저들한테서 일어날 수 있는 이상거래에 대해 제어가능한 부분과 제어할 수 없는 부분을 나누었고 그중 제어가능한 부분을 해결하려고 하였다.
-앱은 react-native로 만들었지만 마켓플레이스는 웹뷰이다.
:늘 그랬다. ios에서만 문제가 생겼다. 웹뷰창에서 닫기 버튼을 눌러도 앱에서는 이를 감지할 수 없었고 열린 온체인마켓 창에서 거래내역을 보기위해 클레이튼 스코프 버튼을 누르게 되면 또하나의 창이 열리며 완전 닫을 수 없는 상태에 도달 했다.
-컴포넌트 재사용성을 고려하다보니 라우터 구분이 이상해지다.
:react의 장점인 컴포넌트 재사용성을 따지다보니, 화면에서 보유 NFT와 판매 NFT 출력 화면이 같았다. 이에 재사용성을 살려 같은 컴포넌트를 사용하게 되어 어떤 NFT든 상세보기로 들어가게 되어 뒤로 돌아갈 시 보유로 갈지 판매중인 화면으로 갈지 갈림길을 잃어버렸다.
-NFT 메타데이터 속성이 변경됐지만 판매등록시 반영이 되지 않는다.
:앱내에서 nft의 속성값을 향상시킬 수 있다. 블록체인이 속성값이 기록되었지만, 앱내에서 속성값을 변경하여 다시 블록체인으로 가져간다면 변경된 속성값이 적용되어야하지만 반영되지 않는다.
-NFT 구매도중 잠에 든다면..
:이상했다. nft를 구매하게 된다면 토큰 전송을 하기 위해 서명을 한 log, 최종적으로 nft를 달라는 log로 이 핵심로그가 2개 쌓이는데, 전자 log는 없었고 후자 로그만 남아있었다. log를 뒤지다가 전자로그가 몇시간이나 전에 발생했다는 것을 알게 되었다. 이 경우 후자로그의 처리 유효시간 이 넘어 버렸기에 전자만 처리하게 되며 토큰만 빠져나가게 된 것이다.
-NFT 판매 취소 후 뒤로가기를 한다면 해당 NFT 상세보기가 실패한다.
:판매등록 을 시작하면 fw SDK 창이 열리면서 서명창이 출력되고 일련의 과정을 거치게 되는데, 이때 취소를 하게되면 어떤이유에서 화면이 새로고침 됐다. 그렇다면 팔려고 했던 NFT의 데이터들이 state에서 날아가게 되어 상세보기에서 어떤 NFT를 다시 보여줘야할 지 알 수가 없게 됐다.
Try:
- 이상거래에서 제어가 가능한 부분은 구매 결과 주기적으로 확인하는 것이다. 첫째는 구매가 이루어지는 순간 정해진 tryCount를 통해 결과를 조회하는 API를 사용하여 찌르도록 하였다. 결과가 정상구매라면 빠르게 nft를 지급하게 되고 실패라면 화면창에 거래실패를 알려주게 된다. 사실상 블록체인에 올라간 데이터, 즉 컨트랙트로 이동된 토큰은 외부사를 통해 처리하는 방법뿐이기에 유저에게 거래실패를 알려주고 CS로 처리하는 방법 밖에 없었다. 또한 구매 버튼을 최종적으로 누를 때 해당 NFT가 거래완료 된 것인지 빠르게 확인하여 이중요청을 방지하도록 하였다. 이는 사실 구매에 실패하여도 네트워크 수수료가 발생하는 점을 막아주었다.
- 앱과 웹뷰 연동은 처음해보았고 웹뷰에서 message를 던져 앱에서 트리거 하여 웹뷰를 닫도록 하는 방법이 있다고 했고 코드를 찾아보니 아래 코드를 사용하여 트리거 메세지를 던질 수 있었다.
// webview
window.ReactNativeWebView.postMessage('CLOSE_WEBVIEW_MESSAGE');
// app
<WebView
source={{
uri: `${params.url || ""}`,
}}
ref={webViewRef}
javaScriptEnabled={true}
onMessage={event => {
if (event.nativeEvent.data.includes("http")) Linking.openURL(event.nativeEvent.data)
else event.nativeEvent.data === CLOSE_WEBVIEW_MESSAGE ? navigate(Screen.BACK) : null
}}
/>
};
- CLOSE_WEBVIEW_MESSAGE 라는 웹뷰 종료 상수를 만들었고 웹뷰에서 message를 던져줄 때 웹뷰를 종료하도록 추가하였다. 이 때, 웹뷰에서 클레이튼 스코프창을 다시 띄우려고 한다면 외부의 또다른 창이므로 해당 웹뷰창에서가 아닌 새로운 웹창으로 열리도록 다른 message를 주어 이후에 온체인마켓 자체의 웹뷰창의 종료 버튼은 숨겨지지 않도록 수정하였다.
- 잃어버린 갈림길을 해결하기 위해 컴포넌트를 아예 분리하는 과정보다 공수를 줄이기 위해 기획팀에게 의도를 파악 한 뒤 상세보기 중인 NFT가 내것이라면 보유로 아니라면 판매중으로 이동시키도록 하였다. 이후 컴포넌트 분리 작업을 추가적으로 감행할 예정이였다.
- 클레이튼 스코프에서 보면 NFT 메타데이터를 갱신 할 수 있는 기능이 있다. 이는 메타마스크로 본인 nft를 갱신할 수 있는데 우리 nft는 fw에서 친화적이게 사용되고 있다. 그래서 블록체인 지갑으로 전송시에 BORA에서 갱신 api를 받아 블록체인으로 이동시, 판매 등록시 두번 갱신 요청을 하도록 수정하였다. 이과정에서 실제 갱신 api가 정상작동하지 않는 버그가 있어 리포트 하기도 했다.
- NFT 구매 시 요청 후 유효시간이 지난 뒤 토큰을 보내겠다는 서명을 한다면, 유효시간을 넘었기에 처리할 수 없지만 fw와 구매컨트랙트는 각기 다른 사의 서비스라 서로간에 알수가 없었다. 결국 토큰은 빠져나갔고 컨트랙트 사는 처리를 못하게 된다. 유효시간만 늘려서는 보안상 이슈가 있었고 현재로선 fw와 구매컨트랙트 간의 유효시간 체크에 따른 서명 불가처리가 필요해보였다.
- 판매 취소후 다시 상세보기로 뒤로가기 전에 해당 NFT의 정보들은 URL의 queryString으로 가지고 있었다. 그래서 nextJS 공식문서에 있는 getServerSideProps 을 사용했다. nextJs는 SSR이라서 react 처럼 route 같은 함수로 현재 주소를 뽑아오는데 제한이 있었고 getServerSideProps를 사용해서 현재 query를 추출해 props 형태로 다시 열릴 컴포넌트로 전달 해주는 방식을 사용했다.
import { GetServerSideProps } from 'next';
import { NftDetail } from '@/components/nft/NftDetail';
import { NftAttributes } from '@/types/components/nft/detail.type';
const Nft = (props: NftAttributes) => {
// nft 다이나믹 라우트
const { nftId, kind, grade, season, player } = props; // 아래 getServerSideProps에서 받아온 props
return (
// kind에 따른 일반 nft와 한정nft 컴포넌트 분기 처리
<div>
<NftDetail
nftId={nftId}
kind={kind}
grade={grade}
season={season}
player={player}
/>
</div>
);
};
export default Nft;
export const getServerSideProps: GetServerSideProps = async (context) => {
// 페이지 요청마다 실행되어 nftId로 다이나믹 라우트를 하기 위해 사용
const { id: nftId, kind, grade, season, player } = context.query; // 쿼리스트링으로 받아온 값
return {
props: {
nftId,
kind: kind || null,
grade: grade || null,
season: season || null,
player: player || null,
},
};
};
Review: 2개의 다른 회사의 SDK, API를 연동하면서 버그도 많이 찾게 되었고 많이 배울 수 있었다. 앱개발은 해본적 없지만 앱 개발 기본은 알아야 웹뷰를 연동하고 제어할 수 있음에 앱개발도 같이 할 수 있는 기회가 되었고 블록체인에서 이루어지고 기록되는 거래고 유저 나이대가 약간은 있는 서비스다 보니 이해를 요구하기 보단 특정 행위에 대한 실패를 잘 알려주는 것이 웹 개발에서 중요했던 것 같다. 유저 api 요청에 대한 로그를 들여다보니 거래건수가 적었을 땐 몰랐지만 거래건수가 조금씩 나오기 시작할 때 이슈가 생기기 시작했고 내가 만든 거래 플랫폼이 직접 유저들이 사용하고 거기서 p2e처럼 되어 간다는 것에 자부심과 즐거움을 느꼈다. 오픈씨나 대형 NFT 거래플랫폼 처럼 많은 거래량은 아니더라도 예전 부터 꿈꿔왔던 nft거래소를 만들어보고 실제 서비스 되며 마지막으로 봤던 일 거래금액이 약 2천만원에 도달하는 것을 보며 뿌듯했었다.
댓글