크립토 좀비 / 크립토 좀비를 진행하면서 요약한 내용이 대다수이다.
DApp: Decentralized Application
탈중앙화된 P2P 네트워크 상에 백엔드 로직이 구동되는 응용프로그램이다.
- 블록체인 상의 스마트 컨트랙트가 기존의 중앙화된 서버에 의해 서비스를 제공하는 시스템을 대체한다.
- 좀 더 좁은 의미에서 DApp은 사용자 인터페이스를 통해 블록 체인의 스마트 컨트랙트를 호출함으로써 동작하는 응용프로그램이다.
DApp
= FrontEnd
+ Smart Contracts on Block chain
1. DApp
1) 구성요소
스마트 컨트랙트
- 서비스 로직이 구현된 이더리움 네트워크에 배포된 바이트 코드
사용자 인터페이스
- DApp의 사용자 인터페이스
- 주로 HTML, CSS, JS 등 프론트엔드 기술로 구현된다.
Web3 API for Js
이더리움 스마트 컨트랙트와 JS 코드 간의 상호작용을 지원한다.
web 3.0은 넓은 의미로 모든 정보가 분산, 분구너화된 차세대 네트워크를 일컫는다.
(소규모 회사에 정보가 집중되어 있는 wev 2.0과 대조된다.)
2) Web3. js
이더리움 네트워크와 상호작용 할 수 있게 하는 JS 라이브러리 모음
1
$ npm install web3
이더리움 네트워크는 노드로 구성되어 있고 각 노드는 블록체인의 복사본을 가지고 있다. 만약 스마트 컨트랙트의 함수를 실행하려면 이 노드들 중 하나에 요청을 보내야 한다. 보낼 내용은 다음과 같다.
- 스마트 컨트랙트의 주소
- 실행하고자 하는 함수
- 그 함수에 전달하고자 하는 변수
이더리움 노드들은 JSON-RPC라고 불리는 언어로만 소통할 수 있고 이는 사람이 읽기 불편하다. 하지만 web3.js를 사용하면 js를 통해 통신을 할 수 있게 된다.
- JSON-RPC HTTP 프로토콜을 통해 이더리움 스마트 컨트랙트를 호출한다.
- 브라우저와 모바일에서 호출이 가능하다.
2. DApp 구현
Ropsten 네트워크에 동작하는 Storage.sol 분산 앱을 제작
우선, Solidity.sol을 ropsten에 배포한다. 후에 화면을 구현하고 web3.js로 이더리움 네트워크를 연결한다.
1) web3 프로바이더 (provider)
web3 프로바이더를 설정하여 우리 코드에 읽기와 쓰기를 처리하기 위해 어떤 노드와 통신을 해야 하는지 설정한다.
전통적인 웹 앱에서 API 호출을 위한 원격 웹 서버의 URL을 설정하는 것과 같다.
Infura라는 서비스를 사용하면, DApp 서비스 사용자가 사용하는 서비스용 이더리움 노드를 운영할 필요가 없다.
✅ Infura
- 빠른 읽기를 위한 캐시 계층을 포함하는 다수의 이더리움 노드를 운영하는 서비스이다.
- 접근을 위한 API를 무료로 사용할 수 있다.
- Infura를 프로바이더로 사용하면, 우리만의 이더리움을 설치하고 계속 유지할 필요 없이 이더리움 블록체인과 메시지를 주고 받을 수 있다.
1
2
const web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
하지만, DApp사용자들이 블록체인에 데이터를 읽기만 하는 것이 아니라 뭔가를 쓰기도 할 것이다. 우리는 이 사용자들이 그들의 개인 키로 트랜잭션에 서명 할 수 있도록 해야 한다.
이더리움은 트랜잭션에 전자 서명을 하기 위해 공개 / 개인 키 쌍을 사용한다. 이런 방식으로 블록체인에서 데이터를 변경한다면 나의 공개 키를 통해 내가 거기 서명을 한 사람이라고 증멸할 수 있다. 하지만 아무도 내 개인 키를 모르기 때문에 내 트랜잭션을 위조할 수 없다.
개인키를 우리가 관리하는 것은 꽤나 어렵기 때문에 메타 마스크와 같은 지갑을 사용할 수 있다. 사용자들이 우리의 DApp과 상호작용하기를 원하면 DApp을 메타마스크와 호환할 수 있도록 해야 한다.
참고로 메타마스크는 내부적으로 Infura의 서버를 web3 프로바이더로 사용한다. 하지만 사용자들이 web3 프로바이더를 선택할 수 있는 옵션을 제공하기 때문에 메타마스크의 web3 프로바이더를 사용하면 편하다.
✅ 메타마스크의 web3 프로바이더
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
window.addEventListener('load', function() {
// Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Mist/MetaMask의 프로바이더 사용
web3js = new Web3(web3.currentProvider);
} else {
// 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
// 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
}
// 이제 자네 앱을 시작하고 web3에 자유롭게 접근할 수 있네:
startApp()
})
- web3라는 전역 자바스크립트 객체를 통해 브라우저에 web3 프로바이더를 주입.
- web3가 존재하는 지 확인하고 만약 존재한다면
web3.currentProvider
를 프로바이더로 사용한다.
2) 컨트랙트
web3프로바이더로 web3.js를 토기화 했으면, 스마트 컨트랙트와 통신할 수 있다.
web3.js가 스마트 컨트랙트와 통신을 하기 위해서는 2가지가 필요하다.
- 컨트랙트 주소
- 스마트 컨트랙트를 모두 작성한 후, 이를 컴파일한 후 이더리움에 배포하게 된다.
- 컨트랙트를 배포하게 되면 이더리움 상에 고정된 주소를 얻게 된다.
- ABI
- Application Binary Interface의 줄임말로 JSON형태로 컨트랙트의 함수를 표현하는 것이다.
- 이더리움에 배포하기 위해 컨트랙트를 컴파일 할 때 솔리디티 컴파일러에데 ABI 얻게 된다.
1
2
// web3.js 컨트랙트 인스턴스화 하기
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
3) 컨트랙트 함수 호출
컨트랙트의 함수를 호출하기 위해 call, send 메서드를 사용할 수 있다.
1
2
3
4
5
// call
myContract.methods.myMethod(매개변수).call()
// send
myContract.methods.myMethod(매개변수).send()
- call은 view와 pure 함수를 위해 사용된다. 로컬 노드에서만 실행되고 블록 체인에 트랜잭션을 만들지 않는다.
- view, pure함수는 읽기 전용함수로 가스를 사용하지도 트랜잭션에 서명하라고 하지도 않는다.
- 비동기적으로 일어난다. (promise를 반환한다.)
- send는 트랜잭션을 만들고 블록체인 상의 데이터를 변경한다. view와 pure가 아닌 모든 함수에 대해 send를 사용한다.
- 트랜잭션을 생성하며 gas를 소모한다.
- 함수를 호출한 사람의 from 주소가 필요하다.
- 사용자가 트랜잭션 전송을 하고 난 후 실제로 블록체인에 적용될 때까지는 상당한 지연이 발생한다. 트랜잭션이 블록에 포함될 때까지 기다려야하는데, 이더리움의 평균 블록 시간이 15초 이기 때문이다. 만약 이더리움에 보류중인 거래가 많거나 사용자가 가스가격을 낮게 보낼 경우 우리 트랜잭션이 블록에 포함되길 기다리는 시간이 오래걸릴 수 있다.
1
2
3
4
5
6
7
8
9
// solidity에서 public으로 변수를 선언하면 자동으로 같은 이름의 퍼블릭 getter함수를 만들어 낸다.
// 만약 solidity에 Struct[] public arr;라는 배열이 있으면 해달 배열의 특정 인덱스 값을 호출하는 함수를 만들 수 있다.
function getArrDetail(index){
retrun myContract.methods.arr(id).call()
};
getArrDetail(15)
.then((result)=> console.log(ressult));
4) 메타마스크
메타 마스크에서 사용자의 계정을 가져온다.
1
2
3
4
5
6
7
8
9
10
11
const userAccount = web3.eth.accounts[0];
// 사용자가 언제든지 메타 마스크에 활성화된 계정을 바꿀 수 있기 때문에 계속 감시해야 한다.
var accountInterval = setInterval(function() {
// 계정이 바뀌었는지 확인
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// 새 계정에 대한 UI로 업데이트하기 위한 함수 호출
updateInterface();
}
}, 100)
5) 트랜잭션 전송
web3 프로바이더에게 트랜잭션을 전송하고, 몇가지 이벤트 리스너를 연결한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// createContract(id)라는 함수가 solidity에 있다고 가정
function createRandomZombie(name) {
alert("기다리세요...");
// 컨트랙트에 전송
return myContract.methods.createContract(od)
.send({ from: userAccount })
.on("receipt", function(receipt) {
alert("성공")
})
.on("error", function(error) {
alert("실패
});
}
- receipt는 트랜잭션이 이더리움의 블록에 포함될 때 발생한다.
- error는 트랜잭션이 블럭에 포함되지 못했을 때 발생한다.
6) Payable 함수 호출
wei 단위를 사용하여 함수에 전달해야 한다.
1 Ether = 10^18 wei
1
2
3
4
5
6
// 1 ETH를 Wei로 바꾼다.
web3js.utils.toWei("1");
// 1이더를 담아 payable 함수 호출
myContract.methods.payableFuc(params)
.send({form: userAccount, value: web3js.utils.toWei("1")});
7) 이벤트 구독
solidity에는 event 정의할 수 있다. web3.js에서는 이 이벤트를 구독할 수 있다.
1
2
3
4
5
6
7
8
// newEvent라는 event가 solidity에 정의되었다고 가정
// DApp 사용자가 해당 이벤트를 발생시키면 모두에게 알림이 발생한다.
myContract.event.newEvent(params)
.on("data", (event) => {
console.log(event.returnValues);
})
.on("error", console.error);
1
2
3
4
5
6
7
8
9
// indexed를 사용하여 현재의 사용자와 연경된 변경만 수신한다.
// 솔리디티: event newEvent(address indexed _from, address indexed _to, uint256 _tokenId);
// filter를 사용해 _to가 userAccount와 같을 때만 코드를 실행한다.
Contract.events.newEvent({ filter: { _to: userAccount } })
.on("data", (event) => {
console.log(event.returnValues);
})
.on("error", console.error);
✅ 지난 이벤트 구독
getPastEvents
를 이용해 지난 이벤트를 확인하고 fromBlock
과 toBlock
필터들을 이용해 이벤트 로그에 대한 시간 범위를 솔리디티에 전달할 수 있다.
여기서 block은 이더리움 블록 번호를 의미한다.
1
2
3
4
5
myContract.getPastEvents("NewEvent", { fromBlock: 0, toBlock: "latest" })
.then((events) => {
// events는 event 객체들의 배열이다.
console.log(event)
});
- 이 메서드를 통해 시작 시간부터의 이벤트 로그에 대한 확인이 가능하다.
- 이벤트를 블록체인에 기록하는 것은 솔리디티에서 가장 비싼 비용 중 하나이다.
- 스마트 컨트랙트에서는 이벤트를 읽을 수 없음에도 이벤트를 이용하는 것은 gas측면에서 훨씬 더 져렴하다.
3. 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
var web3;
// MetaMask에서 Ropsten 네트워크의 PRC URL을 확인하여 변수로 생성한다. (Settings > Network 확인)
const ROPSTEN_URL = '';
// ContractAddress
const CA = '';
// ABI
const STORAGE_ABI = []
// 컨트랙트 호출에 사용할 계정 정보를 세팅한다.
// 계정의 개인 키를 변수로 생성한다. (MetaMask에서 export private key)
const privateKey = '';
// 개인키로부터 계정을 생성해주는 web3 API를 활용하여 주소를 얻어낸다.
var sender;
var senderAddress;
var storageContract;
sender = web3.eth.accounts.privateKeyToAccount('0x' + privateKey);
web3.eth.accounts.wallet.add(sender);
web3.eth.defaultAccount = sender.address;
senderAddress = web3.eth.defaultAccount;
// web3 객체 만들기
// 화면이 로드될 때, web3 객체를 생성한다.
window.addEventListener('load', () => {
if( typeof web3 !== 'undefined') {
window.web3 = new Web3(web3.currentProvider);
alert('web3 injected')
} else {
window.web3 = new Web3(new web3.providers.HttpProvider(ROPSTEN_URL))
}
startApp();
});
// 계정 정보 생성 및 초기 값 세팅
function startApp() {
// 1. 계정 정보
// 2. storage 컨트랙트 인스턴스
// 3. 화면에 초기 값 세팅
}
// retrieve() 함수 호출 후 화면에 결과 표시
function retrieve() {
}
// store() 함수 호출 후 화면에 결과 표시
function store() {
}