보다 더 생생한 기록

[코드리뷰] Project 2 - 인센티브 기반 커뮤니티 본문

블록체인

[코드리뷰] Project 2 - 인센티브 기반 커뮤니티

viviviviviid 2022. 11. 10. 02:49
PROJECT 1에도 많이 성장했지만,
2에서는 성장함과 동시에 티키타카가 잘되는 팀원들이 있어서 즐거움까지 더해졌다.

 

WEBTOON COMMUNITY

 


SUMMARY

1. 배경

  • 웹툰을 좋아하는 세명이 팀이 되어, 인센티브 기반 웹툰 커뮤니티 개발 계획 시작
  • 웹툰 최강자전 리그 中 결승전의 시점을 배경으로 삼고 있음

2. 구현 기능

  • 회원가입, 로그인, 마이페이지
  • 좋아하는 웹툰에 투표
  • 승리를 예상하는 웹툰에 배팅
  • 결승전 웹툰에 관해 논의할 수 있는 채팅
  • 좋아하는 웹툰 작가님의 그림을 NFT로 민팅

3. 토큰노믹스

  • 토큰 보상 : 배팅에서 승리, 회원가입, 투표, 채팅
  • 토큰 소각 : 배팅에서 패배, 좋아하는 웹툰 작가님의 그림을 NFT로 민팅

 


USE STACK

 


PAGE PREVIEW

메인페이지, 회원가입, 로그인
최강자전 페이지에서 투표, 배팅, 채팅

  1.  

CODE REVIEW

server/controller

index.js : 각종 라우터 기능 포함

app.post("/user/login", function (req, res) {
  const userInfo = req.body.signIn;
  Login(userInfo,res);
});

위와 같은 라우터 기능들이 잔뜩모여있다. 여기서 Login의 파라미터로 post의 res를 넣는데, 이게 이번에 얻은 중요한 깨달음 중 하나다. Login.js의 코드를 가져오면,

 const Login = (data ,res) => {
 	...

	connection.query(`  SELECT ~~~ where A.user_id = "${data.user_id}" `, function(error, results, fields) {
		if (error) throw error;
		if (results.length === 0 || results[0].password !== data.password){
			res.status(500).send("fail");
		}else{
			res.status(200).send(results);
		}
  	})

    ...
  }

을 볼 수 있는데, 원래는 connection.query 내의 results 값을 밖으로 빼내려 했었다. 전역변수를 포함한 여러가지 방법을 써봤지만 콘솔에 찍히는건 undefined 뿐이였다. 그럴때 등장한 방법이 저 res 방법과 또 다른 한 가지 방법이었다. 다른 방법은 하이퍼링크를 걸어 두겠지만 res 방법보단 살짝 복잡하기에 res 방법을 서술하겠다. 물론 저 방법에서 도출된 내용이기에 꼭 숙지하도록 하자. (저기 답변자 왈 "Welcome to CallBack Hell" lol)

 

index.js 에서 Login.js 에 props 로 res를 보냈다. 그러면 이제 Login.js의 함수에서도 res.method를 사용할 수 있게 된다. 

res를 넘기는것 까진 가능할거라 생각했지만 메소드까지 사용할 수 있는건 큰 충격으로 다가왔다. 자 이제 이 방법을 쓰면 쿼리 함수내에서 return을 쓰고 지지고 볶는일이 사라진다. 

 

server/model

CreateDatabase.js : DB와 테이블을 생성할 수 있는 파일. 서버쪽을 노드로 돌리면 자동으로 실행

connection.query("CREATE TABLE if not exists vote(tournament varchar(255), num_one int, num_two int)", 
    function (error, results, fields) {
      if (error) throw error;
 });
 // @@@@@ 테이블이 처음 생성되는 시점에 한번만 실행되도록 하기위함. cuz : 1행만 필요하기때문 @@@@@
 connection.query(`SELECT * FROM vote WHERE tournament = "final"`, function(error, results, fields) { 
   if (error) throw error;
   if (results.length === 0){
     connection.query(  
        INSERT INTO vote VALUES ("final", "0", "0")`,
        function (error, results, fields) {
          if (error) throw error;
      });
    }
  })

우선 webtoon이라는 DB를 생성하고 그 내부에 vote, user, nft 라는 테이블을 생성한다. 위의 코드는 그 내용중 vote와 관련된 내용이다. 초반 4줄은 테이블을 생성하는거지만 밑의 내용은 column 생성 내용이다. vote 테이블을 콘솔창에서 찍어보자.

table : vote

위를 보면 row 한 줄만 존재한다. 한 줄만 필요하기에 SELECT 문에서 얻은 results를 이용하여 (results.length === 0) 이라는 if 문을 만들었고 그 내부에서 INSERT INTO 가 실행되어 1 row 외에는 생성되지 않게 만들었다. 원하는 row 줄 수에 따라서 0이 아닌 다른 값을 넣으면 되겠다. 다른 누군가가 보면 쉬운 내용이겠지만, 처음에 원하는대로 안되서 INSERT INTO가 계속 들어가 DB가 흘러넘치는 사태가 발생했었다. 

 

server/services

sendToken.js : 서버계정에서 타 지갑으로 ERC20 토큰 전송

const contract = new web3.eth.Contract(Abi, contractHx);

const SendToken = async (to, amount) => {
  var transactionData = contract.methods.transfer(to, amount).encodeABI(); //Create the data for token transaction.
  var rawTransaction = {"to": contractHx, "gas": 100000, "data": transactionData }; 
  
  web3.eth.accounts.signTransaction(rawTransaction, process.env.SERVER_PRIVATE_KEY)
  .then(signedTx => web3.eth.sendSignedTransaction(signedTx.rawTransaction))
 }

 

토큰을 보내는 가장 기본적인 형태다.

 

  1. abi 파일로 만들어진 contract를 이용해서 3번라인에서 contract내의 transfer 함수를 호출하고 ABI 형태로 인코딩한다.
  2. 그 뒤 raw 한 transaction 을 직접 형성해준다.
  3. rawTransaction을 실제로 구동시키려면 private key가 필요하므로 이 두 개의 데이터를 signTransaction 메소드에 파라미터로 삽입하여 sign 을 진행한다.
  4. 마지막으로 sign 된 signedTx를 sendSignedTransaction을 이용해 보낸다.

여기서 궁금한 점이 생겼다.

var로 선언된 rawTransaction과 signedTx.rawTransaction이 어떤 차이점을 가지고 있을까?

 

콘솔로 찍어보니 차이점은 명확했다.

 

var로 직접 선언한 rawTransaction은 객체형식이지만,

sign된 rawTransaction은 해쉬화가 되어있었다. 

var rawTransaction = {"to": contractHx, "gas": 100000, "data": transactionData }; 

console.log(singnedTx.rawTransaction) 
=> result : 0xf8ad8...2f62 (352 글자수)

 

sendTokenU2S.js : 유저지갑에서 서버지갑으로 ERC20 토큰 전송

const SendTokenU2S = async (fromKey, to, amount) => {
  var transactionData = contract.methods.transfer(to, amount).encodeABI(); //Create the data for token transaction.
  var rawTransaction = {"to": contractHx, "gas": 100000, "data": transactionData }; 

  web3.eth.accounts.signTransaction(rawTransaction, fromKey)
  .then(signedTx => web3.eth.sendSignedTransaction(signedTx.rawTransaction))
}

직전의 sendToken과의 차이점은 서버계정에서 유저지갑으로 토큰을 전달하는 것이아닌, 유저 지갑에서 서버지갑을 포함한 다른지갑으로 토큰을 전송한다는 차이점을 가지고 있다.

그러한 이유로 현재 페이지에 로그인된 유저지갑의 privateKey인 fromKey를 가지고 sign을 하게 된다. 

 

server/services/tokenUse

Bet.js : 승리를 예상하는 팀에 배팅을 할 수 있음

SendTokenU2S(results[0].private_key, process.env.SERVER_ADDRESS, data.token_bet)
setTimeout(() => getTOKENBalanceOf(results[0].address)
.then(
	req => {
		const total_betted = parseInt(results[0].token_bet) + parseInt(data.token_bet);
        connection.query(`
        UPDATE user 
        SET token_bet="${total_betted}", token_amount="${req}" 
        WHERE user_id = "${data.user_id}"`
)}), 30) 
// 트랜잭션 컨펌 속도보다 저장속도가 빨라, 토큰수량 업데이트 전 값이 DB로 들어가버림. 
// 그래서 setTimeout 함수로 가나슈 컨펌속도 평균인 10ms를 고려해 조금 늦은시점에 업데이트 되도록 변경

원하는 상황이 나오지 않아 많이 고민했던 내용이다. 문제 상황은 다음과 같다. 

토큰을 보낸뒤에 잔액을 확인하고 그 내용을 DB에 최신 잔액을 업데이트 해야하는데, 트랜잭션이 들어간뒤 컨펌하는 사이에 DB에 먼저 저장되다보니 업데이트가 한템포씩 늦었던 상황이다.

 

생각나는 해결책은 두 가지가 있었다.

  1. await, async 를 이용하는 것
  2. 트랜잭션이 들어갈때까지 DB에 업데이트 하는 함수를 지연시키는 것

정말 안타깝게도 첫번째 해결책은 관련 지식 부족 이슈로 계속 실패했고, 결국 후자를 선택하게 되었다. 

 

가나슈내부에서 트랜잭션이 보내지고 컨펌되는 시간까지 평균 15ms 인것을 확인했고, 널널하게 30ms 이후에 함수를 실행하는 것으로 코드를 짜서 일단 문제는 해결되었다.

 

하지만 이건 이번 프로젝트에서만 먹히는 최악의 수였다. 왜냐하면 이 프로젝트 이후에도 가나슈를 계속 쓸 가능성은 현저히 낮고, 다른 네트워크를 사용한다면 시간대별 traffic 에 따라 컨펌되는 속도가 천차만별이기 때문이다.

 

 제대로 공부안한걸 반성하고, 주변에 물어보든 구글링을 하던해서 무조건 숙지해놓자.

 

 

Vote.js : 원하는 팀에 투표 및 투표집계를 위해 DB에 저장

if (results[0].isVoted === 0){

	@@@@@@@ Bet과 동일 @@@@@@@

}else{
    res.status(500).send(false);
}

connection.query(`SELECT * FROM vote WHERE tournament = "final"`, function(error, results, fields) { 
    if (error) throw error;

    if(data.choice === '1'){
      const votes = parseInt(results[0].num_one) + 1;
      console.log(votes);
      connection.query(`UPDATE vote SET num_one="${votes}" WHERE tournament = "final"`)
    }
    else if(data.choice === '2'){
      const votes = parseInt(results[0].num_two) + 1;
      console.log(votes);
      connection.query(`UPDATE vote SET num_two="${votes}" WHERE tournament = "final"`)
    }
})

Bet 과 코드는 거의 비슷하나 추가된 내용이 두 가지 있다. 

 

하나는 DB user 테이블의  isVoted가 0일 때만 send 토큰이 이뤄진다는 점이다.

isVoted는 현재 어느 팀에 투표했는지를 나타내는 내용이며 0은 아직선택안함, 1은 1팀, 2는 2팀이다. 

즉 아무것도 선택안한 0일때만 투표가 가능하고, 투표가 이뤄지면 선택한 팀으로 숫자가 업데이트 된다.

 

두번째는 투표가 이뤄질때마다 아래의 테이블 중 선택된 팀에 1씩 추가가 된다는 점이다.

table : vote

 

WinBet.js : 배팅 승리시 1.8배의 토큰을 지급, 무승부면 배팅 금액 그대로 반환, 패배시 토큰 소각

 

여기 내용도 Bet과 Vote와 많은 공통점을 가진다.

 

우선 user 테이블의 isVoted를 체크한다. 이때 투표한 팀과 배팅한 팀은 동일하도록 조건을 걸어 놨기때문에 isVoted를 투표에서 우승한 팀을 비교하여 배팅에서 이겼는지, 졌는지 무승부인지 판가름하고 토큰을 비율에 맞춰 전송한다는 점이 차이점이다.

 

client/pages

League.jsx : 웹툰 대항전 전체를 아우르는 페이지. 배팅 가능 시간이 나오며, 시간이 종료되면 자동으로 배팅 상금 전달

if(tournament === "2" && days===0 && hours===0 && minutes===0 && seconds===0){
  if(betEnd===false){
    setBetEnd(true);
    winner()
  }
}

투표 종료까지 주어진 날짜, 시, 분, 초가 useState로 실시간 업데이트가 되는데, 이 상태가 전부 0이면 투표가 종료되는 시점이므로 이걸 트리거삼아 배팅을 끝내고 자동으로 토큰이 전송되게 하였다.

 

client/pages/league_sub

RoundOf2.jsx : 웹툰 대항전 결승. 배팅과 투표 기능 포함

  const vote_first = () => {  // 1번에 투표
    const data = {
      user_id: userData.user_id,
      choice: '1'
    }
    axios.post('http://localhost:8080/user/vote', data)
    .then(function(res){
      alert("투표 완료! \n20 토큰이 부여되었습니다.")
    }).catch(function (error) {
      alert("투표는 한번만 가능합니다");
    });
  }

사실상 지금까지 백엔드에서 짜온 코드들이 실행되는 장소라 할 수 있는 코드다. 하지만 별다를 건 없다.

 

파일 하단 HTML 부분에는 투표와 배팅 버튼이 존재하고, 그 버튼들을 누르면 어디에 투표했는지, 어디에 배팅했는지를 서버쪽으로 보내주는 역할을 한다. 

 


추가적으로 배운 내용들 (중복 有)

  1.  res를 props로 넘기면 쿼리문에서 리턴대신 사용 가능하다
  2. 리덕스 사용방법
  3. jsx의 사용 이유 -> js 파일내에서 html 문법이 가능하다 (몰라서 똥꼬쇼했던 과거의 나. 반성해라)
  4. 가나슈 cli 사용시
    • -d : 동일한 지갑들로 유지
    • --port 7545 : cli는 default 값이 8545인데 이걸 변경할 수 있게 해줌
  5. HTML 작성시 맨 상단에 div를 써서 감싸자니 공간이 낭비되고, 그걸 안하자니 에러가 난다. 이때 필요한게 Fragment이다. html 내용들을 감싸주긴 하지만, 아무것도 없는 허상이라고 보면 된다 (import Fragment from "react" 를 하면 사용가능)
  6. dotenv 사용시 주의사항 (이건 정확한 내용이 아님. 그냥 한명의 경험에서 도출된 결과)
    • import dotenv 와 dotenv.config() 없이 env 내용이 호출되긴 한다.
    • process.env.[  ]을 함수 파라미터에 직접넣으면 실행은 된다.
    • 하지만 const transactionHx = process.env.TRANSACTION_HX 와 같이 재선언을 하게 되면 값이 undefined로 찍히더라.

 

'블록체인' 카테고리의 다른 글

[Solidity] Lazy Minting 코드 분석  (0) 2022.11.16
CORS / OPTIONS / preflight  (0) 2022.11.10
[코드리뷰] Project 1 - Opensea.io 클론코딩  (1) 2022.10.27
웹 개발  (0) 2022.08.31
HTTP, SSR/CSR  (0) 2022.08.08