세상을 바꿀 Web 3.0 - Web3.js 예제로 실습하기(2)

Jeenie·2022년 8월 23일
0

세상을 바꿀 web 3.0

목록 보기
4/7
post-thumbnail
post-custom-banner

이 포스트는 Cryptozombies의 레슨을 따라하며 정리한 글입니다.

이전 포스트에서는
web3.js로 프로젝트를 시작하고,
스마트 컨트랙트와 통신해 함수를 호출하고,
메타마스크에서 사용자 계정을 가져오는 것을 진행했다.

이번 포스트에서는 간편한 jQuery를 이용해서
스마트 컨트랙트에서 전달받은 데이터를 어떻게 파싱하고 표현하는지 실습해볼 것이다.

React를 사용한 web3-react는 이 다음 포스트에서 시작한다.

1. 좀비 군대 보여주기

  var cryptoZombies;
  var userAccount;

  function startApp() {
    var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
    cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
    
	var accountInterval = setInterval(function() {
      if(web3.eth.accounts[0] !== userAccount) {
        userAccount = web3.eth.accounts[0];
        getZombiesByOwner(userAccount)
          .then(displayZombies);
      }
    },100);
  }

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

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

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

  window.addEventListener('load', function() {
    if (typeof web3 !== 'undefined') {
      web3js = new Web3(web3.currentProvider);
    } else {
    }
    startApp()

  })

주기적으로 계정을 확인하는 setInterval 함수 안에서는
바뀐 계정이 현재 계정과 같지 않다면 계정을 바꿔주고,
유저의 좀비를 불러오는 함수 getZombiesByOwner에 해당 계정을 보내고
실행에 성공한다면 displayZombies 함수를 실행했다.

그럼 지난 포스트에서 만들지 않은 displayZombies 함수를 만들어보자.

  • 실행 결과값은 zombies ID 배열이어야 한다.
  • #zombies div 안에 내용이 있다면 비워서 새 좀비를 로딩하기 전에 기존 내용을 삭제한다.
  • 각 Zombies ID마다 getZombieDetail(id)를 호출해, 스마트컨트랙트에서 좀비의 정보를 찾는다.
  • 화면에 표시하기 위해 HTML 템플릿에 좀비에 대한 정보를 넣고, 템플릿을 #zombies에 넣는다.
  <body>
    <div id="zombies"></div>

    <script>
  var cryptoZombies;
  var userAccount;

  function startApp() {
    var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
    cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
    
	var accountInterval = setInterval(function() {
      if(web3.eth.accounts[0] !== userAccount) {
        userAccount = web3.eth.accounts[0];
        getZombiesByOwner(userAccount)
          .then(displayZombies);
      }
    },100);
  }

function displayZombies(ids) {
  $("zombies").empty();
  for(id of ids) {
    getZombieDetails(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>`);
    });
  }
}

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

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

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

  window.addEventListener('load', function() {
    if (typeof web3 !== 'undefined') {
      web3js = new Web3(web3.currentProvider);
    } else {
    }
    startApp()
  })

    </script>
  </body>

이제 우리 UI는 사용자의 메타마스크 계정을 감지하고,
자동으로 좀비 군대를 홈페이지에 표현할 수 있다.

2. 트랜잭션 보내기

이제 send 메소드로 스마트 컨트랙트의 데이터를 변경해보자.

send 함수는 실제로 call 함수와 다른 부분이 있다.

  • 트랜잭션을 전송(send)하려면 함수를 호출한 사람의 from 주소가 필요하다.
    (솔리디티 코드에서는 msg.sender)
    함수를 호출한 사람은 내 DApp의 사용자니까, 메타마스크가 열려서 유저에게 서명을 하게 하면 된다.

  • 트랜잭션 전송(send)는 가스를 소모한다.

  • 사용자가 트랜잭션을 전송(send)하고 실제로 블록체인에 적용될 때까지는 상당한 지연이 있다.
    이더리움의 평균 블록 시간이 15초이기 때문에, 트랜잭션이 블록에 포함될 때까지 기다려야한다.
    만약 이더리움에 보류 중인 거래가 많거나, 사용자가 가스 가격을 지나치게 낮게 보낼 경우는 우리 트랜잭션이 블록에 포함되길 몇분씩 기다려야할 수도 있다.

그러니 이 코드의 비동기적 특성을 다루기 위한 로직이 필요하다.

Q. 🤷🏻‍♀️ : 아니 몇분씩이라니 너무 불편하잖아! 왜이렇게 느린거야?
A. 맞아. 그래서 이더리움은 변하려고 하고있어! (이더리움 2.0 포스트)
블록체인의 속도 포스트도 참고하면 좋을 거야

2-1. 좀비 만들기

우선 사용자가 호출할 우리 컨트랙트 내의 createRandomZombie 함수를 살펴보자.

function createRandomZombie(string _name) public {
  require(ownerZombieCount[msg.sender] == 0);
  uint randDna = _generateRandomDna(_name);
  randDna = randDna - randDna % 100;
  _createZombie(_name, randDna);
}

트랜잭션을 보내서, 컨트랙트 안에 있는 이 함수를 호출해야한다.

컨트랙트 내부의 함수를 호출하기 위한 함수는

  • 시간이 걸릴테니, 트랜잭션이 보내졌다는 걸 유저가 알 수 있게 UI를 업데이트한다.
  • 우리 컨트랙트에 전송(send)하기
    - 트랜잭션 반영됨 ( getZombiesByOwner 호출해서 다시 그리기 )
    - 트랜잭션 반영 안됨 ( 사용자에게 트랜잭션 실패했다고 알려주기 )
function createRandomZombie(name) {
  $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
  return CryptoZombies.methods.createRandomZombie(name)
    .send({ from: userAccount })
    .on("receipt", function(receipt) {
    $("#txStatus").text("Successfully created " + name + "!");
    getZombiesByOwner(userAccount).then(displayZombies);
  })
    .on("error", function(error) {
    $("#txStatus").text(error);
  })
}

그리고 이 함수는, 우리의 Web3 프로바이더에게 트랜잭션을 전송(send)하고, 몇 가지 이벤트 리스너들을 연결한다.

  • receipt트랜잭션이 이더리움의 블록에 포함될 때, 즉 좀비 생성되고 우리의 컨트랙트에 저장되었을 때 발생한다.
  • error트랜잭션이 블럭에 포함되지 못했을 때, 예를 들어 사용자가 충분한 가스를 전송하지 않았을 때 발생한다.
    UI를 통해 사용자에게 트랜잭션이 전송되지 않았음을 알리고, 다시 시도할 수 있게 한다.

전송(send)를 호출할 때 gas와 gasPrice를 선택적으로 지정할 수 있다.

.send({ from: userAccount, gas: 3000000 })

지정하지 않으면 사용자들이 가스비를 선택할 수 있다.

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

    <script>
      var cryptoZombies;
      var userAccount;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

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

	  function displayZombies(ids) {
	    $("zombies").empty();
	    for(id of ids) {
 	     getZombieDetails(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>`);
         });
        }
      }

      function createRandomZombie(name) {
        $("#txStatus").text("Creating new zombie on the blockchain. This may take a while...");
        return CryptoZombies.methods.createRandomZombie(name)
        .send({ from: userAccount })
        .on("receipt", function(receipt) {
          $("#txStatus").text("Successfully created " + name + "!");
          getZombiesByOwner(userAccount).then(displayZombies);
        })
        .on("error", function(error) {
          $("#txStatus").text(error);
        })
      }

      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);
        })
      }


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

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

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

  window.addEventListener('load', function() {
    if (typeof web3 !== 'undefined') {
      web3js = new Web3(web3.currentProvider);
    } else {
    }
    startApp()
  })
    </script>
  </body>

createRandomZombie와 동일한 방식으로 feedOnKitty도 추가해주었다.

3. Payble 함수 호출하기

이제 Web3.js에서 특별한 처리가 필요한 payable 함수levelup 함수를 만들어보자.

3-1. levelup 함수 만들기

배포되어있는 스마트 컨트랙트 안에 있는 ZombieHelper 함수 내부에는,
사용자가 레벨업할 수 있는 곳에 payable 함수가 추가되어있다.

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

함수를 이용해 이더를 보내는 건 매우 간단하지만,
이더가 아니라 wei로 얼마를 보낼지 정해야하는 제한이 있다.

3-1-1. Payable 개념

Solidity는 가상화폐를 다루는 언어로 탄생했기 때문에 가상화폐로 접근하기 위한 키워드payable이 있다.

"이더리움 플랫폼" 위에서
"이더(ether) 코인을 전송"하는
스마트 컨트랙트(smart contract)
를 작성하기 위해서는
payable 키워드를 반드시 사용해야한다.

Payble을 작성한 함수에서만 이더(ether)를 보낼 수 있고,
payable작성하지 않은 함수에서는 이더(ether)를 보낼 수 없는 것.

스마트 컨트랙트 코드를 이더리움넷(mainnet/testnet)에 배포(deploy)했다면,
그리고 스마트 컨트랙트 외부에서 스마트 컨트랙트 내부에 있는 코인이동(전송) 함수를 사용하려 한다면,
해당 함수는 반드시 payable 키워드가 함께 작성된 함수여야한다.

address 타입과 같이 사용되면 지불가능한 주소(address payable) 의 타입이 되고,
function과 같이 사용하면 지불가능한 함수(payable function)가 된다.

Payable이 이 작업을 수행하고, 수정자(modifier) Payable이 있는 솔리디티의 모든 기능은 Ether를 보내고 받을 수 있도록 한다.
0이 아닌 Ether 값을 가진 트랜잭션은 처리하고, 0인 트랜잭션은 거부한다.
또한 지불가능한 키워드를 포함하지 않은 경우 트랜잭션은 자동으로 거부한다.

3-1-2. Wei란?

wei는 이더의 가장 작은 하위 단위로, 하나의 이더는 10^18 개의 wei이다.

Web3.js에는 wei를 변환해주는 변환 유틸리티가 있다.

web3js.utils.toWei("1");

컨트랙트에서 정의한 levelup 함수에서는 levelUpFee를 0.001 ether로 설정했다.
그래서 levelup 함수를 호출할 때, 아래처럼 사용자가 0.001 이더를 보내게 할 수 있다.

CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })

3-2. 따라하기

위에서 설명한 컨트랙트의 levelUp 함수를 호출하는 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);
    });
    // 좀비의 레벨만 변하기 때문에, 스마트컨트랙트에 요청하고 UI를 다시 그릴 필요 없다
  })
}

4. 이벤트(Event) 구독하기

보다시피 Web3.js를 통해 컨트랙트와 상호작용하는 것은 간단하다.
한번 컨트랙트를 작성하고 나면, 내부함수를 호출하거나 트랜잭션을 전송하는 것은 일반적인 웹 API와 다르지 않다.

그럼 이제, 컨트랙트에서 이벤트를 구독해보자.

4-1. 새로운 좀비 수신하기

배포되어있는 컨트랙트 중 zombiefactory.sol 파일에는, 새로운 좀비가 생성될 대마다 매번 호출되던 NewZombie라는 이벤트가 있다.

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

Web3.js에서는 이벤트를 구독하면,
해당 이벤트 발생시마다 Web3 프로바이더가 내 코드 내의 어떤 로직을 실행시킬 수 있다.

cryptoZombies.eventes.NewZombie()
.on("data", function(event) {
  let zombie = event.returnValues;
  // `event.returnValue` 객체에서 이 이벤트의 세가지 반환 값에 접근할 수 있다.
  console.log("새로운 좀비가 태어났습니다!", zombie.zombieId, zombie.name, zombie.dna);
})
.on("error", console.error);

하지만 이 로직은, DApp에서 어떤 사용자의 좀비가 태어나든지 항상 알림을 보낸다.

현재 사용자의 좀비가 새로 태어났을 때만 알림을 보내게 하려면 어떻게 해야할까?

4-2. indexed 사용하기

이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해서는 indexed를 사용해야한다!

배포되어있는 컨트랙트 안에 구현되어있는 Transfer 이벤트의 파라미터에는 indexed 키워드가 사용되어 있다.

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

fromtoindexed 되어 있기 때문에, 우리 프론트엔드의 이벤트 리스너에서 이들을 필터링할 수 있다.

indexed 키워드

솔리디티 컨트랙트 이벤트의 키워드.
우리의 정보가 들어있는 특정 이벤트만 가져오기 위해서 사용한다.
블록들 안에 출력된 이벤트들을 필터링하여 내가 원하는 특정 이벤트만 가져올 수 있다.
필터링하려고 하는 파라미터 값에 indexed만 써주면 된다.

// `filter`를 사용해 `_to`가 `userAccount`와 같을 때만 코드를 실행
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  // 이때의 event는 생성된 좀비의 정보 객체.
  let data = event.returnValues;
  // 현재 사용자가 방금 좀비를 받았다!
  // 해당 좀비를 보여줄 수 있도록 UI 업데이트 처리
}).on("error", console.error);

4-3. 지난 이벤트에 대해 요청하기

getPastEvents를 이용해 지난 이벤트들에 대해 요청을 하고,
fromBlocktoBlock 필터를 이용해 이벤트 로그에 대한 "시간범위"를 솔리디티에 전달할 수 있다.

getPastEvents 함수

: 토큰의 모든 트랜스퍼 이벤트를 조회할 수 있다.
MyEvent: 특정 이벤트 유형 내에서 필터를 지정할 수 있다 (매개변수 값으로 필터링할 수 있음)
getPastEvents : 이벤트 유형에 대한 모든 이벤트를 반환한다.

fromBlock과 toBlock

: 이때의 block은 이더리움 블록의 번호를 말한다.
시작블록끝블록을 보낼 수 있기 때문에 시간범위를 전달할 수 있는 것

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

getPastEvents 메소드를 사용하면 시작시간부터의 이벤트 로그들에 대해 요청을 할 수 있다.

따라서, 이벤트를 저렴한 storage로 사용할 수 있다 ! 📦📦📦

저렴한 storage?

말 그대로, local storage / Async storage처럼 사용할 수 있는 것.
"데이터를 블록체인에 기록하는 것" 은 솔리디티에서 가장 비싼 비용을 지불하는 작업 중 하나다.
데이터를 블록체인에 비싸게 기록하는 대신 이벤트를 이용하면 가스 측면에서 훨씬 저렴하다.

( 예시 ) 좀비 전투 기록용
좀비가 다른 좀비를 공격할 때마다, 한 좀비가 이길 때마다 이벤트를 생성할 수 있다.
스마트 컨트랙트가 결과를 계산할 때 필요하지 않지만, 사용자들은 앱의 프론트엔드에서 찾아볼 수 있게 된다.

4-4. 따라하기

해당 유저의 Transfer 이벤트를 감지하는 코드를 만들어보자!
그리고 현재 사용자가 새로운 좀비를 받았을 때 앱의 UI를 업데이트시켜야한다.

cryptoZombies.events.Transfer를 수신하는 코드는 startApp() 함수의 끝부분에 추가되어야 한다.
이벤트 리스너를 추가하기 전에 cryptoZombies 컨트랙트가 확실히 초기화될 수 있도록 하기 위해서!

function startApp() {
  var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
  cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);

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

  cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
  .on("data", function(event) {
    let data = event.returnValues;
    getZombiesByOwner(userAccount)
    .then(displayZombies);
  }).on("error", console.error);
}

5. 마무리하기

이것으로 스마트 컨트랙트와 상호작용 하는 첫 번째 Web3.js 프론트엔드를 완료했다!

크립토좀비 프로젝트에서는, 스마트 컨트랙트와 상호작용할 때 필요한 핵심 로직만을 사용했다.
매우 기초적인 부분만 다뤄서 전혀 어렵지 않았고, 재밌게 따라했다.

간단하지만 전체적인 개념을 이해하기 정말 좋은 프로젝트라고 생각한다.

(물론 Solidity에 대한 이해 부족이나, 설명없이 지나가는 용어들이 있어 내가 따로 정리를 한 부분도 있지만)

다음에 진행할 web3-react는 조금 더 실무적인 부분에 집중해서 구현해봐야겠다.

profile
Web Front-end developer
post-custom-banner

0개의 댓글