[프로젝트1] NFT 마켓 클론코딩

Donghun Seol·2022년 12월 30일
0

1. 프로젝트 개요

1.1 오픈씨 클론코딩

대표적인 NFT거래소인 오픈씨를 클론코딩하는 프로젝트다.
처음에 web3의 아키텍쳐에 대한 이해가 부족해서 web2적으로 구현해 나갔었고 이 때문에 시간을 많이 낭비했다. 크리스마스도 끼어 있었고, 지정해 준 팀원 중 한명이 코스를 그만두게 되어서 인원에 비해 작업량이 많고, 시간은 부족했다. 그럼에도 불구하고 나를 포함한 모든 팀원들이 열성적으로 참여해줬고 결과적으로 MVP는 구현할 수 있었다.

여기를 클릭하면 클라이언트에 접속할 수 있다
http://react-to-s3.s3-website.ap-northeast-2.amazonaws.com

1.2 스크린샷

카테고리 explore

구매하기

lightsail EC2 인스턴스에 배포된 서버 로그

2. 맡은 역할

2.1 팀장

깃 저장소를 관리할 수 있는 권한을 주기때문에 자원해서 팀장을 맡았다. 최근 학습한 깃헙 액션을 적극적으로 적용해보고 싶었고, 깃 플로우에 대해서도 잘 이해해보고 싶었기 때문이었다. 깃헙액션은 클라이언트의 CD 파이프라인을 구축하는데 아주아주 유용하게 활용했다. 다만 github의 기능 중 project, issue, taskcard, board등은 코드 작성하느라 바빠서 활용하지 못하고, 디스코드와 노션으로 대부분의 소통을 한 점은 아쉬움으로 남는다.

2.2 백엔드

원래부터 희망하는 포지션이라서 당연하게도(?) 내가 맡게 되었다. 일반적인 api서버를 구축하는 것에는 자신있었다. 하지만 개발과정 중 컨트랙트의 이벤트를 실시간으로 수신해서 db를 업데이트하는 과정을 구현하는 것은 어려움이 많이 있었다.

2.3 컨트랙트

컨트랙트를 할 인원이 부족해서, 결국 내가 다 하게 되었다. 스마트 컨트랙트에 대한 이해가 깊지 않은 상태에서 시작해서 많이 힘들었다. 결과적으로는 컨트랙트의 구성요소와 솔리디티 문법에 대해 많은 고민을 할 수 있었다. 이를 통해 해당 주제에 대해 더 깊게 이해할 수 있어서 보람있었다.

3. 구현한 내용

3.1 백엔드

웹3에서 백엔드와 DB의 역할

처음에는 몰랐다. 서버와 DB가 그렇게까지 필요없다는것을... dApp에서 핵심 로직은 클라언트와 블록체인간에만 동작하면 된다. 클라이언트도 언제든지 체인에 읽기/쓰기가 가능하기 때문이다. 그래서 서버와 DB는 이를 서포트 해주는 역할을 수행해줘야 한다. 따라서 이 프로젝트에서 서버는 블록체인의 큰 레이턴시를 보완하기 위해 체인의 정보를 항상 DB에 캐싱해놓고, 클라이언트가 원하면 재빨리 제공해주는 역할을 하기로 했다.

이러한 기능을 구현하기 위해서 infura websocket api를 통해 블록이벤트 수신을 구현했다. 서버에 명시된 특정 CA의 컨트랙트에서 Transfer 이벤트가 emit 될때마다, 서버는 이를 수신해서 DB를 갱신한다. 이벤트 리스너와 DB갱신은 비동기적으로 이루어지는데, 생각보다 노드 서버의 비동기처리의 성능이 너무너무 우수했다.

컨트랙트의 구조상 새로운 토큰 민팅시에 Transfer 이벤트가 두번 발생하는데, 이를 수신한 이벤트 리스너가 각자 DB를 업데이트하려 시도해서 DB에 레이스컨디션이 발생해서 duplicate key 에러가 자꾸 발생했다.. 비동기처리 속도가 너무 빠르기 때문이라 생각한다. 결국 findOneAndUpdate 메서드에 {upsert : true} 옵션을 주어 해결했다.

3.2 스마트 컨트랙트

가장 아쉬움이 많이 남는 부분이다. ERC-721 표준과 거래소 컨트랙트를 완벽히 이해하지 못하고 프로젝트를 해서 어떻게 컨트랙트를 작성해야할지 많이 막막했었다. 몇일 고민하다가 alchemy tutorial 을 참고해서 약간만 개선해서 컨트랙트를 배포했다. 그런데...

이 컨트랙트는 교육을 목표로 작성되어 제한적인 로직과 메서드를 가지고 있었다. 단순 민팅과 거래는 가능하지만 리스팅을 선택하거나, 리스팅된 NFT를 제거하는 등의 기능은 제공해주지 않았다. 이 때문에 컨트랙트에 의존적일 수 밖에 없는 클라인트와 서버 모두 제한적인 기능으로 구현될 수 밖에 없었다.

web3 프로젝트는 무엇보다도 컨트랙트에 있는 자료구조와 메서드를 설계한 다음에, 이를 바탕으로 클라이언트와 서버를 설계해야 제대로 기획할 수 있다는 점을 깨달았다.

또한 클론코딩 프로젝트의 핵심인 오픈씨의 스마트컨트랙트를 이해하지 못했다는 점은 매우 아쉽다. 추가로 반드시 학습해서 Factory - LootBox - Creature 모델을 이해하고 넘어가는 것이 매우 시급하다고 생각한다.

3.3 배포

백엔드를 도커라이즈해서 깃헙 액션으로 AWS ECR에 등록하고, AWS Fargate를 활용해서 배포하려고 생각은 계속했는데, 밀려드는 오류와, 컨트랙부분도 담당하느라 결과적으로는 시도해보지 못했다. 그래서 LightSail에서 3개월동안 무료로 제공해주는 EC2 인스턴스에 수동으로 노드 환경을 설정하고, 한땀한땀 Git pull 해서 또 다시 pm2로 가동시켰다.

굳이 라이브 서버를 개발과정중에 띄운 것은 팀원들의 개발 능률 향상을 위해서 꼭 서비스 해주고 싶었기 때문이다. 라이브로 api를 응답해주는 백엔드가 있으면 프론트엔드와 실시간으로 소통하면서 api를 수정하고, 에러에 신속히 대응할 수 있기 때문이다. 클라우드 서비스를 활용한 덕분에 이러한 좋은 개발 경험을 제공해 준 점은 백엔드 개발자로서 매우 자랑스럽게 생각한다.

(잠들려다가 api 서버가 터졌다는 연락을 받고 대응한적도 있었다. 원래 백엔드는 항상 온콜 해야된다 카더라. 😢)

4. 문제와 해결책

솔직히 해결하고 나서 해답을 적는건 쉬운데 막상 로그를 뒤져서 찾아내는건 힘들었다. 😢

web3 dApp 설계

레퍼런스를 찾아서 따라하면서 MVP로 돌아가게는 구현했지만 아직 dApp과 Contract에 대해서 배울 내용이 너무너무너무 많다. 문제를 해결했다는 느낌이 아니라 의문과 호기심만 잔뜩 떠앉게 되었다.

체인데이터와 데이터베이스 동기화

요런 이벤트 리스너를 붙여주었다.

 Contract.events.Transfer().on('data', async (event) => {
      const { transactionHash, address, returnValues } = event;
      const isExist = await Transaction.findOne({ transactionHash });
      if (isExist) {
        logger.info(`TransactionHash : ${transactionHash} Already processed`);
        return;
      }
   //... 생략
 }
)

이벤트리스너 레이스 컨디션 발생

이런식으로 upsert 활용했다.

const updateUserDB = async (account) => {
  let user = await User.findOne({ account });
  if (!user) {
    user = await User.findOneAndUpdate(
      { account },
      { account, nickname: 'anonymous', collected: [], created: [] },
      {
        upsert: true,
      },
    );
  }
  console.log(user);
  return user;
};

db Array에 중복된 데이터 배제

$addToSet 연산자를 활용했다.

      await User.updateOne(
        { account: newOwner },
        { $addToSet: { collected: nft._id } },
      );

서버 로그설정

로그파일을 남기려고 winston logger를 설정했다.
요런 로거를 사용했는데, 콘솔에 예쁘게 출력하려고 나름 노력했지만 아직 많이 부족한 상황

const { createLogger, format, transports } = require('winston');

const koreanTime = () =>
  new Date().toLocaleString('en-US', {
    timeZone: 'Asia/Seoul',
  });

const logger = createLogger({
  transports: [
    new transports.File({
      filename: 'combined.log',
      format: format.combine(
        format.timestamp({ format: koreanTime }),
        format.json(),
      ),
    }),
    new transports.File({ filename: 'error.log', level: 'error' }),
  ],
  exitOnError: false,
});

if (process.env.NODE_ENV !== 'production') {
  logger.add(
    new transports.Console({
      level: 'info',
      colorize: true,
      format: format.combine(
        format.timestamp({ format: koreanTime }),
        format.simple(),
      ),
    }),
  );
}

module.exports = logger;

nodemon *.log 인식

로그파일을 만드니, 노드몬이 로그파일을 인식해서 자꾸 개발서버를 재시작시켰다.
nodemone.json에 설정해줘서 해결

{
  "verbose": true,
  "ignore": [
    "*.test.js",
    "**/metadataFiles/**",
    "**/combined.log",
    "**/error.log"
  ],
  "execMap": {
    "rb": "ruby",
    "pde": "processing --sketch={{pwd}} --run"
  }
}
profile
I'm going from failure to failure without losing enthusiasm

0개의 댓글