Basic - 앱 프론트엔드 & Web3.js

EllaDev·2022년 4월 7일
0

CrytoZombie

목록 보기
6/6

해당 글은 CrytoZombie를 공부하면서 정리한 내용입니다.

CrytoZombie

Ch1. Web3.js

사용자들이 상호작용 할 수 있는 기본적인 웹 페이지를 만들것이다.
이를 만들기 위해, 우리는 이더리움 재단에서 만든 자바스크립트 라이브러리인 Web3.js를 사용한다.

Web3.js 란?

이더리움 네트워크는 노드로 구성되어 있고, 각 노드는 블록체인의 복사본을 가지고 있다. 스마트 컨트랙트의 함수를 실행하고자 한다면, 이 노드들 중 하나에 질의를 보내 아래 내용을 전달해야 한다.
1. 스마트 컨트랙트의 주소
2. 실행하고자 하는 함수
3. 그 함수에 전달하고자 하는 변수들
이더리움 노드들은 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는 이런 골치 아픈 질의를 아닌 편리하고 쉽게 읽을 수 있는 자바스크립트 인터페이스로 상호작용을 할 수 있게 한다.

위의 질의문을 작성할 필요 없이, 자네의 코드에서 함수를 아래와 같이 호출하는 것이다.

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

먼저 프로젝트에 Web3.js를 설정하도록 해야한다.

= 프로젝트에서 가장 많이 사용하는 패키지 도구를 써서 Web3.js를 추가

// NPM을 사용할 때
npm install web3

// Yarn을 사용할 때
yarn add web3

// Bower를 사용할 때
bower install web3

= 단순히 github에서 간략화된 .js 파일을 다운로드하거나 CDN 코드를 사용한다.

<!-- web3.min.js 파일을 프로젝트에 추가 -->
<script language="javascript" type="text/javascript" src="web3.min.js"></script>

<!-- CDN을 프로젝트에 추가 | -->
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>

<!-- CDN을 프로젝트에 추가 | unpkg -->
<script src="https://unpkg.com/web3@latest/dist/web3.min.js"></script>

Ch2. Web3 Provider

이 단계에서는 Web3 프로바이더로 Web3.js를 초기화하는 단계이다.
Web3.js에서 Web3 프로바이더를 설정하는 것은 우리 코드에 읽기와 쓰기를 처리하려면 어떤 노드와 통신을 해야 하는지 설정하는 것이다. 이는 전통적인 웹 앱에서 API 호출을 위해 원격 웹 서버의 URL을 설정하는 것과 같다.

나만의 이더리움 노드를 프로바이더로 운영할 수도 있다. 하지만 편리하게 쓸 수 있는 제3자 서비스가 있다. DApp의 사용자들을 위해 이더리움 노드를 운영할 필요가 없이 사용할 수 있는 서비스이다.(Infura)

Infura

Infura는 빠른 읽기를 위한 캐시 계층을 포함하는 다수의 이더리움 노드를 운영하는 서비스이다. 접근을 위한 API를 무료로 사용할 수 있다. Infura를 프로바이더로 사용하면, 나만의 이더리움을 설치하고 계속 유지할 필요 없이 이더리움 블록체인과 메세지를 확실히 주고받을 수 있다.

다음과 같이 Web3에 Web3 프로바이더로 Infura를 쓰도록 설정할 수 있다:

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

하지만, 많은 사용자들이 우리의 DApp을 사용할 것이기에(이 사용자들은 단순히 읽기만 하는 게 아니라 블록체인에 뭔가 쓰기도 할 것이기에) 우리는 이 사용자들이 그들의 개인 키로 트랜잭션에 서명을 할 수 있도록 해야 할 것이다.

*참고: 이더리움(그리고 일반적으로 블록체인)은 트랜잭션에 전자 서명을 하기 위해 공개/개인 키 쌍을 사용한다. 말하자면 전자 서명을 위해 엄청나게 안전한 비밀번호 같은 것이다. 이런 방식으로 내가 만약 블록체인에서 어떤 데이터를 변경하면, 나의 공개 키를 통해 내가 거기 서명을 한 사람이라고 증명할 수 있다. 하지만 아무도 내 개인 키를 모르기 때문에, 내 트랜잭션을 누구도 위조할 수 없다.

앱 프론트엔드에서 사용자들의 개인 키를 관리하려 하는 것은 아마 좋은 생각이 아니다. 이를 대신 처리해주는 서비스가 많은데 이중 가장 유명한 것은 메타마스크(Metamask)이다.

메타마스크

  • 메타마스크: 사용자들이 이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는 크롬과 파이어폭스의 브라우저 확장 프로그램
  • 해당 계정들을 써서 Web3.js를 사용하는 웹사이트들과 상호작용을 할 수 있도록 한다.

*참고: 메타마스크는 내부적으로 Infura의 서버를 Web3 프로바이더로 사용한다. 하지만 사용자들에게 그들만의 Web3 프로바이더를 선택할 수 있는 옵션을 주기도 한다. 즉 메타마스크의 Web3 프로바이더를 사용하면, 사용자에게 선택권을 주는 것이기도 하면서 앱에서 걱정할 거리를 하나 줄일 수 있다.

메타마스크의 Web3 프로바이더 사용

메타마스크는 web3라는 전역 자바스크립트 객체를 통해 브라우저에 Web3 프로바이더를 주입한다.
앱에서는 web3가 존재하는지 확인하고, 만약 존재한다면 web3.currentProvider를 프로바이더로서 사용하면 되다.

= 템플릿 코드

// 사용자가 메타마스크를 설치했는지 확인하고 설치가 안 된 경우 우리 앱을 사용하려면 메타마스크를 설치해야 한다고 알려주는 것

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

  // 이제 자네 앱을 시작하고 web3에 자유롭게 접근할 수 있네:
  startApp()

})

*참고: 메타마스크 말고도 사용자들이 쓸 수 있는 다른 개인 키 관리 프로그램도 있다.(ex - 미스트(Mist) 하지만, 그것들도 모두 web3 변수를 주입하는 동일한 형태를 사용한다.


Ch3. 컨트랙트와 대화

이 단계는 스마트 컨트랙트와 통신하는 단계이다.
Web3.js는 스마트 컨트랙트와 통신을 위해 2가지를 필요로 하다(컨트랙트의 주소와 ABI)

컨트랙트 주소

스마트 컨트랙트를 모두 작성한 후에는 그걸 컴파일한 후 이더리움에 배포할 것이다.
컨트랙트를 배포한 후, 해당 컨트랙트는 영원히 존재하는, 이더리움 상에서 고정된 주소를 얻는다. 레슨2를 상기해보면, 이더리움 메인넷에서 크립토키티의 주소는 "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d"였다.
우리의 스마트 컨트랙트와 통신을 하기 위해 배포 후 이 주소를 복사해야 할 것이다.

컨트랙트 ABI

ABI는 Application Binary Interface의 약자로 기본적으로 JSON 형태로 컨트랙트의 메소드를 표현하는 것이다. 우리 컨트랙트가 이해할 수 있도록 하려면 Web3.js가 어떤 형태로 함수 호출을 해야 하는지 알려주는 것이다.

이더리움에 배포하기 위해 컨트랙트를 컴파일할 때, 솔리디티 컴파일러가 자네에게 ABI를 줄 것이다. 그러니 컨트랙트 주소와 함께 이를 복사하여 저장해야 한다.

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

컨트랙트의 주소와 ABI를 얻고 나면, Web3에서 인스턴스화 할 수 있다.

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

Ch4. 컨트랙트 함수 호출

Web3.js는 컨트랙트의 함수를 호출하기 위해 우리가 사용할 두 개의 메소드를 가진다.(call과 send)

Call

call은 view와 pure 함수를 위해 사용한다. 로컬 노드에서만 실행하고, 블록체인에 트랜잭션을 만들지 않는다.

*복습: view와 pure 함수는 읽기 전용이고 블록체인에서 상태를 변경하지 않는다. 가스를 전혀 소모하지 않고, 메타마스크에서 트랜잭션에 서명하라고 사용자에게 창을 띄우지도 않는다.

// 123을 매개 변수로 myMethod라는 이름의 함수를 call한다.
myContract.methods.myMethod(123).call()

Send

send는 트랜잭션을 만들고 블록체인 상의 데이터를 변경한다. view와 pure가 아닌 모든 함수에 대해 send를 사용해야 한다.

*참고: 트랜잭션을 send하는 것은 사용자에게 가스를 지불하도록 하고, 메타마스크에서 트랜잭션에 서명하라고 창을 띄울 것이다. Web3 프로바이더로 메타마스크를 사용할 때, send()를 호출하면 자동으로 이 모든 것이 이루어지고, 우리의 코드에 어떤 특별한 것도 추가할 필요가 없다.

// 123을 매개 변수로 myMethod라는 이름의 함수를 호출하는 트랜잭션을 send할 수 있다
myContract.methods.myMethod(123).send()

예제

이제 컨트랙트에서 데이터에 접근하기 위해 call을 사용하는 실제 예제를 보자.

// 좀비 배열을 public으로 만듬
Zombiep[] public zombies;

솔리디티에서, public으로 변수를 선언하면 자동으로 같은 이름의 퍼블릭 "getter" 함수를 만들어 진다.
ID 15인 좀비를 찾는다면, zombies(15)와 같이 변수를 함수인 것처럼 호출할 수 있다.

아래 코드는 프론트엔드에서 좀비 ID를 받아 해당 좀비에 대해 컨트랙트에 질의를 보내고, 결과를 반환하는 자바스크립트 함수를 작성하는 방법이다.

*참고: 아래 사용하는 모든 코드 예제들은 콜백 대신 Promise를 사용하는 Web3.js 1.0 버전을 사용하고 있다. 온라인에서 찾을 수 있는 다른 많은 코드들은 Web3.js의 이전 버전을 사용하고 있다. 1.0 버전에서 문법이 많이 바뀌었으니 코드가 다르다면 버전을 참고 해야한다.

//Web3 프로바이더와 통신하여 컨트랙트의 Zombie[] public zombies에서 인덱스가 id인 좀비를 반환하도록 한다.
function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}

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

// console result 값 
/*
{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}
*/
  • 컨트랙트 통신은 비동기적으로 일어나고 Web3는 Promise를 반환한다.

Ch5. 메타마스크 & 계정

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

주입되어 있는 web3 변수에 현재 활성화된 계정이 무엇인지 아래와 같이 확인할 수 있다.

var userAccount = web3.eth.accounts[0]

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

사용자가 언제든지 메타마스크에서 활성화된 계정을 바꿀 수 있기 때문에, 앱은 이 변수의 값이 바뀌었는지 확인하기 위해 계속 감시를 하고 값이 바뀌면 그에 따라 UI를 업데이트해야 할 것한다.
위 코드는 userAccount가 여전히 web3.eth.accounts[0]과 같은지 100밀리초마다 확인하고 있다.
그렇지 않다면, userAccount에 현재 활성화된 계정을 다시 할당하고, 화면을 업데이트하기 위한 함수를 호출한다.


Ch6. 데이터 보여주기

startApp() 내에서 getZombiesByOwner의 호출 결과를 써서 호출한 displayZombies의 결과로 좀비 ID 배열([0, 13, 47])을 전달받을 수 있다.

= displayZombies 함수 다음 기능을 한다,
1. 먼저 이미 무언가가 #zombies div의 안에 들어 있다면 이 div의 내용을 비운다. 이것은 사용자가 그들의 활성화된 MetaMask 계정을 변경하면 새로운 좀비 군대를 로딩하기 전에 기존의 것을 삭제하는 것과 같다.
2. 반복을 통해 각 id마다 getZombieDetails(id)를 호출해서 우리의 스마트 컨트랙트에서 좀비에 대한 모든 정보를 찾는다.
3. 화면에 표시하기 위해 HTML 템플릿에 좀비에 대한 정보를 집어넣고, 해당 템플릿을 #zombies div에 붙여넣는다.

// 우리 컨트랙트에서 좀비 상세 정보를 찾아, `zombie` 객체 반환
getZombieDetails(id)
.then(function(zombie) {
  // HTML에 변수를 넣기 위해 ES6의 "template literal" 사용
  // 각각을 #zombies div에 붙여넣기
  $("#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>`);
});

Ch7. 트랜젝션

send 함수를 이용해 스마트 컨트랙트의 데이터를 변경하는 방법
1. 트랜잭션을 전송(send)하려면 함수를 호출한 사람(from) 주소가 필요하다.(솔리디티 코드에서는 msg.sender와 같음). 이는 DApp의 사용자가 되어야 하고 메타마스크가 나타나 그들에게 서명을 하도록 한다.
2. 트랜잭션 전송(send)은 가스를 소모한다.
3. 사용자가 트랜잭션 전송을 하고 난 후 실제로 블록체인에 적용될 때까지는 상당한 지연이 발생할 것이다. 트랜잭션이 블록에 포함될 때까지 기다려야 하는데, 이더리움의 평균 블록 시간이 15초이기 때문이지. 만약 이더리움에 보류 중인 거래가 많거나 사용자가 가스 가격을 지나치게 낮게 보낼 경우, 우리 트랜잭션이 블록에 포함되길 기다려야 하고, 이는 몇 분씩 걸릴 수 있다.

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

좀비 만들기

= 솔리디티 코드

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

= web3에서 위의 함수를 호출하는 코드

function createRandomZombie(name) {
  // 시간이 꽤 걸릴 수 있으니, 트랜잭션이 보내졌다는 것을
  // 유저가 알 수 있도록 UI를 업데이트해야 함
  $("#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 + "!");
    // 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함
    getZombiesByOwner(userAccount).then(displayZombies);
  })
  .on("error", function(error) {
    // 사용자들에게 트랜잭션이 실패했음을 알려주기 위한 처리
    $("#txStatus").text(error);
  });
}

= Web3 프로바이더에게 트랜잭션을 전송(send) 후, 이벤트 리스너 연결

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

*참고: 자네가 send를 호출할 때 gas와 gasPrice를 선택적으로 지정할 수 있다. (.send({ from: userAccount, gas: 3000000 })). 만약 지정하지 않는다면, 메타마스크는 사용자가 이 값들을 선택할 수 있도록 한다.


Ch8. Payable 함수 호출

= 솔리디티 코드(ZombieHelper.sol)

function levelUp(uint _zombieId) external payable {
  require(msg.value == levelUpFee);
  zombies[_zombieId].level++;
}
  • 이더가 아니라 wei로 얼마를 보낼지 정해야 한다.

Wei란?

  • wei : 이더의 가장 작은 하위 단위이다.(1 eth = 10^18 wei)
// web3 변환 유틸리티
web3js.utils.toWei("1"); // 1 ETH를 Wei로 변경

// levelUp함수 호출
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })  // levelUpFee = 0.001 ether로 설정

Ch9. Event 구독

새로운 좀비 수선

zombiefactory.sol에서 새로운 좀비가 생성될 때마다 매번 호출되던 NewZombie라는 이벤트가 있다.
event NewZombie(uint zombieId, string name, uint dna);

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

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

DApp에서 현재 사용자의 좀비만이 아니라 어떤 좀비가 생성되든지 항상 알림을 보낼 것이다.
현재 사용자가 만든 것에 대해서만 알림을 보내고 싶다면 어떻게 해야 할까?

Indexed

이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해, ERC721을 구현할 때 Transfer 이벤트에서 했던 것처럼 솔리디티 컨트랙트에 indexed 키워드를 사용해야 한다.

// 솔리디티 소스
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
// _from과 _to가 indexed 되어 있기 때문에, 프론트엔드의 이벤트 리스너에서 이들을 필터링할 수 있다.

//  프론트
// `filter`를 사용해 `_to`가 `userAccount`와 같을 때만 코드를 실행
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
  let data = event.returnValues;
  // 현재 사용자가 방금 좀비를 받았네!
}).on("error", console.error);

event와 indexed 영역을 사용하는 것은 컨트랙트에서 변화를 감지하고 프론트엔드에 반영할 수 있게 하는 유용한 방법이다.

지난 이벤트에 대해 질의

getPastEvents를 이용해 지난 이벤트들에 대해 질의를 하고, fromBlocktoBlock 필터들을 이용해 이벤트 로그에 대한 시간 범위를 솔리디티에 전달할 수 있다.(여기서 "block"은 이더리움 블록 번호를 나타낸다).

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

위 메소드를 사용해서 시작 시간부터의 이벤트 로그들에 대해 질의를 할 수 있기 때문에, 이를 통해 이벤트를 저렴한 형태의 storage로 사용하는 것 예시를 만들 수 있다.

다시 생각해보면, 데이터를 블록체인에 기록하는 것은 솔리디티에서 가장 비싼 비용을 지불하는 작업 중 하나이다. 하지만 이벤트를 이용하는 것은 가스 측면에서 훨씬 더 저렴하다.

여기서 단점이 되는 부분은 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다는 것이다. 하지만 히스토리로 블록체인에 기록하여 앱의 프론트엔드에서 읽기를 원하는 데이터가 있다면, 이는 새겨놓아야 할 중요한 사용 예시이다.

예를 들어, 우린 이것을 좀비 전투의 히스토리 기록용으로 사용할 수 있다. 좀비가 다른 좀비를 공격할 때마다, 그리고 누군가 이길 때마다 우린 이벤트를 생성할 수 있다. 스마트 컨트랙트는 추후 결과를 계산할 때 이 데이터가 필요하지 않지만, 사용자들이 앱의 프론트엔드에서 찾아볼 수 있는 유용한 데이터이다.


전체 소스

<!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>
    <script language="javascript" type="text/javascript" src="web3.min.js"></script>
    <script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
  </head>
  <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];
           
            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);
      }

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

좀더 생각해볼 것들
1. attack, changeName, changeDna, 그리고 ERC721 함수인 transfer, ownerOf, balanceOf 함수를 구현한다. 이런 함수들의 구현은 우리가 다룬 모든 다른 send 트랜잭션과 동일하다.

  1. setKittyContractAddress, setLevelUpFee, 그리고 withdraw를 실행할 수 있는 "관리 페이지"를 구현한다. 이러한 구현들은 우리가 이미 다룬 함수들과 동일할 것이다. 그저 해당 컨트랙트를 배포했던 이더리움 주소에서 이 함수들을 호출했는지 확인하면 된다. 이 함수들은 onlyOwner 제어자를 가지고 있다.

  2. 이 앱에서 구현하고 싶은 다른 몇 가지 화면이 있다.

  • 개별 좀비 페이지: 특정 좀비에 대한 영구적인 링크를 통해 그 좀비의 정보를 볼 수 있는 곳이다. 이 페이지에서는 좀비의 외관과 이름, 주인(사용자 프로필 페이지에 대한 링크와 함께), 승리/패배 횟수, 전투 기록, 기타 등등을 보여줄 것이다.
  • 사용자 페이지: 영구적인 링크를 통해 사용자의 좀비 군대를 볼 수 있는 곳이다. 개별 좀비를 클릭하여 해당 페이지를 볼 수 있을 것이고, 자네가 메타마스크에 로그인되어 있고, 군대를 가지고 있다면 좀비를 클릭해 공격할 수도 있을 것이다.
  • 홈페이지: 현재 사용자의 좀비 군대를 볼 수 있는, 사용자 페이지의 한 종류이다(우리가 index.html에서 구현학 시작했던 곳이다).
  1. UI 상에서 사용자가 크립토키티를 먹이로 줄 수 있는 방법이 있어야 한다. 홈페이지에서 각 좀비 옆에 "먹이 주기" 같은 버튼을 만들고, 사용자가 고양이의 ID를 입력하게 하는 텍스트 박스를 만들 수 있다.(또는 그 고양이의 URL, 예를 들면: https://www.cryptokitties.co/kitty/578397). 이 버튼은 feedOnKitty 함수를 호출할 것이네.

  2. UI 상에서 한 사용자가 다른 사용자의 좀비를 공격할 수 있는 방법이 있어야 한다.
    이를 구현하는 하나의 방법은 한 사용자가 다른 사용자의 페이지로 들어가면, "이 좀비 공격하기" 버튼을 보여주는 것이다. 사용자가 그 버튼을 클릭하면, 현재 사용자의 좀비 군대를 포함하는 모달 창을 띄우고 "어떤 좀비로 공격하시겠습니까?" 메세지를 보여주면 된다.
    또 사용자의 홈페이지에서 각 좀비 옆에 "좀비 공격하기" 버튼을 둘 수도 있다. 사용자가 그걸 클릭하면, 사용자가 좀비의 ID를 입력하여 찾을 수 있는 찾기 영역을 가지는 모달 창을 띄울 수 있다. 또는 "아무 좀비나 공격하기" 같은 옵션을 줘서 임의로 찾을 수도 있을 것이다.
    그리고 쿨다운 기간이 아직 다 지나지 않은 사용자의 좀비는 회색 처리를 할 수도 있다. UI 상에서 사용자에게 해당 좀비로는 아직 공격할 수 없고 얼마나 더 기다려야 하는지 보여줘야한다.

  3. 사용자의 홈페이지에는 각 좀비의 이름 또는 DNA를 바꾸고, 일정 비용을 내고 레벨업을 할 수 있는 옵션이 있을 수 있다. 사용자의 레벨이 충분하지 않으면 어떤 옵션들을 회색 처리를 할 수 있다.

  4. 새로운 사용자들을 위해, createRandomZombie()를 호출해 군대의 첫 번째 좀비를 만들 수 있는 입력 창과 함께 환영 메세지를 보여줄 수 있다.

  5. 마지막 챕터에서 논의한 것처럼, 우리 스마트 컨트랙트에 indexed 프로퍼티로 사용자의 address를 가지는 Attack 이벤트를 추가하고 싶을 수 있다. 이를 통해 실시간 알림을 만들 수 있을 것이다 - 사용자에게 그의 좀비가 공격당하면 알림 창을 띄워 알려주어, 그를 공격한 사용자/좀비를 보여주고 복수할 수 있게 하는 것이다.

  6. 또한 일종의 프론트엔드 캐시 계층을 구현하여 똑같은 데이터를 위해 Infura에 계속 접근하지는 않도록 하고 싶을 수 있다(우리의 현재 displayZombies 구현은 인터페이스를 새로고침할 때마다 각 좀비에 대해 getZombieDetails를 호출하지 - 하지만 현실적으로 우리의 군대에 추가된 새 좀비에 대해서만 이 함수를 호출하면 된다).

10.실시간 채팅방을 만들기.


내가 받은 좀비들

profile
Frontend Developer

0개의 댓글