2022. 3. 31. 17:38ㆍ개발 잡부/블록체인
1. Web3.js 소개
이번 챕터에서는 실제로 상호작용이 가능한 웹페이지를 만들 예정이다.
그러기 위해 우선 Web3.js를 알아보자.
Web3.js
Web3.js는 JSON-RPC로 이루어진 복잡한 질의과정을
축약해주고, 쉽게 만들어준다.
Web3를 사용하는 방법은 여러가지가 있지만,
이 튜토리얼에서는 스크립트 태그로 불러올 것이다.
정답
...
<script language="javascript" type="text/javascript" src="./web3.min.js"></script>
...
2. Web3 프로바이더 (Provider)
이더리움의 노드와 통신하기 위해서는
어떤 노드와 통신해야 하는지 알아야 한다 (노드의 주소)
이 메커니즘은 API호출을 위해 웹서버의 URL을 설정하는 것과 같다.
Infura
Infura는 캐시 계층을 포함하는 다수의 이더리움 노드를 운영하는 서비스다.
접근을 위한 API를 무료로 사용할 수 있다.
사실 이 Web3, Web3 프로바이더, Infura의 관계는 이해가 좀 안 된다.
따로 키워드를 빼놨다가 정리해봐야겠다.
메타마스크 (Metamask)
사용자들이 자신들의 개인키를 이용해 읽기/쓰기를 하기 때문에,
이런 개인키를 적접 관리하는 것은 좋은 방법은 아니다.
이런 개인키를 관리하는 서비스가 여럿 있는데, 그 중에 하나가 '메타마스크'다.
메타마스크는 이더리움 계정과 개인 키를 안전하게 관리할 수 있게 해주는
크롬 / 파이러폭스의 확장 프로그램이다.
참고로 이 메타마스크는 Infura의 서버를
Web3 프로바이더로 사용하기 때문에,
메타마스크를 사용하면 Infura연결 걱정을 안 해도 된다.
아무튼 우리는 이 메타마스크를 사용하고,
사용자의 브라우저에 설치가 되어있지 않다면
알림을 보낼 것이다.
정답
...
<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>
...
3. 컨트랙트와 대화하기
컨트랙트 주소
우리가 컨트랙트를 만들고 이더리움에 배포하는 과정을 살펴보자.
1. 컨트랙트를 컴파일한다.
2. 컴파일된 컨트랙트를 이더리움에 배포한다.
3. 이더리움의 한 블록에 내 컨트랙트가 있다.
여기서 배포된 내 블록의 위치를 '컨트랙트 주소'라고 한다.
이 주소와 통신하면, 우리의 컨트랙트와 상호작용할 수 있게 된다.
컨트랙트 ABI
ABI는 Application Binary Interface의 약자이다.
기본적으로 JSON의 현태로 컨트랙트의 메소드를 표현하는 것인데,
인터페이스처럼 컨트랙트의 형식을 표현하는 것이다. (명세서)
컨트랙트를 컴파일하면 자동으로 ABI를 준다.
이걸 잘 간직하고 있다가 사용해야한다.
컨트랙트 인스턴스화하기
컨트랙트의 주소와 ABI를 얻고나면, Web3에서 컨트랙트를 객체화할 수 있다.
객체화 한다는것은 컨트랙트를 하나의 객체로써 사용할 수 있다는 뜻이다.
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
정답
...
<!-- 1. 여기에 cryptozombies_abi.js를 포함하게 -->
<script language="javascript" type="text/javascript" src="cryptozombies_abi.js"></script>
// 2. 여기서 코딩을 시작하게
var cryptoZombies;
function startApp() {
var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
}
...
4. 컨트랙트 함수 호출하기
인스턴스화된 컨트랙트에서 함수를 호출할 수 있다.
call
call은 view와 pure함수만을 위해 사용한다.
이는 트랜잭션을 만들지 않고, 로컬노드에서 실행한다.
Web3.js를 사용하여 다음과 같이 call할 수 있다.
myContract.methods.myMethod(123).call()
send
view와 pure가 아닌 모든 함수에 대해 send를 사용하면 된다.
참고로 메타마스크를 쓰고 있다면,
이 send함수가 호출될 때 서명 창이 뜬다.
myContract.methods.myMethod(123).send()
퍼블릭 변수와 웹에서의 접근
우리가 public변수로 선언한 zombies의 경우 누구나 조회가 가능하다.
이 경우, 다음과 같이 조회할 수 있다.
cryptoZombies.methods.zombies(id).call()
변수임에도 getter함수가 생겨서 함수처럼 가져오는 모습이다.
call, send와 Promise
컨트랙트와의 통신은 비동기적으로 이루어진다.
즉, Promise를 반환한다.
그러므로 await나 then으로 이후를 처리하면 된다.
(기타 라이브러리를 사용하던가...)
정답
...
// 1. 여기에 `zombieToOwner`를 정의하게.
function zombieToOwner(id) {
return cryptoZombies.methods.zombieToOwner(id).call();
}
// 2. 여기에 `getZombiesByOwner`를 정의하게.
function getZombiesByOwner(owner) {
return cryptoZombies.methods.getZombiesByOwner(owner).call();
}
...
5. 메타마스크 & 계정
우리는 위에서 쓴 코드에서 owner에 해당하는 사용자 지갑 주소를 가져와야 한다.
어떻게? 메타마스크한테 달라고 한다.
accounts
위에서 설명했듯 메타마스크는 전역으로 web3라는 변수를 사용할 수 있게 한다.
이 web3의 내부에는 이런 프로퍼티가 있다.
web3.eth.accounts[0]
계정변경 감지하기
우리 사이트내에서 계정을 바꾸지 않아도
메타마스크를 통해 사용자가 계정을 바꿀수도 있다.
그런 경우를 대비해서 0.1초마다 감지하는 코드를 짜보자.
var accountInterval = setInterval(function() {
// 계정이 바뀌었는지 확인
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
updateInterface();
}
}, 100);
정답
...
// 1. 여기에 `userAccount`를 선언하게.
var userAccount;
...
// 2. 여기에 `setInterval` 코드를 만들게.
var accountInterval = setInterval(function() {
// 계정이 바뀌었는지 확인
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
getZombiesByOwner(userAccount)
.then(displayZombies) ;
}
}, 100);
...
6. 좀비 군대 보여주기
뭔가 내용은 긴데
그냥 표현 코드를 길게 설명했다.
바로 정답으로 가자
정답
...
// Start here
$("#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>`);
})
...
7. 트랜잭션 보내기
트랜잭션을 전송하려면 함수를 호출한 사람의 주소가 필요하다.
메타마스크가 알아서 잘 가져와 줄 것이다.
그리고 또 고려할 점은, 트랜잭션 전송을 하고서
응답이 오기까지 평균 15초고, 더 늦어질수도 있다.
메타마스크와 send
우리가 send를 호출할 때, gas와 gasPrice를 지정하지 않으면
메타마스크 서명창에서 사용자가 선택하게 할 수 있다.
Web3 프로바이더의 응답
우리가 send를 보냈을 때, 여러가지 응답을 준다.
"receipt"는 성공적으로 반영되었을 때고,
"error"는 실패했음을 알리는 것이다.
정답
...
// Start here
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);
});
}
...
8. Payable 함수 호출하기
Wei (웨이)
wei는 이더의 가장 작은 하위 단위다.
10^18 wei가 1 이더와 같은데, 다음과 같이 변환 함수가 있다.
web3.js.utils.toWei("1");
이러한 wei값을 value에 담아 보내주면 된다.
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })
정답
...
// 여기서 시작하게.
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. 이벤트 구독하기
이 챕터에서는 이벤트를 구독하고, indexed 키워드에 대해 알아본다.
이벤트 구독하기
이벤트를 구독하는 방법은 다음과 같다.
(일반 함수와 유사하다)
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);
"data" 이벤트에 따라서 호출된다.
indexed 사용하기
하지만 모든 좀비 생성에 알람을 받을 필요는 없다.
이벤트를 필터링하는 방법을 알아보자.
순서는 다음과 같다.
1. 솔리디티에서 (컨트랙트에서) 인자를 indexed로 받는다.
2. 호출할 때, 인자로 filter를 넣어준다.
3. filter와 같은 값인 경우에만 이벤트가 호출된다.
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
지난 이벤트에 대해 질의하기
지난 이벤트들 (시작시점부터 시작해서 전부)을 가져올 수 있다.
cryptoZombies.getPastEvents("NewZombie", { fromBlock: 0, toBlock: "latest" })
첫번째 인자는 이벤트의 이름,
두번째 인자는 시작 ~ 끝 범위의 위치다.
정답
...
// 여기서 시작하게.
cryptoZombies.events.Transfer({ filter: { _to: userAccount } })
.on("data", function(event) {
let data = event.returnValues;
getZombiesByOwner(userAccount).then(displayZombies);
}).on("error", console.error)
...
후기
프론트엔드 부분이라 그런지 어렵지는 않았는데
대부분의 문제가 복-붙이라 좀 아쉬웠다.
근데 사실 그렇다고 하나하나 지령으로 하라고 하는게 더 나빠보이긴하다.
이로써 기초 단계는 마무리 되었다.
다음에는 Advanced Path를 해보면서 테스트와 배포를 배우고,
간단한 '커피 사주는' 프로젝트를 만들어볼까 한다.
'개발 잡부 > 블록체인' 카테고리의 다른 글
address에서 send, transfer를 호출할 수 없는 이슈 (0) | 2022.04.01 |
---|---|
SPDX-License-Identifier 이슈 (0) | 2022.04.01 |
크립토 좀비 1-5. ERC721 & 크립토 수집품 (0) | 2022.03.31 |
크립토 좀비 1-4. 좀비 전투 시스템 (0) | 2022.03.31 |
크립토 좀비 1-3. 고급 솔리디티 개념 (0) | 2022.03.30 |