함수 제어자(function modifier)
접근 제어자(visibility modifier):
1. private은 컨트랙트 내부의 다른 함수들에서만 호출될 수 있음을 의미하지.
2. internal은 private과 비슷하지만, 해당 컨트랙트를 상속하는 컨트랙트에서도 호출될 수 있지.
3. external은 오직 컨트랙트 외부에서만 호출될 수 있네.
4. 마지막으로 public은 내외부 모두에서, 어디서든 호출될 수 있네.
상태 제어자(state modifier)
1. view는 해당 함수를 실행해도 어떤 데이터도 저장/변경되지 않음을 알려주지.
2. pure는 해당 함수가 어떤 데이터도 블록체인에 저장하지 않을 뿐만 아니라, 블록체인으로부터 어떤 데이터도 읽지 않음을 알려주지.
이들 모두는 컨트랙트 외부에서 불렸을 때 가스를 전혀 소모하지 않네(하지만 다른 함수에 의해 내부적으로 호출됐을 경우에는 가스를 소모하지).
사용자 정의 제어자
function test() external view onlyOwner anotherModifier { / ... / }
payable 제어자
payable 함수 이더를 받을 수 있는 특별한 함수 유형
이더리움에서는, 돈(이더), 데이터(transaction payload), 그리고 컨트랙트 코드 자체 모두 이더리움 위에 존재하기 때문에, 함수를 실행하는 동시에 컨트랙트에 돈을 지불하는 것이 가능하네.
함수를 실행하기 위해 컨트랙트에 일정 금액을 지불하게 하는 것이 가능!
contract OnlineStore {
function buySomething() external payable {
// 함수 실행에 0.001이더가 보내졌는지 확실히 하기 위해 확인:
require(msg.value == 0.001 ether);
// 보내졌다면, 함수를 호출한 자에게 디지털 아이템을 전달하기 위한 내용 구성:
transferThing(msg.sender);
}
}
여기서 일어나는 일은 누군가 web3.js(DApp의 자바스크립트 프론트엔드)에서 다음과 같이 함수를 실행할 때 발생하네:
// OnlineStore
는 자네의 이더리움 상의 컨트랙트를 가리킨다고 가정하네:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})
value 필드를 주목하게. 자바스크립트 함수 호출에서 이 필드를 통해 ether를 얼마나 보낼지 결정하네(여기서는 0.001이지). 트랜잭션을 봉투로 생각하고, 함수 호출에 전달하는 매개 변수를 자네가 써넣은 편지의 내용이라 생각한다면, value는 봉투 안에 현금을 넣는 것과 같네 - 편지와 돈이 모두 수령인에게 전달되지.
참고: 만약 함수가 payable로 표시되지 않았는데 자네가 위에서 본 것처럼 이더를 보내려 한다면, 함수에서 자네의 트랜잭션을 거부할 것이네.
자네가 컨트랙트로 이더를 보내면, 해당 컨트랙트의 이더리움 계좌에 이더가 저장되고 거기에 갇히게 되지 - 자네가 컨트랙트로부터 이더를 인출하는 함수를 만들지 않는다면 말이야.
자네는 다음과 같이 컨트랙트에서 이더를 인출하는 함수를 작성할 수 있네:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}
우리가 Ownable 컨트랙트를 import 했다고 가정하고 owner와 onlyOwner를 사용하고 있다는 것을 참고하게.
자네는 transfer 함수를 사용해서 이더를 특정 주소로 전달할 수 있네. 그리고 this.balance는 컨트랙트에 저장돼있는 전체 잔액을 반환하지. 그러니 100명의 사용자가 우리의 컨트랙트에 1이더를 지불했다면, this.balance는 100이더가 될 것이네.
자네는 transfer 함수를 써서 특정한 이더리움 주소에 돈을 보낼 수 있네. 예를 들어, 만약 누군가 한 아이템에 대해 초과 지불을 했다면, 이더를 msg.sender로 되돌려주는 함수를 만들 수도 있네:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
혹은 구매자와 판매자가 존재하는 컨트랙트에서, 판매자의 주소를 storage에 저장하고, 누군가 판매자의 아이템을 구매하면 구매자로부터 받은 요금을 그에게 전달할 수도 있겠지:
seller.transfer(msg.value).
keccak256을 통한 난수 생성
다음과 같은 방식으로 난수를 만들어낼 수 있네:
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
이 예시에서는 now의 타임스탬프 값, msg.sender, 증가하는 nonce(딱 한 번만 사용되는 숫자, 즉 똑같은 입력으로 두 번 이상 동일한 해시 함수를 실행할 수 없게 함)를 받고 있네.
그리고서 keccak을 사용하여 이 입력들을 임의의 해시 값으로 변환하고, 변환한 해시 값을 uint로 바꾼 후, % 100을 써서 마지막 2자리 숫자만 받도록 했네. 이를 통해 0과 99 사이의 완전한 난수를 얻을 수 있네.
이 메소드는 정직하지 않은 노드의 공격에 취약하네.
이더리움에서는 자네가 컨트랙트의 함수를 실행하면 트랜잭션(transaction)으로서 네트워크의 노드 하나 혹은 여러 노드에 실행을 알리게 되네. 그 후 네트워크의 노드들은 여러 개의 트랜잭션을 모으고, "작업 증명"으로 알려진 계산이 매우 복잡한 수학적 문제를 먼저 풀기 위한 시도를 하게 되네. 그리고서 해당 트랜잭션 그룹을 그들의 작업 증명(PoW)과 함께 블록으로 네트워크에 배포하게 되지.
한 노드가 어떤 PoW를 풀면, 다른 노드들은 그 PoW를 풀려는 시도를 멈추고 해당 노드가 보낸 트랜잭션 목록이 유효한 것인지 검증하네. 유효하다면 해당 블록을 받아들이고 다음 블록을 풀기 시작하지.
이것이 우리의 난수 함수를 취약하게 만드네.
우리가 동전 던지기 컨트랙트를 사용한다고 해보지 - 앞면이 나오면 돈이 두 배가 되고, 뒷면이 나오면 모두 다 잃는 것이네. 앞뒷면을 결정할 때 위에서 본 난수 함수를 사용한다고 가정해보세. (random >= 50은 앞면, random < 50은 뒷면이네).
내가 만약 노드를 실행하고 있다면, 나는 오직 나의 노드에만 트랜잭션을 알리고 이것을 공유하지 않을 수 있네. 그 후 내가 이기는지 확인하기 위해 동전 던지기 함수를 실행할 수 있지 - 그리고 만약 내가 진다면, 내가 풀고 있는 다음 블록에 해당 트랜잭션을 포함하지 않는 것을 선택하지. 난 이것을 내가 결국 동전 던지기에서 이기고 다음 블록을 풀 때까지 무한대로 반복할 수 있고, 이득을 볼 수 있네.
그럼 이더리움에서는 어떻게 난수를 안전하게 만들어낼 수 있을까?
하나의 방법은 이더리움 블록체인 외부의 난수 함수에 접근할 수 있도록 오라클을 사용하는 것이네.
향후 레슨에서는, 우린 oracle(이더리움 외부에서 데이터를 받아오는 안전한 방법 중 하나)을 사용해서 블록체인 밖에서 안전한 난수를 만드는 방법을 다룰 수도 있네.
이더리움에서 토큰은 기본적으로 그저 몇몇 공통 규약을 따르는 스마트 컨트랙트이네 — 즉 다른 모든 토큰 컨트랙트가 사용하는 표준 함수 집합을 구현하는 것이지. 예를 들면 transfer(address _to, uint256 _value)나 balanceOf(address _owner) 같은 함수들이 있네.
내부적으로 스마트 컨트랙트는 보통 mapping(address => uint256) balances와 같은 매핑을 가지고 있네. 각각의 주소에 잔액이 얼마나 있는지 기록하는 것이지.
즉 기본적으로 토큰은 그저 하나의 컨트랙트이네. 그 안에서 누가 얼마나 많은 토큰을 가지고 있는지 기록하고, 몇몇 함수를 가지고 사용자들이 그들의 토큰을 다른 주소로 전송할 수 있게 해주는 것이지.
왜 이렇게 해야 하나요?
모든 ERC20 토큰들이 똑같은 이름의 동일한 함수 집합을 공유하기 때문에, 이 토큰들에 똑같은 방식으로 상호작용이 가능하네.
즉 자네가 하나의 ERC20 토큰과 상호작용할 수 있는 애플리케이션 하나를 만들면, 이 앱이 다른 어떤 ERC20 토큰과도 상호작용이 가능한 것이지. 이런 방식으로 자네의 앱에 더 많은 토큰들을 추가할 수 있지. 커스텀 코드를 추가하지 않고도 말이네. 자네는 그저 새로운 토큰의 컨트랙트 주소만 끼워넣으면 되네. 그러고 나면, 짠, 자네의 앱에서 사용할 수 있는 또 다른 토큰이 생기는 것이네.
이러한 것의 한 예로는 거래소가 있네. 한 거래소에서 새로운 ERC20 토큰을 상장할 때, 실제로는 이 거래소에서 통신이 가능한 또 하나의 스마트 컨트랙트를 추가하는 것이네. 사용자들은 이 컨트랙트에 거래소의 지갑 주소에 토큰을 보내라고 할 수 있고, 거래소에서는 이 컨트랙트에 사용자들이 출금을 신청하면 토큰을 다시 돌려보내라고 할 수 있게 만드는 것이지.
거래소에서는 이 전송 로직을 한 번만 구현하면 되네. 그리고서 새로운 ERC20 토큰을 추가하고 싶으면, 데이터베이스에 단순히 새 컨트랙트 주소를 추가하기만 하면 되는 일이지.
다른 토큰 표준
ERC20 토큰은 화폐처럼 사용되는 토큰으로는 정말 적절하네. 하지만 우리의 좀비 게임에서 좀비를 표현할 때에는 그다지 쓸모 있지가 않지.
첫째로, 좀비는 화폐처럼 분할할 수가 없네 - 난 자네에게 0.237ETH를 보낼 수 있지만, 자네에게 0.237개의 좀비를 보내는 것은 말이 되지 않지.
둘째로, 모든 좀비가 똑같지는 않네. 자네의 레벨2 좀비 "Steve"는 내 레벨732 좀비 "H4XF13LD MORRIS 💯💯😎💯💯"와는 완전히 다르지(Steve와는 비교할 수가 없지!).
여기에 크립토좀비와 같은 크립토 수집품을 위해 더 적절한 토큰 표준이 있네 - 바로 ERC721 토큰이지.
ERC721 토큰은 교체가 불가하네. 각각의 토큰이 유일하고 분할이 불가하기 때문이지. 자네는 이 토큰을 하나의 전체 단위로만 거래할 수 있고, 각각의 토큰은 유일한 ID를 가지고 있네. 그러니 이게 우리의 좀비를 거래할 수 있게 하기에는 아주 적절하지.
ERC721과 같은 표준을 사용하면 우리의 컨트랙트에서 사용자들이 우리의 좀비를 거래/판매할 수 있도록 하는 경매나 중계 로직을 우리가 직접 구현하지 않아도 된다는 이점이 있네. 우리가 스펙에 맞추기만 하면, 누군가 ERC721 자산을 거래할 수 있도록 하는 거래소 플랫폼을 만들면 우리의 ERC721 좀비들을 그 플랫폼에서 쓸 수 있게 될 것이네. 그러니 자네만의 거래 로직을 만드느라 고생하는 것보다 토큰 표준을 사용하는 것이 명확한 이점이 있는 것이지.
ERC721 스펙에서는 토큰을 전송할 때 2개의 다른 방식이 있음을 기억하게:
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
첫 번째 방법은 토큰의 소유자가 전송 상대의 address, 전송하고자 하는 _tokenId와 함께 transfer 함수를 호출하는 것이네.
두 번째 방법은 토큰의 소유자가 먼저 위에서 본 정보들을 가지고 approve를 호출하는 것이네. 그리고서 컨트랙트에 누가 해당 토큰을 가질 수 있도록 허가를 받았는지 저장하지. 보통 mapping (uint256 => address)를 써서 말이지. 이후 누군가 takeOwnership을 호출하면, 해당 컨트랙트는 이 msg.sender가 소유자로부터 토큰을 받을 수 있게 허가를 받았는지 확인하네. 그리고 허가를 받았다면 해당 토큰을 그에게 전송하지.
자네가 눈치를 챘을지 모르겠지만, transfer와 takeOwnership 모두 동일한 전송 로직을 가지고 있네. 순서만 반대인 것이지(전자는 토큰을 보내는 사람이 함수를 호출하네; 후자는 토큰을 받는 사람이 호출하는 것이지).
그러니 이 로직만의 프라이빗 함수, _transfer를 만들어 추상화하는 것이 좋을 것이네. 두 함수에서 모두 쓸 수 있도록 말이야. 이렇게 하면 똑같은 코드를 두 번씩 쓰지 않아도 되지.
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" })
시작하기
자네 프로젝트의 작업 흐름에 맞춰, 자네 프로젝트에서 가장 많이 사용하는 패키지 도구를 써서 Web3.js를 추가할 수 있네:
// NPM을 사용할 때
npm install web3
// Yarn을 사용할 때
yarn add web3
// Bower를 사용할 때
bower install web3
// ...기타 등등.
또는 단순히 github에서 간략화된 .js 파일을 다운로드하고 자네 프로젝트에 포함시킬 수도 있네:
우리가 처음 필요로 하는 것은 Web3 프로바이더(Provider)이네.
이더리움은 똑같은 데이터의 복사본을 공유하는 노드들로 구성되어 있네. Web3.js에서 Web3 프로바이더를 설정하는 것은 우리 코드에 읽기와 쓰기를 처리하려면 어떤 노드와 통신을 해야 하는지 설정하는 것이지.
자네는 자네만의 이더리움 노드를 프로바이더로 운영할 수도 있네. 하지만 자네가 편리하게 쓸 수 있는 제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를 사용하는 웹사이트들과 상호작용을 할 수 있도록 해주지.
그리고 개발자로서, 사용자들이 웹 브라우저를 써서 웹사이트를 통해 자네의 DApp과 상호작용을 하길 원한다면, 자네는 분명 자네 DApp을 메타마스크와 호환할 수 있게 하고 싶을 것이네.
참고: 메타마스크는 내부적으로 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()
})
자네는 자네가 만드는 모든 앱에서 이 예제 코드를 사용할 수 있네. 사용자들이 자네 앱을 사용하려면 메타마스크를 사용하도록 하기 위해서 말이네.
참고: 메타마스크 말고도 사용자들이 쓸 수 있는 다른 개인 키 관리 프로그램도 있네. 미스트(Mist) 웹 브라우저 같은 것들이지. 하지만, 그것들도 모두 web3 변수를 주입하는 동일한 형태를 사용하네. 그러니 사용자들이 다른 프로그램을 쓰더라도 여기서 우리가 설명하는 방식으로 사용자의 Web3 프로바이더를 인식할 수 있을 것이네.
Web3.js는 자네의 스마트 컨트랙트와 통신을 위해 2가지를 필요로 할 것이네: 컨트랙트의 주소와 ABI이지.
컨트랙트 주소
스마트 컨트랙트를 모두 작성한 후, 자네는 그걸 컴파일한 후 이더리움에 배포할 것이네. 컨트랙트를 배포한 후, 해당 컨트랙트는 영원히 존재하는, 이더리움 상에서 고정된 주소를 얻을 것이네.
컨트랙트 ABI
ABI는 Application Binary Interface의 줄임말이네. 기본적으로 JSON 형태로 자네 컨트랙트의 메소드를 표현하는 것이지. 자네 컨트랙트가 이해할 수 있도록 하려면 Web3.js가 어떤 형태로 함수 호출을 해야 하는지 알려주는 것이지.
이더리움에 배포하기 위해 컨트랙트를 컴파일할 때(레슨 7에서 다룰 내용이지)), 솔리디티 컴파일러가 자네에게 ABI를 줄 것이네. 그러니 컨트랙트 주소와 함께 이를 복사하여 저장해야 하겠지.
Web3.js 컨트랙트 인스턴스화하기
컨트랙트의 주소와 ABI를 얻고 나면, 자네는 다음과 같이 Web3에서 인스턴스화할 수 있네:
// myContract 인스턴스화
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
Web3.js는 컨트랙트의 함수를 호출하기 위해 우리가 사용할 두 개의 메소드를 가지고 있네: call과 send이지.
Call
call은 view와 pure 함수를 위해 사용하네. 로컬 노드에서만 실행하고, 블록체인에 트랜잭션을 만들지 않지.
Web3.js를 사용하여, 다음과 같이 123을 매개 변수로 myMethod라는 이름의 함수를 call할 수 있네:
myContract.methods.myMethod(123).call()
Send
send는 트랜잭션을 만들고 블록체인 상의 데이터를 변경하네. view와 pure가 아닌 모든 함수에 대해 send를 사용해야 하는 것이지.
참고: 트랜잭션을 send하는 것은 사용자에게 가스를 지불하도록 하고, 메타마스크에서 트랜잭션에 서명하라고 창을 띄울 것이네. Web3 프로바이더로 메타마스크를 사용할 때, send()를 호출하면 자동으로 이 모든 것이 이루어지고, 우리의 코드에 어떤 특별한 것도 추가할 필요가 없지.
Web3.js를 사용하여, 다음과 같이 123을 매개 변수로 myMethod라는 이름의 함수를 호출하는 트랜잭션을 send할 수 있네:
myContract.methods.myMethod(123).send()
구문은 call()과 거의 똑같지.
좀비 데이터 받기
우리가 좀비 배열을 public으로 만들었던 것을 기억해보게:
Zombie[] public zombies;
솔리디티에서, public으로 변수를 선언하면 자동으로 같은 이름의 퍼블릭 "getter" 함수를 만들어 내네. 그러니 자네가 ID 15인 좀비를 찾길 원한다면, 변수를 함수인 것처럼 호출할 수 있지: zombies(15).
여기에 우리의 프론트엔드에서 좀비 ID를 받아 해당 좀비에 대해 컨트랙트에 질의를 보내고, 결과를 반환하는 자바스크립트 함수를 작성하는 방법이 있네:
function getZombieDetails(id) {
return cryptoZombies.methods.zombies(id).call()
}
// 함수를 호출하고 결과를 가지고 무언가를 처리:
getZombieDetails(15)
.then(function(result) {
console.log("Zombie 15: " + JSON.stringify(result));
});
cryptoZombies.methods.zombies(id).call()는 Web3 프로바이더와 통신하여 우리 컨트랙트의 Zombie[] public zombies에서 인덱스가 id인 좀비를 반환하도록 할 것이네.
이는 외부 서버로 API 호출을 하는 것처럼 비동기적으로 일어난다는 것을 알아두게. 즉 Web3는 여기서 Promise를 반환하네.
Promise가 만들어지면(이는 Web3 프로바이더로부터 응답을 받았다는 것을 의미하지) 우리 예제 코드는 then 문장을 실행하고, 여기서 result를 콘솔에 로그로 기록하지.
result는 다음과 같이 생긴 자바스크립트 객체가 될 것이네:
{
"name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
"dna": "1337133713371337",
"level": "9999",
"readyTime": "1522498671",
"winCount": "999999999",
"lossCount": "0" // Obviously.
}
이후 이 객체를 해석하기 위한 프론트엔드 로직을 만들어 의미 있는 방향으로 이 객체를 프론트엔드에 표시할 것이네.
메타마스크에서 사용자 계정 가져오기
메타마스크는 확장 프로그램 안에서 사용자들이 다수의 계정을 관리할 수 있도록 해주네.
우리는 주입되어 있는 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.accounts[0]과 같은지 확인하기 위해 100밀리초마다 확인하고 있네(즉 사용자가 해당 계정을 활성화해놓았는지 확인하는 것이지). 그렇지 않다면, userAccount에 현재 활성화된 계정을 다시 할당하고, 화면을 업데이트하기 위한 함수를 호출하네.
자네가 컨트랙트로부터 받은 데이터를 실제로 보여줄 수 없다면 이 튜토리얼은 완전하다고 할 수 없을 것이네.
하지만, 현실적으로 자네는 React나 Vue.js 같은 프론트엔드 프레임워크를 사용하고 싶을 수도 있네. 그것들이 프론트엔드 개발자로서의 자네의 삶을 훨씬 편하게 만들어주기 때문이지.
그래서 CryptoZombies.io의 초점을 이더리움과 스마트 컨트랙트에 맞추기 위해, 우리는 jQuery를 이용한 간단한 예제를 통해 어떻게 자네가 스마트 컨트랙트에서 전달받은 데이터를 파싱하고 표현할 수 있을지 보여줄 것이네.
좀비 데이터 보여주기 - 간단한 예제
지난 챕터에서 우리가 startApp() 내에서 getZombiesByOwner의 호출 결과를 써서 호출한 displayZombies를 상기해보게. 그 함수의 결과로 아래와 같이 생긴 좀비 ID 배열을 전달받을 수 있을 걸세:
[0, 13, 47]
따라서 displayZombies 함수는 다음과 같은 것을 할 것이네:
먼저 이미 무언가가 #zombies div의 안에 들어 있다면 이 div의 내용을 비우게(이렇게 하면 사용자가 그들의 활성화된 MetaMask 계정을 변경하면 새로운 좀비 군대를 로딩하기 전에 기존의 것을 삭제할 것이네).
반복을 통해 각 id마다 getZombieDetails(id)를 호출해서 우리의 스마트 컨트랙트에서 좀비에 대한 모든 정보를 찾게. 그리고서
화면에 표시하기 위해 HTML 템플릿에 좀비에 대한 정보를 집어넣고, 해당 템플릿을 #zombies div에 붙여넣게.
여기서도 우린 기본 템플릿 엔진이 없는 jQuery를 이용하기 때문에, 보기 싫을 수 있지만, 다음의 간단한 예제처럼 각 좀비에 대한 정보를 출력할 수 있네:
// 우리 컨트랙트에서 좀비 상세 정보를 찾아, zombie
객체 반환
getZombieDetails(id)
.then(function(zombie) {
// HTML에 변수를 넣기 위해 ES6의 "template literal" 사용
// 각각을 #zombies div에 붙여넣기
$("#zombies").append(`
<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>
좀비 스프라이트는 어떻게 표현하나요?
위 예제에서 우리는 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의 절대 좌표 포지셔닝을 이용해 다른 이미지 위에 위치할 걸세.
이제 send 함수를 이용해 스마트 컨트랙트의 데이터를 변경하는 방법을 살펴보도록 하지.
이 함수에는 call 함수와는 꽤 다른 부분이 있네:
트랜잭션을 전송(send)하려면 함수를 호출한 사람의 from 주소가 필요하네(솔리디티 코드에서는 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) {
$("#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 })와 같이 말이야. 만약 지정하지 않는다면, 메타마스크는 사용자가 이 값들을 선택할 수 있도록 할 걸세.
Wei란?
wei는 이더의 가장 작은 하위 단위이네 - 하나의 이더는 10^18개의 wei이지.
// 이렇게 하면 1 ETH를 Wei로 바꿀 것이네
web3js.utils.toWei("1");
우리 DApp에서, 우리는 levelUpFee = 0.001 ether로 설정했네. 그러니 levelup 함수를 호출할 때, 아래의 코드를 써서 사용자가 0.001 이더를 보내게 할 수 있네.
CryptoZombies.methods.levelUp(zombieId)
.send({ from: userAccount, value: web3js.utils.toWei("0.001") })
엉망진창이다. 설명 읽어도 하나~~도 모르겠다.
기초부터 차근차근 다시 공부하자ㅜㅜ