1. 담당 역할
- NextJS 기반 NFT 마켓플레이스 클라이언트 전면 개발
- NestJS 기반 NFT 거래 API 개발
- 퍼블리싱 산출물(HTML/CSS)을 React 구조로 재구현
- 외부 SDK(FaceWallet) 연동 및 서명 플로우 UI 구현
- NFT 구매·판매·취소 등 복잡한 사용자 흐름 UI 제어
- 보유/판매 NFT 리스트 무한 스크롤 및 필터·정렬 기능 개발
- 모바일 앱(WebView) 연동 및 iOS 이슈 대응
2. 온체인 마켓 플레이스 소개
개발 배경
서비스 내에 유저들은 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 연동 및 개발
- 반응형 모바일 웹뷰 개발
- NFT 리스트
- 보유 NFT / 판매 중 NFT 분리 노출
- 무한 스크롤 기반 데이터 로딩
- 판매 상태, 가격 기준 필터 및 정렬 기능
- NFT 거래 플로우
- NFT 판매 등록 / 취소
- NFT 구매 및 거래 결과 처리
- 외부 SDK 서명 → 거래 대기 → 결과 반영까지의 UI 상태 관리
- NFT 리스트
5. 개발 방향 및 사용 기술
거래소 클라이언트는 타입스크립트를 베이스로 NextJS를 사용했다. React로만 개발하려고 했으나 당시 팀인원이 부족하여 모두가 풀스택으로 개발을 하고 있었고 이왕 시작하는 거 다 같이 NextJS로 개발해 보는 건 어떠냐는 의견이 나왔다. 블록체인으로 개발된 서비스들은 대부분 동작도, 업데이트도 느리다. SSR을 쓴다면 그나마 개선된 것처럼 보이지 않을까, 실제 NFT 마켓플레이스들이 NextJS로 개발을 많이 하고 있는 추세였다. 그래서 React는 기본적으로 익히고 있으니 NextJS로 개발을 시작했다.
클라이언트와 연동할 서버는 NestJS를 사용했다. 코인과 NFT는 실제 가치가 있는 재화다 보니 서버 처리 과정에서 정확하고 재사용성 높은 밸리데이션을 사용하고 싶었다. 내부 기능은 스펜딩 지갑이라 불리는 RDB의 서비스 데이터 베이스에 적재된 숫자로 코인 개수나 NFT 보여주는 시스템이 있었고 이를 온체인 지갑으로 출금하면 RDB의 데이터를 업데이트하는 과정을 겪게 된다. NFT 민팅이나 코인 전송 등 데이터들은 조금이라도 빨리 처리하기 위해 Redis가 1차적으로 데이터 적재 후 RDB와 동기화하는 하이브리드 형태를 사용했다. 온체인 관련된 처리는 메타보라에서 보라코인을 공급하면서 마켓거래와 코인전송에 관련된 API를 제공하였고 나의 Nest 서버에서 다시 외부 API 요청을 통해 받은 응답값으로 로깅, 이후 행위 처리들을 실행하는 구조였다.
메타마스크를 사용하던 것을 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를 선택했습니다. 그 결과 프론트엔드와 서버 개발자 모두가 빠르게 상태 흐름을 이해하고 유지보수할 수 있었습니다.
기술 스택
- Frontend: React, NextJS, TypeScript
- Backend: NestJS, TypeScript
- State Management: Zustand
- UI Library: Ant Design
- App 연동: React Native WebView
- External: FaceWallet SDK, BORA API
6. 개발 회고
Problem & Try:
Problem 1. 외부 API를 연동하였고 이상거래가 감지된다.
fw와 BORA API를 번갈아가면서 구매와 판매로직을 구현하는 데 있어 가장 중요한 부분은 유저들이 토큰을 지불했다면 원했던 NFT를 정상적으로 구매하게 해야 한다. 자체 거래 컨트랙트도, API도 없었고 블록체인 특성상 데이터를 되돌릴 수 없기에 외부 API와 거래시스템에 의존할 수밖에 없었고 오로지 스펜딩 관련 API만 관여할 수 있었다.
간헐적으로 동시간대에 다른 유저가 같은 NFT를 구매한다면 한 유저는 토큰이 지불됐지만 NFT를 받지 못하게 되며 이를 거래 컨트랙트를 개발한 쪽에서 매일 스케줄링을 돌려 이상거래 된 유저에게 자동환불되도록 시스템을 구현하였다. 이 과정이 가능한 이유는 거래컨트랙트가 유저들의 토큰을 중간에서 이동시키는 역할을 해주기 때문이다 이상거래된 토큰을 권한이 있는 컨트랙트가 갖고 있기에 되돌려줌과 동시에 거래 수수료 일부도 차감하여 지급하는 것이 가능하다.
나는 유저들한테서 일어날 수 있는 이상거래에 대해 제어가능한 부분과 제어할 수 없는 부분을 나누었고 그중 제어가능한 부분을 해결하려고 하였다.
Try. 이상거래에서 제어가 가능한 부분은 구매 결과 주기적으로 확인하는 것이다. 첫째는 구매가 이루어지는 순간 정해진 tryCount를 통해 결과를 조회하는 API를 사용하여 찌르도록 하였다. 결과가 정상구매라면 빠르게 NFT를 지급하게 되고 실패라면 화면창에 거래실패를 알려주게 된다. 사실상 블록체인에 올라간 데이터, 즉 컨트랙트로 이동된 토큰은 외부사를 통해 처리하는 방법뿐이기에 유저에게 거래실패를 알려주고 CS로 처리하는 방법 밖에 없었다. 또한 구매 버튼을 최종적으로 누를 때 해당 NFT가 거래완료 된 것인지 빠르게 확인하여 이중요청을 방지하도록 하였다. 이는 사실 구매에 실패하여도 네트워크 수수료가 발생하는 점을 막아주었다. 이후 레디스를 활용하여 락을 걸도록 하였다. 우선적으로 누르는 유저가 구매하려는 NFT를 key 형태로 락을 건 다음 구매자의 처리는 막도록 하였다.
Problem 2. 앱에서 NFT를 온체인으로 옮기면 만들어진 NFT 정보가 다르다.
앱에서는 NFT의 레벨이나 능력치를 키울 수 있는 시스템이 있었다. 이렇게 만들어진 NFT를 온체인상으로 처음 옮기게 되면 정확하게 NFT민팅되었다. 하지만 이 민팅된 NFT를 다시 스펜딩 내부 지갑으로 가져온 뒤 능력치를 키운 뒤 다시 온체인으로 내보내면 온체인에서는 이를 동기화하지 못했다. 그래서 과거의 능력치로 보이게 된다.
Try. 온체인을 옮긴 NFT 정보가 다른 것을 디버깅하기 위해 온체인 전송에 사용되는 NFT 정보 파라미터가 일치하는지 확인했다. 일치했으나 온체인에서 동기화가 안된다는 것을 알게 됐다. 클레이튼 스코프를 보면 NFT 스탯을 동기화하는 버튼이 있었는데 메타보라에서는 이러한 동기화를 위해 API 개발이 필요했다. 사전에 우리의 NFT가 변동될 정보를 메타보라에 전달했고 동기화할 수 있는 API 문서를 전달받아 연동했다. 문제는 온체인 마켓에 이 API를 요청해도 바로 변경이 안된다는 것이었다. 메타보라에서 수정을 하였으나 계속해서 같은 문제가 간헐적으로 발생했고 NFT 메타데이터가 담긴 S3의 캐싱문제가 인가 싶어 확인했으나 아니었다. 우선적으로 해당 NFT 정보나 거래에 사용하기 위해 업데이트된 스탯은 우리 서버의 redis 정보로 보여주기로 했다. 온체인 마켓 컨트랙트와 서버 관리를 직접 할 수 없어 아쉬웠다. 여기서 외부 플랫폼으로 이 NFT를 보면 여전히 변경이 안된 상황이 존재했다.
Problem 3. 앱은 react-native로 만들었지만 마켓플레이스는 웹뷰이다.
IOS에서만 문제가 생겼다. 웹뷰창에서 닫기 버튼을 눌러도 앱에서는 이를 감지할 수 없었고 열린 온체인마켓 창에서 거래내역을 보기 위해 클레이튼 스코프 버튼을 누르게 되면 또 하나의 창이 열리며 정말 닫을 수 없는 상태에 도달했다.
Try. 앱과 웹뷰 연동은 처음 해보았고 웹뷰에서 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를 주어 이후에 온체인마켓 자체의 웹뷰창의 종료 버튼은 숨겨지지 않도록 수정하였다.
Problem 4. 컴포넌트 재사용성을 고려하다 보니 라우터 구분이 이상해지다.
React의 장점인 컴포넌트 재사용성을 따지다 보니, 화면에서 보유 NFT와 판매 NFT 출력 화면이 같았다. 이에 재사용성을 살려 같은 컴포넌트를 사용하게 되어 어떤 NFT든 상세 보기로 들어가게 되어 뒤로 돌아갈 시 보유로 갈지 판매 중인 화면으로 갈지 갈림길을 잃어버렸다.
Try. 잃어버린 갈림길을 해결하기 위해 컴포넌트를 아예 분리하는 과정보다 공수를 줄이기 위해 기획팀에게 의도를 파악 한 뒤 상세 보기 중인 NFT가 내 것이라면 보유로 아니라면 판매 중으로 이동시키도록 하였다. 이후 컴포넌트 분리 작업을 추가적으로 감행할 예정이었다. 실무 프로젝트에서 경험이 부족했었던 것 같다. 이후에는 컴포넌트 구조를 명확히 기획하고 개발하는 습관을 갖게 되었다.
Problem 5. NFT 구매도중 잠에 든다면..
이상했다. NFT를 구매하게 된다면 토큰 전송을 하기 위해 서명을 한 log, 최종적으로 NFT를 달라는 log로 이 핵심로그가 2개 쌓이는데, 전자 log는 없었고 후자 로그만 남아있었다. log를 뒤지다가 전자로그가 몇 시간이나 전에 발생했다는 것을 알게 되었다. 이 경우 후자로그의 처리 유효시간 이 넘어 버렸기에 전자만 처리하게 되며 토큰만 빠져나가게 된 것이다.
Try. NFT 구매 시 요청 후 유효시간이 지난 뒤 토큰을 보내겠다는 서명을 한다면, 유효시간을 넘었기에 처리할 수 없지만 fw와 구매 컨트랙트(메타보라)는 각기 다른 사의 서비스라 서로 간에 알 수가 없었다. 결국 토큰은 빠져나갔고 컨트랙트 사는 처리를 못하게 된다. 유효시간만 늘려서는 보안상 이슈가 있었고 현재로선 fw와 구매컨트랙트 간의 유효시간 체크에 따른 서명 불가처리가 필요해 보였다. 결론적으로 지갑 서명과 실제 거래 확인 및 전송 컨트랙트를 한 서비스가 처리해야하는데 fw와 메타보라가 서로 다른 플랫폼이라 생기는 이슈였고 이러한 상황을 외부 서비스에 버그리포트를 하니 우선적으로 매일 스케줄링을 돌려 잘못 전송된 코인을 되돌려주는 배치를 개발했다. 하지만 이미 날린 수수료는 어떻게 할 것인가..
Problem 6. NFT 판매 취소 후 뒤로 가기를 한다면 해당 NFT 상세 보기가 실패한다.
판매등록을 시작하면 fw SDK 창이 열리면서 서명창이 출력되고 일련의 과정을 거치게 되는데, 이때 취소를 하게 되면 어떤 이유에서 화면이 새로고침 됐다. 그렇다면 팔려고 했던 NFT의 데이터들이 state에서 날아가게 되어 상세 보기에서 어떤 NFT를 다시 보여줘야 할지 알 수가 없게 됐다.
Try. 판매 취소 후 다시 상세 보기로 뒤로 가기 전에 해당 NFT의 정보들은 URL의 queryString으로 가지고 있었다. 그래서 NextJS 공식문서에 있는 getServerSideProps 을 사용했다. NextJs는 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,
},
};
};
Problem 7. 사용자에게 코인을 보상할 때, 블록체인 수수료가 낭비됐다.
100명의 유저에게 같은 양의 코인을 보상하는 기능이 운영툴에 있었다. 한번 클릭을 할 때마다 메타보라의 API를 사용하여 전송하게 되었는데 일반적인 토큰 컨트랙트에 있는 MultiSender를 사용하지 않고 Transfer 함수만 사용하여 1명 유저마다 이 함수를 루프 돌려 처리하니 수수료가 낭비가 발생했다.
Try. 유저에게 코인 대량 전송 시 MultiSender를 활용하고자 했다. 외부 API로 코인을 전송하다 보니 기능 수정에 제한이 있었다. 당시 협업을 하던 메타보라에 이러한 기능 개발을 제안했고 빠르게 수정되어 내부적으로는 Nest 서버에서 한 번에 보상하는 대신 유저를 더 꼼꼼히 체크하기 위해 추가적인 밸리데이션을 적용하여 실제 받을 유저인지를 체크했고 유저 여러 명으로 묶인 파라미터로 요청하여 수수료를 절감할 수 있었다.
Review:
블록체인처럼 되돌릴 수 없는 도메인에서는 프론트엔드가 곧 안전장치라는 점을 깊이 체감했습니다.
단순한 화면 구현이 아니라, 사용자의 행동 흐름을 예측하고 실패 가능성을 UI로 제어하는 것이 프론트엔드 개발자의 중요한 역할임을 배울 수 있었습니다. 실제 재화가 오가는 서비스를 운영하며 사용자 경험과 안정성의 중요성을 동시에 경험한 프로젝트였습니다.
7. 성과
- 실제 유저가 사용하는 NFT 마켓플레이스 운영
- 일 거래 금액 약 2천만 원 규모까지 성장
- 외부 SDK·API 연동 환경에서 안정적인 거래 UX 제공
![]() |
![]() |
![]() |
![]() |
![]() |
'포트폴리오 > Project_4' 카테고리의 다른 글
| 골프장 키오스크 웹 개발 (0) | 2025.12.29 |
|---|





댓글