[Solidity] 크립토좀비 레슨6 학습리뷰

드림보이즈·2023년 2월 28일

목표 : 앱 프론트엔드 & Web3.js


챕터 1 : Web3.js

이더리움 노드는 JSON-RPC 언어로 서로 소통

{"jsonrpc":"2.0","method":"eth_sendTransaction","params":[{"from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155","to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","gas":"0x76c0","gasPrice":"0x9184e72a000","value":"0x9184e72a","data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675"}],"id":1}

만약 스마트 컨트랙트 함수를 실행하고 싶다면, 노드를 선택하고

  • 스마트 컨트랙트 주소
  • 실행하고자 하는 함수
  • 함수에 전달하고자 하는 변수들

Web3.js를 이용하면 편리하고 쉽게 읽을 수 있는 JS 인터페이스로 상호작용 가능

CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
  .send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })

패키지 설치 or HTML에 스크립트 집어넣기

<script language="javascript" type="text/javascript" src="web3.min.js"></script>

Q. index.html을 만들고 web3.js 집어넣어라

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <!-- 여기에 web3.js를 포함하게. -->
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
  </head>
  <body>

  </body>
</html>

챕터 2 : Web3 Provider

어떤 노드와 통신할지 설정해야 되니까

Infura

빠른 읽기를 위한 캐시 계층을 포함하는 다수 이더리움 노드 운영 서비스

접근을 위한 API를 무료로 사용 가능

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

그러나

단순히 '읽기'(접근)만 하는 것이 아닌 '쓰기'도 할 것이기에,

사용자들이 개인키로 서명을 할 수 있게 해야겠지

앱 프론트엔드에서 개인키를 관리? 되겠냐?

이를 처리해주는 서비스가 있지

메타마스크

이더리움 계정과 개인키를 안전하게 관리해주는 브라우저 확장 프로그램

이 계정을 써서 Web3.js를 이용하는 웹사이트들과 상호작용할 수 있도록 한다.

사용자가 웹 브라우저를 써서 웹사이트를 통해 Dapp과 상호작용한다면, 필요!

(메타마스크는 내부적으로 Infura 서버를 Web3 provider로 사용, 하지만 그들만의 provider를 선택할 수 있는 옵션도 있음)

메타마스크의 Web3 provider 사용하기

web3 라는 전역 자바스크립트 객체를 통해 브라우저에 프로바이더를 주입

즉, 앱에서 web3가 있는지 확인만 하고 있다면 web3.currentProvider를 사용하면 됨


window.addEventListener('load', function() {
  	// Web3가 브라우저에 주입되었는지 확인
  	if(typeof web3 !== 'undefined') {
      	//Mist/MetaMask 프로바이더 사용
      	web3js = new Web3(web3.currentProvider);
    } else {
      //사용자가 Metamask를 설치하지 않은 경우에 처리
      // 사용자에게 설치하라는 메세지 보여줄 것
    }
  
  //이제 앱 실행하고 web3에 자유롭게 접근가능
  startApp()
})  
  

챕터 3 : 컨트랙트와 대화하기

Web3.js는 스마트컨트랙트와 통신 위해 2가지 필요

  • 컨트랙트 주소
  • ABI

컨트랙트 ABI

기본적으로 JSON 형태로 컨트랙트의 메서드를 표현하는 것

컨트랙트가 이해할 수 있도록 Web3.js가 어떤 형태로 함수호출을 해야 하는지 알려주는 것

Web3.js 컨트랙트 인스턴스화 하기

// myContract 인스턴스화
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

Q. 컨트랙트 인스턴스화 하라

 // 2. 여기서 코딩을 시작하게
      var cryptoZombies;
      function startApp() {
        var CryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = web3js.eth.Contract(cryptozombiesABI, cryptoZombiesAddress);
      }

챕터 4 : 컨트랙트 함수 호출하기

web3.js는 컨트랙트 함수 호출하기위해 call / send 메서드 사용

call

view, pure 함수 위해 사용, 트랜잭션 만들지 않음

가스도 안 쓰고, 서명할 필요도 없음

myContract.methods.myMethod(123).call()

send

트랜잭션을 만들고 블록체인 상의 데이터를 변경

view, pure가 아닌 모든 함수에 대해 send 사용해야

가스 지불, 메타마스크에서 서명하라고 할 거임

(메타마스크로 하니까 다 알아서 해주고 ㅈㄴ 편하네?)

myContract.methods.myMethod(123).send()

getter 함수

솔리디티에서 public으로 변수를 선언하면 자동으로

같은 이름의 "getter"함수를 만들어 놓는다고 했었다.

Zombie[] public zombies;

라고 한다면

zombies(id)로 찾을 수 있다는 뜻이다

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

// 함수를 호출하고 결과를 가지고 무언가를 처리:
getZombieDetails(15)
.then(function(result) {
  console.log("Zombie 15: " + JSON.stringify(result));
});

비동기적 Promise를 반환

result는 이런식

{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}

Q. getZombieDetails, zombieToOwner, getZombiesByOwner 함수 만들어라


function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

function zombieToOwner(id) {
  return cryptoZombies.methods.zombieToOwner(id).call()
}

function getZombiesByOwner(owner) {
  return cryptoZombies.methods.getZombies.ByOwner(owner).call()
}

챕터 5 : 메타마스크 & 계정

만약 홈페이지에 사용자의 전체 좀비 군대를 보여주고 싶다면?

getZombiesByOwner(owner)를 사용하면 될텐데,

여기서 owner는 솔리디티 주소일텐데, 어떻게 사용자의 주소를 알아낼까?

메타마스크에서 사용자 계정 가져오기

메타마스크에서는 주입된 web3 변수 안에 '현재 활성화된 계정'이 뭔지

var userAccount = web3.eth.accounts[0]

로 알아낼 수 있음

사용자가 메타마스크에서 활성화된 계정을 바꿀 수 있기 때문에, 계속 감시하고 업데이트를 해줘야 한다.

그래서 setInterval로 0.1초마다 현재 활성화 계정을 확인하고, 다르다면 업데이트 하는 식으로 할 수 있겠지.

var accountInterval = setInterval(function() {
	// 계정 바뀌었는지 확인
    if (web3.eth.accounts[0] !== userAccount) {
    	userAccount = web3.eth.accounts[0];
        //새 계정에 대한 UI로 업데이트 위한 함수호출
        updateInterface();
        }
},100);        
        
        

Q. 위의 코드 복붙하고, updateInterface() 대신 getZombiesByOwner 넣고 then displayZombies 넣어라

var userAccount;

var accountInterval = setInterval(function() {
         
          if (web3.eth.accounts[0] !== userAccount) {
            userAccount = web3.eth.accounts[0];
           
            getZombiesByOwner(userAccount)
            .then(displayZombies);
          }
        }, 100);

챕터6 : 좀비 군대 보여주기

리액트, 뷰 : 여기서 다루지 않음

jQuery로 데이터를 파싱하고 표현하자

좀비 보여주기

이미

<div id="zombies"></div>

존재

getZombies

  • div 비워주고
  • for문을 돌려 id 당 정보를 붙여넣기

좀비 스프라이트는 어떻게 표현?

받은 DNA 문자열을 이미지로 어떻게 바꿀까?

DNA 문자열을 부분 문자열로 나누고, 모든 2자리 숫자를 이미지에 대응시켜 작업을 했었다.

// 좀비 머리 표현하는 1-7 정수 얻기
var head = parseInt(zombie.dna.substring(0,2)) % 7 + 1

// 순차적인 파일 이름으로 7개의 머리 이미지
var headSrc = "../assets/zombieparts/head-" + head + ".png"

Q. displayZombies 구현해라

function displayZombies(ids) {
  $("#zombies").empty();
  for (id of ids) {
    	getZombieDetail(id)
    .then(function(zombie) {
          $("#zombies").append(`<div class="zombie">
              <ul>
                <li>Name: ${zombie.name}</li>
                <li>DNA: ${zombie.dna}</li>
                <li>Level: ${zombie.level}</li>
                <li>Wins: ${zombie.winCount}</li>
                <li>Losses: ${zombie.lossCount}</li>
                <li>Ready Time: ${zombie.readyTime}</li>
              </ul>
            </div>`);
        });
  }
}

챕터 7: 트랜잭션 보내기

send

호출한 사람 주소 필요, 서명하라고 할거고, 가스 소모

트랜잭션 전송하고 적용될 때 까지 시간이 오래걸릴 거임

좀비 생성

Web3.js에서 호출하기

function createRandomZombie(name) {
  // 시간이 걸리니, 유저가 알 수 있도록 UI 업뎃
  $("#txStatus").text("Creating new Zombie on the block chain. 기다려줘");
  
  // 컨트랙트에 전송
  return CryptoZombies.methods.createRandomZombie(name)
  .send({from:userAccount})
  .on("receipt", function(receipt) {
    	$("#txStatus").text("성공적으로" + name + "생성 했습니다!");
    
    // UI 다시 그려야지
    getZombiesByOwner(userAccount).then(displayZombies);
    .on("error", function(error) {
      	//실패했음을 알려주기
      $("#txStatus").text(error);
    });
  }    
  

receipt는 트랜잭션이 이더리움 블록에 포함될 때 발생
error는 블럭에 포함되지 못했을 때,

Q. createRandomZombie, feedOnKitty 구현하라

function feedOnKitty(zombieId, kittyId) {
       
       
        $("#txStatus").text("Eating a kitty. This may take a while...");
       
        return CryptoZombies.methods.feedOnKitty(zombieId, kittyId)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Ate a kitty and spawned a new Zombie!");
         
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
         
          $("#txStatus").text(error);
        });
      }

챕터 8 : Payable 함수 호출

레벨업 함수에서 payable 함수를 사용했었지

function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}

Wei

wei는 이더의 가장 작은 단위
( 1 eth = 10^18 wei)

// 1 이더를 웨이로 변환
web3js.utils.toWei("1");
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })

이렇게 레벨업 함수 쓸 때 사용자가 0.001 이더 보내게 할 수 있음

Q. levelUp 함수를 만들어라

function levelUp(zombieId) {
        $("#txStatus").text("좀비를 레벨업하는 중...");
        return CryptoZombies.methods.levelUp(zombieId)
        .send({ from: userAccount, value: web3js.utils.toWei("0.001") })
        .on("receipt", function(receipt) {
          $("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다.");
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        });
      }

챕터 9 : 이벤트 구독하기

새로운 좀비 수신하기

event NewZombie(uint zombieId, string name, uint dna);

web3.js에서 이벤트를 구독하여 해당 이벤트가 발생할 때 마다 프로바이더가 코드 내의 어떠한 로직을 실행시킬 수 있도록 가능

cryptoZombies.events.NewZombie()
.on("data", function(event) {
	let zombie = event.returnValues;
    // 'event.returnValues' 객체에서 이 이벤트의 세 가지 반환 값에 접근 가능
    console.log("새로운 좀비 생성!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);

그러나

이러면 모든 좀비가 태어날 때 마다 모든 사용자에게 알림이 가겠지, "현재 사용자"가 만든 것에 대한 알람만 보내고 싶은데?

indexed

이벤트 필터링하고, 현재 사용자와 연관된 변경만을 수신하기 위해, Transfer 이벤트처럼 indexed를 사용해야됨

event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);

_from, _to가 indexed되어 있기 때문에, 프론트의 이벤트리스터에서 필터링 가능

// filiter로 _to가 userAccount와 같을 때만 코드 실행
cryptoZombies.events.Transfer({ filter: {_to: userAccount})
.on("data", function(event) {
	let data = event.returnValues;
    // 여기서 UI 업데이트
}).on("error", console.error);   

지난 이벤트에 대해 질의

getPastEvents를 이요해 지난 이벤트들에 질의하고, fromBlock toBlock 필터를 이용해 이벤트 로그에 대한 시간 범위를 이용 가능

cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
  // `events`는 우리가 위에서 했던 것처럼 반복 접근할 `event` 객체들의 배열이네.
  // 이 코드는 생성된 모든 좀비의 목록을 우리가 받을 수 있게 할 것이네.
});

이벤트를 저렴한 형태의 storage로 사용 가능

대신 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다.

히스토리로 블록체인에 기록하여 앱의 프론트에서 읽기를 원하는 데이터가 있다면, 아주 중요한 사용 예시

좀비 공격, 누가 이길 떄 마다 이벤트를 생성한다면,
스마트 컨트랙트는 추후 결과를 계산할 때 이 데이터가 필요하지 않지만, 사용자들이 앱의 프론트엔드에서 찾아볼 수 있는 유용한 데이터

Q. Transfer 이벤트 감지하는 코드를 startApp 안에 추가하자

cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
        .on("data", function(event) {
          let data = event.returnValues;
          getZombiesByOwner(userAccount).then(displayZombies);
        }).on("error", console.error);
profile
시리즈 클릭하셔서 카테고리 별로 편하게 보세용

0개의 댓글