web3.js 때문에 머리아픈지 꽤 오랜 시간이 지났는데도 web3.js는 여전히 나를 당황시킨다.
알꺼 같으면서도 모르겠고 알았던 것도 모르겠고 모르겠던건 더 모르겠고 울고 싶을 때가 한두번이 아니다!!!!!!(유리멘탈...)
도움이 될만한 인강이나 유튜브 영상 또는 도서를 찾아봤지만 내가 무능력한건지 이것도 찾다가 지침... ㅋㅋ
그러다가 우연치 않게 접하게 된 CryptoZombies.
https://cryptozombies.io/ko/course
100% 내가 원하는 내용과 흡사하다고는 말할 수 없지만 꽤나 도움이 되는 듯 하다.
커리큘럼은 Solidity, Advanced Solidity, Chainlink, Beyond Etheruem 이렇게 4가지로 나뉘어 있는데 Solidity가 Beginner to Intermediate Smart Contract라고 써져 있었다. 그래서 Solidity로 시작!!
6개의 코스가 준비되어 있었다. (나중에 보니까 오픈 안된 코스가 몇개 있더라..)
6번 코스에 앱 프론트엔드 & Web3.js라고 써있길래 무식하고 용감하게 시작을 해봤더니 역시 눈물이 났다.(나만 그런가...?)
그래서 1번 코스에서 부터 시작!!
1번 코스는 솔리디티에 대한 기본적인 내용을 알 수 있었다. 문법과 개념 정리를 할 수 있었는데 생각보다 재미있더라. 신나게 1번 코스를 끝냈다. (물론, 난 천재가 아니어서 전부 다 기억하진 못함.. solidity로 전향해??)
근데 내가 컨트랙트 짤꺼 아닌데.. 라는 생각이 강하게 들면서 2번 코스를 시작했는데 ㅋㅋ 머리에 안들어오더라...
그래서 1시간 정도 시간 낭비를 하다가~ 다시 6번 코스를 도전하게 됐다. 그랬더니 어랏? 아까 눈물나던 부분이 무슨 내용인지 얼추 알겠는거?? 와하하하하 신나~ React로 개발하는 거였으면 더 좋았을 텐데 이 튜토리얼은 이더리움과 스마트 컨트랙트에만 초첨을 맞추기 위해 jQuery로 진행된다고 6번 코스 중간 쯤에 친절하게 써있었다. 뭐~ 나중에 다시 해보믄 되지(아직 안해봐서 자신있음)
cryptozombie github에 코드 확인하면 방금 말한 코스는 lesson으로 폴더별로 나뉘어 있으며 lesson-6가 내가 진행한 6번 코스임을 참고하시길...
web3.js는 스마트 컨트랙트와 통신할 수 있는 이더리움 재단에서 만든 자바스크립트 라이브러리이다.
이더리움 네트워크는 노드로 구성되어 있고, 각 노드는 블록체인의 복사본을 가지고 있다. 스마트 컨트랙트의 함수를 실행하기 위해서는 아래의 정보를 노드에 보내야 한다.
- 스마트 컨트랙트의 주소
- 실행하고자 하는 함수
- 그 함수에 전달하고자 하는 변수들
이더리움 노드들은 JSON-RPC라고 불리는 언어로만 소통할 수 있고 이 언어는 사람이 사용하기 어려우므로 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 활용
CryptoZombies.methods.createRandomZombie("Vitalik Nakamoto 🤔")
.send({ from: "0xb60e8dd61c5d32be8058bb8eb970870f07233155", gas: "3000000" })
1. web3.js 설정하기
npm install web3
yarn add web3
둘 중 사용하는 패키지 도구를 써서 web3를 프로젝트에 추가한다.
web3 github을 확인하면 CDN 또는 UNPKG를 사용하는 방법도 있으니 자신에게 맞는 방법으로 사용하면 된다.
예제 진행에서는 스크립트 태그가 통째로 주어져서 그대로 복사, 붙여넣기를 하면 됐다.
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
</head>
<body>
</body>
</html>
2. Web3 프로바이더(Provider)
이더리움은 똑같은 데이터의 복사본을 공유하는 노드들로 구성되어 있다.
Web3 프로바이더를 설정하는 것은 우리 코드에서 읽기와 쓰기를 처리하려면 어떤 노드와 통신해야 하는지 설정하는 것이다. 전통적인 웹 앱에서 API 호출을 위해 원격 웹 서버의 URL을 설정하는 것과 같다.
메타마스크(Metamask)
메타마스크는 사용자들이 이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주면 크롬과 파이어폭스의 브라우저 확장 프로그램으로 해당 계정들을 써서 Web3.js를 사용하는 웹사이트들과 상호작용을 할 수 있도록 해준다.
메타마스크는 web3라는 전역 자바스크립트 객체를 통해 브라우저에 Web3 프로바이더를 주입한다. 그로므로 앱이 Web3의 존재 여부를 확인하고, 만약 존재한다면 web3.currentProvider를 프로바이더로 사용하면 된다.
아래의 템플릿 코드는 메타마스크를 설치했는지 여부를 확인하고, 만약 미설치 시 메타마스크를 설치해야 한다고 알려주는 코드이다.
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
</head>
<body>
<script>
window.addEventListener('load', function() {
// Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Mist/MetaMask의 프로바이더 사용
web3js = new Web3(web3.currentProvider);
} else {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
// 이제 앱을 시작하고 web3에 자유롭게 접근할 수 있다
startApp()
})
</script>
</body>
</html>
3. 컨트랙트와 통신하기
앞서 메타마스크의 Web3 프로바이더로 Web3.js를 초기화했다. 이제 스마트 컨트랙트와 통신하기 위해서는 아래의 두가지 정보가 필요하다.
- 컨트랙트 주소
- 컨트랙트 ABI
컨트랙트 주소
스마트 컨트랙트를 모두 작성한 후 컴파일해 이더리움에 배포한다. 우선 배포에 대한 자세한 내용은 다음 기회가 된다면 설명하도록 하고 컨트랙트에 대해 계속 설명하자면, 배포된 컨트랙트는 이더리움 상에서 유니크하고 수정 불가한 주소를 갖게된다.
테스트 진행 시 필요한 메인넷에서의 크립토키티 주소 : 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d
lesson 2에서 나왔던 내용이라고 적혀있었지만 나는 ㅋㅋ 모르므로 패쓰~!!
무튼 스마트 컨트랙트랑 통신하기 위해서는 배포 후 위의 주소가 필요하다고 했다.
컨트랙트 ABI
ABI는 JSON 형태로 컨트랙트의 메소드를 표현한다. 컨트랙트가 이를 이해할 수 있게 하려면 Web3.js가 어떤 형태로 함수를 호출해야 하는지 알려준다.
이더리움에 배포하기 위해 컨트랙트를 컴파일 할 때 솔리디티 컴파일러가 ABI를 생성해준다.
원활한 학습 진행을 위해 cryptozombies_abi.js라는 파일의 cryptoZombiesABI 변수에 ABI를 저장해주었고 내용은 아래와 같다.
아래의 더보기 클릭 시 ABI 전체 확인 가능 👇
/* cryptozombies_abi.js */
var cryptoZombiesABI = [
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
}
],
"name": "levelUp",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_kittyId",
"type": "uint256"
}
],
"name": "feedOnKitty",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "zombies",
"outputs": [
{
"name": "name",
"type": "string"
},
{
"name": "dna",
"type": "uint256"
},
{
"name": "level",
"type": "uint32"
},
{
"name": "readyTime",
"type": "uint32"
},
{
"name": "winCount",
"type": "uint16"
},
{
"name": "lossCount",
"type": "uint16"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "getZombiesByOwner",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "zombieToOwner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_address",
"type": "address"
}
],
"name": "setKittyContractAddress",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_newDna",
"type": "uint256"
}
],
"name": "changeDna",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"name": "_owner",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "_balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_name",
"type": "string"
}
],
"name": "createRandomZombie",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "getAllZombies",
"outputs": [
{
"name": "",
"type": "uint256[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_tokenId",
"type": "uint256"
}
],
"name": "takeOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_newName",
"type": "string"
}
],
"name": "changeName",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_fee",
"type": "uint256"
}
],
"name": "setLevelUpFee",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_zombieId",
"type": "uint256"
},
{
"name": "_targetId",
"type": "uint256"
}
],
"name": "attack",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_from",
"type": "address"
},
{
"indexed": true,
"name": "_to",
"type": "address"
},
{
"indexed": false,
"name": "_tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_owner",
"type": "address"
},
{
"indexed": true,
"name": "_approved",
"type": "address"
},
{
"indexed": false,
"name": "_tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "attackResult",
"type": "bool"
},
{
"indexed": false,
"name": "winCount",
"type": "uint16"
},
{
"indexed": false,
"name": "lossCount",
"type": "uint16"
}
],
"name": "AttackResult",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"name": "zombieId",
"type": "uint256"
},
{
"indexed": false,
"name": "name",
"type": "string"
},
{
"indexed": false,
"name": "dna",
"type": "uint256"
}
],
"name": "NewZombie",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
}
]
cryptoZombies_abi.js를 프로젝트에 포함시켜 변수를 사용하여 ABI에 접근할 수 있다.
4. Web3.js 컨트랙트 인스턴스화 하기
컨트랙트의 주소 + ABI를 얻고 나면 Web3에서 인스턴스화를 할 수 있다.
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<script>
// 추후 인스턴스화된 컨트랙트를 저장하는데 사용할 예정
var cryptoZombies;
function startApp() {
// 메인넷에서 크립토좀비의 컨트렉트 주소임
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
// 컨트랙트 인스턴스화
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}
window.addEventListener('load', function() {
// Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Mist/MetaMask의 프로바이더 사용
web3js = new Web3(web3.currentProvider);
} else {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
// 이제 앱을 시작하고 web3에 자유롭게 접근할 수 있다
startApp()
})
</script>
</body>
</html>
5. 컨트랙트 함수 호출하기
컨트랙트의 설정이 끝났으므로 Web3.js로 컨트랙트와 통신을 할 수 있게 되었다.
Web3.js는 컨트랙트 함수를 호출하기 위해 call과 send를 가지고 있다.
1) Call
call은 읽기 전용으로 view, pure 함수를 위해 사용한다.
로컬 노드에서만 실행하고, 트랜잭션을 만들지 않는다.(블록체인 상의 상태를 변경하지 않음)
가스비를 소모하지 않음.
// 123을 매개변수로 myMethod라는 이름의 함수를 call 함
myContract.methods.myMethod(123).call()
2) Send
view와 send를 제외한 모든 함수에 대해 사용한다.
트랜잭션을 만들고 블록체인 상의 데이터를 변경한다.
가스비를 지불해야 하며 메타마스크에서 트랜잭션에 서명하라고 창을 띄운다.
구문은 Call과 동일하다.
좀비 데이터 받기
솔리디티에서 public으로 선언한 변수를 함수처럼 호출할 수 있다.
* public으로 선언한 변수는 자동으로 같은 이름의 퍼블리 "getter"함수를 생성하므로 변수를 함수인 것처럼 호출할 수 있다.
프론트엔드에서 좀비 ID를 받아 해당 좀비에 대한 컨트랙트를 호출해서 결과를 반환 하는 자바스크립트 함수를 작성한다.
* web3.js 1.0 버전 사용으로 모든 예제는 콜백 대신 Promise를 사용함
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<script>
var cryptoZombies;
function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}
function getZombieDetails(id) {
// Web3 프로바이더와 통신하여 컨트랙트의 Zombie[] public zombies에서 인덱스가 id인 좀비를 반환하도록 한다.
return cryptoZombies.methods.zombies(id).call();
}
// id를 매개변수로 받아 ZombieFactory.sol에서의 매핑 zombieToOwner에 대한 call을 환
// mapping (uint => address) public zombieToOwner;
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call();
}
// owner를 ZombieHelper.sol의 함수 getZombiesByOwner에 대한 call을 반환
// function getZombiesByOwner(address _owner)
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call();
}
window.addEventListener('load', function() {
if (typeof web3 !== 'undefined') {
web3js = new Web3(web3.currentProvider);
} else {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
getDetailZombie의 result로 아라왜 같은 자바스크립트 객체가 생성된다.
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
이렇게 스마트 컨트랙트와 상호 작용을 위한 코드가 작성되었다.
6. 메타마스크 & 계정
이제 각 요소들을 하나로 합쳐 나가보도록 하자. 서비스 전체 사용자의 좀비 군대를 보여주기 위해서는 사용자가 가지고 있는 모든 좀비들의 id를 찾기 위해 첫번째로 getZombiesByOwner(owner)함수를 활용한다.
여기서 솔리디티의 컨트랙트는 owner에 솔리디티 address를 보내도록 되어있다는 문제가 발생한다.
메타마스크에서 사용자 계정 가져오기
메타마스크는 확장 프로그램 안에서 사용자들이 다수의 계정을 관리해줄 수 있도록 해준다. 다음으로 web3 변수에 현재 활성화된 계정이 무엇인지 확인할 수 있다.
var userAccount = web3.eth.accounts[0]
사용자가 언제든지 메타마스크에서 활성화된 계정을 바꿀 수 있기 때문에 이 변수의 값이 바뀌었는지 확인하기 위해 계속 감시하고 값이 바뀌면 그에 따라 UI가 업데이트 되도록 해야한다.
이를 위해 다음과 같이 setInterval을 쓸 수 있다.
var accountInterval = setInterval(function() {
// 계정이 바뀌었는지 확인
if(web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
updateInterface();
}
}, 100);
userAccount가 web3.eth.account[0]과 같은지를 100밀리세컨즈마다 확인하고 다른 경우, 활성화된 계정을 다시 할당하고 화면을 업데이트하기 위한 updateInterface()함수를 호출한다.
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<body>
<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로 업데이트하기 위한 함수 호출
// zombiehelper.sol에서 getZombieByOwner로 전체 좀비 보여줌
getZombiesByOwner(userAccount)
// displayZombie는 다음 스텝에서 구현 예정
.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 {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
7. 좀비 군대 보여주기(displayZombies 함수 만들기)
좀비 데이터 보여주기 - 예제
좀비 데이터를 보여주기 위한 <div id="zombies"></div>를 생성하고 displayZombies 함수를 생성한다.
displayZombies 함수는
- #zombies는 이미 무언가 내용이 있을 경우, div의 내용을 비운다.(새로운 좀비 군대를 로딩하기 전 기존 것 삭제)
- for 구문을 통해 id 마다 getZombieDetails(id)를 호출해서 컨트랙트에서 좀비에 대한 모든 정보를 찾음.
- 화면에 표기하시 위해 HTML 템플릿에 좀비에 대한 정보를 집어넣고 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>`);
});
앞서 만들었던 getZombiesByOwner의 호출한 결과값으로 호출한 displayZombies의 결과값으로 좀비 ID 배열을 전달 받는다. (말이 좀.. 이상한데... 무튼..)
좀비 스프라이트 표현
갑자기 스프라이트는 무슨 흐름인지 번역본으로 봐서 그런가...잘 모르겠으나... 앞서 좀비 데이터 예제에서 DNA를 문자열로 간단히 표현하였는데, DApp에서 이것을 이미지로 바꾸려고 한다.
DNA 문자열을 부분 문자열로 나누고, 모든 2자리 숫자를 이미지에 대응 시켜 아래와 같이 처리하였다.
// 좀비 머리를 표현하는 1~7의 정수 얻기
var head = parseInt(zombie.dna.substring(0,2)) % 7 + 1
// 순차적인 파일 이름으로 7개의 머리 이미지를 생성함.
var headSrc = "../assets/zombieparts/head-" + head + ".png"
CSS의 절대 좌표 포지셔닝을 이용해 각 컴포넌트는 다른 이미지 위에 위치하도록 한다.
여기서 Vue.js 컴포넌트 소스를 제공한다. 이것도 시간날 때 보는 것으로~ ^^;;;;;;
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
</head>
<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에 접근
for (id of ids) {
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>`);
});
}
}
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 {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
8. 트랜잭션 보내기
지금까지 사용자의 메타마스크 계정을 감지하고 자동으로 UI를 홈페이지 표현하는 작업을 마쳤다.
이제 send함수를 이용해 컨트랙트 데이터를 변경하도록 하자.
* 주의사항
send()를 하려면 함수를 호출한 사람의 from 주소(DApp 사용자 주소)가 필요하다(솔리디티 코드에서의 msg.sender).
send()는 가스비가 발생한다.
트랜잭션을 전송하고 실제 블록체인에 적용될때까지 이더리움 평균 시간은 15초이고 만약 이더리움에서 보류중인 거래가 많거나 사용자가 가스비를 지나치게 낮게 보내면 더 오래 걸릴 수도 있다.
그러므로 비동기적 특정을 다루기 위한 로직이 필요하다.
좀비 만들기
아래의 코드는 컨트랙트(솔리티디 코드)의 createRandomZombie이다.
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
메타마스크를 사용해 web3.js에서 위의 컨트랙트 함수를 호출해본다.
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) {
$("#txStatues").text("Successfully created " + name + "!");
// 블록체인에 트랜잭션이 반영되었으므로 UI를 다시 그려야 함
getZombiesByOwner(userAccount).then(displayZombies);
})
.on("error", function(error) {
//사용자들에게 실패함을 알려주기 위한 처리
$("#txStatus").text(error);
});
}
- receipt - 트랜잭션이 이더리움 블록에 포함될 때 발생함.
- error - 트랜잭션이 블록에 포함되지 못했을 때, 발생함.
참고!! send 호출 시 gas와 gasPrice를 선택적으로 지정할 수 있음
.send({ from: userAccount, gas: 3000000 })
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/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);
}
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) {
// 시간이 꽤 걸릴 수 있으니, 트랜잭션이 보내졌다는 것을
// 유저가 알 수 있도록 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);
});
}
function feedOnKitty(zombieId, kittyId) {
// 유저가 알 수 있도록 UI를 업데이트해야 함
$("#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!");
// 블록체인에 트랜잭션이 반영되었으며, UI를 다시 그려야 함
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 {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
9. payable 함수 호출하기
attack, chageName, changDna의 로직은 매우 비슷할 것이고 이를 구현하는 것은 단순할 것이므로 생략하도록 한다.
Web3.js에서 특별한 처리가 필요한 payable함수를 알아보도록 하자!
레벨업
ZombieHelper를 살펴보면 사용자가 레벨업할 수 있는 곳에 payable 함수가 추가되어 있다.
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
함수를 이용해 이더를 보내는 방법은 간단하지만 이더가 아니라 wei로 얼마나 보낼지를 정해야 하는 제한이 있다.
Wei는 이더의 가장 작은 하위 단위로 1ETH = 10^18wei이다.
Web3.js에는 이더를 wei로 바꿔주는 유틸리티가 있다.
// 1ETH를 wei로 바꿈
web3js.utils.toWei("1");
levelUp 함수를 호출할 때 0.001 ETH를 보내도록 설정해놨었다.
cryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/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);
}
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: web3js.utils.toWei("0.001" })
.on("receipt", function(receipt) {
$("#txStatus").text("압도적인 힘! 좀비가 성공적으로 레벨업했습니다.");
// 좀비의 레벨만 변경되므로 UI를 다시 그릴 필요가 없다.
})
.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 {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
9. 이벤트(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.returnValue;
// 'event.returnValue' 객체에서 이벤트의 세가지 값에 접근할 수 있다.
console.log("새로운 좀비가 태어났습니다!", zombie.zombieId, zombie.name, zombie.dna);
}).on("error", console.error);
이 로직은 현재 사용자의 좀비 뿐 아니라 어떤 좀비가 생성되던지 항상 알림을 보내는 것이다. 그럼 현재 사용자가 만든 것만 알림을 보내고 싶다면??
indexed 사용하기
이벤트를 필터링하고 현재 사용자와 연관된 변경만을 수신하기 위해 ERC721을 구현할 때 Transfer 이벤트에서 했던 것처럼 솔리디티 컨트랙트에서 indexed 키워드를 사용해야 한다.
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
indexed 된 _from과 _to는 filter를 사용할 수 있다.
// 'filter'를 사용해 '_to'가 'userAccount'와 같을 때만 코드 실행
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
// 현재 사용자가 방금 좀비를 받음
// 해당 좀비를 보여줄 수 있도록 UI 업데이트
}).on("error", console.error);
event와 indexed는 컨트랙트의 변화를 감지하고 프론트엔드에 반영할 수 있는 유용한 방법이다.
지난 이벤트 호출
getPastEvents를 이용해 지난 이벤트를 호출하고 fromBlock과 toBlock필터들을 이용해 이벤트 로그에 대한 시간 범위를 솔리디티에 전달한다.
cryptoZombie.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
.then(function(events) {
// 'events'는 반복접근할 'event' 객체들의 배열
// 생성된 모든 좀비의 목록을 받을 수 있도록 할 예정
});
getPastEvents를 통해 이벤트를 저렴한 형태의 storage로 사용하는 것이다.
데이터를 블록체인에 기록하는 것은 솔리디티에서 가장 비싼 비용을 지불하는 작업 중 하나이다. 하지만 이벤트를 이용하는 것은 가스 측면의 비용을 절감할 수 있으므로 저렴하다고 표현하였다.
단, 단점으로 스마트 컨트랙트 자체 안에서는 이벤트를 읽을 수 없다. 히스토리로 블록체인에 기록하여 원할 때 이벤트를 읽을 수 있도록 하는 방법으로 보완할 수 있다.
<!-- index.html -->
<!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 src="https://cdn.jsdelivr.net/npm/web3@latest/dist/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);
// filter를 사용해 _to가 userAccount와 같을 때만 코드 실행
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: web3js.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 {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
startApp()
})
</script>
</body>
</html>
여기까지가 cryptozombies.io의 lesson 6로 진행된 내용이다. 앞서 lesson 1~5에서 이미 작성되었던 solidity에 대한 내용이 지속적으로 언급되기 때문에 아마도 웹 프론트엔드와 관련된 web3.js만 따로 공부하긴 힘들 것 같다.
마지막으로 반복적이며 기본적인 것들이기 때문에 이렇게 마무리하며 스스로 코드를 작성해볼 수 있도록 체크리스트를 공유해줬다.(고..오맙습니다~) 체크리스트는 그냥 긁어와서 그대로의 어투가 남아있다 ㅋㅋ 뭔 컨셉인지 ㅋㅋㅋ
이 내용을 벌써 두번째 정독하고 있지만 아직도 100% 흡수되었다고 느껴지지 않는다. 아래의 체크리스트는 그래서 훗날을 기약해야 할 것 같다.
- attack, changeName, changeDna, 그리고 ERC721 함수인 transfer, ownerOf, balanceOf 함수를 구현하게. 이런 함수들의 구현은 우리가 다룬 모든 다른 send 트랜잭션과 동일할 것이네.
- 자네가 setKittyContractAddress, setLevelUpFee, 그리고 withdraw를 실행할 수 있는 "관리 페이지"를 구현하게. 다시 한번 말하지만, 프론트엔드에 더 특별한 로직은 없네 - 이러한 구현들은 우리가 이미 다룬 함수들과 동일할 것이네. 자네는 그저 해당 컨트랙트를 배포했던 이더리움 주소에서 이 함수들을 호출했는지 확인하면 되네. 이 함수들은 onlyOwner 제어자를 가지고 있으니 말이야.
- 이 앱에서 구현하고 싶은 다른 몇 가지 화면이 있을 수 있네:b. 사용자 페이지: 영구적인 링크를 통해 사용자의 좀비 군대를 볼 수 있는 곳이지. 개별 좀비를 클릭하여 해당 페이지를 볼 수 있을 것이고, 자네가 메타마스크에 로그인되어 있고, 군대를 가지고 있다면 좀비를 클릭해 공격할 수도 있을 것이네.
- c. 홈페이지: 현재 사용자의 좀비 군대를 볼 수 있는, 사용자 페이지의 한 종류이지(우리가 index.html에서 구현학 시작헀던 곳이네).
- a. 개별 좀비 페이지: 특정 좀비에 대한 영구적인 링크를 통해 그 좀비의 정보를 볼 수 있는 곳이지. 이 페이지에서는 좀비의 외관과 이름, 주인(사용자 프로필 페이지에 대한 링크와 함께), 승리/패배 횟수, 전투 기록, 기타 등등을 보여줄 것이네.
- UI 상에서 사용자가 크립토키티를 먹이로 줄 수 있는 방법이 있어야 하겠지. 홈페이지에서 각 좀비 옆에 "먹이 주기" 같은 버튼을 만들고, 사용자가 고양이의 ID를 입력하게 하는 텍스트 박스를 만들 수 있겠지(또는 그 고양이의 URL, 예를 들면: https://www.cryptokitties.co/kitty/578397). 이 버튼은 feedOnKitty 함수를 호출할 것이네.
- UI 상에서 한 사용자가 다른 사용자의 좀비를 공격할 수 있는 방법이 있어야 할 것이네.또 사용자의 홈페이지에서 각 좀비 옆에 "좀비 공격하기" 버튼을 둘 수도 있네. 사용자가 그걸 클릭하면, 사용자가 좀비의 ID를 입력하여 찾을 수 있는 찾기 영역을 가지는 모달 창을 띄울 수 있겠지. 또는 "아무 좀비나 공격하기" 같은 옵션을 줘서 임의로 찾을 수도 있을 것이네.
- 그리고 쿨다운 기간이 아직 다 지나지 않은 사용자의 좀비는 회색 처리를 할 수도 있겠지. UI 상에서 사용자에게 해당 좀비로는 아직 공격할 수 없고 얼마나 더 기다려야 하는지 보여줄 수 있도록 말이야.
- 이를 구현하는 하나의 방법은 한 사용자가 다른 사용자의 페이지로 들어가면, "이 좀비 공격하기" 버튼을 보여주는 것이네. 사용자가 그 버튼을 클릭하면, 현재 사용자의 좀비 군대를 포함하는 모달 창을 띄우고 "어떤 좀비로 공격하시겠습니까?" 메세지를 보여주면 되네.
- 사용자의 홈페이지에는 각 좀비의 이름 또는 DNA를 바꾸고, 일정 비용을 내고 레벨업을 할 수 있는 옵션이 있을 수 있네. 사용자의 레벨이 충분하지 않으면 어떤 옵션들을 회색 처리를 할 수 있곘지.
- 새로운 사용자들을 위해, createRandomZombie()를 호출해 군대의 첫 번째 좀비를 만들 수 있는 입력 창과 함께 환영 메세지를 보여줄 수 있네.
- 마지막 챕터에서 논의한 것처럼, 우리 스마트 컨트랙트에 indexed 프로퍼티로 사용자의 address를 가지는 Attack 이벤트를 추가하고 싶을 수 있네. 이를 통해 실시간 알림을 만들 수 있을 것이네 - 사용자에게 그의 좀비가 공격당하면 알림 창을 띄워 알려주어, 그를 공격한 사용자/좀비를 보여주고 복수할 수 있게 하는 것이지.
- 또한 일종의 프론트엔드 캐시 계층을 구현하여 똑같은 데이터를 위해 Infura에 계속 접근하지는 않도록 하고 싶을 수 있네(우리의 현재 displayZombies 구현은 인터페이스를 새로고침할 때마다 각 좀비에 대해 getZombieDetails를 호출하지 - 하지만 현실적으로 우리의 군대에 추가된 새 좀비에 대해서만 이 함수를 호출하면 되네).
- 실시간 채팅방을 만들어 자네가 다른 사용자들의 군대를 부술 때마다 그 사용자를 도발할 수 있도록 하는 것? 제발 만들어 주게...
너무너무 긴 글을 혹시나 읽어주신 분들이 있다면 정말정말 감사드립니다.
하지만, 앞서 언급했듯이 solidity에 대한 내용을 전부 다 생략한채로 설명한 거라 아마도 이 글만 읽는 사람들은 내용이 쉽사리 와닿지 않으리라 생각된다.
부디 시간을 투자해 모든 코스를 완료해보시기를 바란다.(그렇게 말할 자격이 없는 나부터...)
'web3.js' 카테고리의 다른 글
[Ethereum] web3.js의 사용 (0) | 2022.09.01 |
---|---|
[web3-react] MetaMask로 지갑 연동하기 (0) | 2022.05.04 |