일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 솔리디티
- for문 사용해보기
- 2448
- 자바스크립트
- 10871
- Mist
- 더하기 사이클
- 시험 성적
- 가상 화폐
- if문 사용해보기
- 이더리움
- 백준
- 별 찍기 - 11
- Dapp
- 1%d
- 1546
- 평균은 넘겠지
- Baekjoon
- 세 수
- 1065
- 비트 코인
- 단계별로 풀어보기
- Remix
- 10817
- X보다 작은 수
- 1110
- 알고리즘 문제풀이
- 그대로 출력하기
- 블록 체인
- 함수 사용하기
- Today
- Total
블링블링 범블링
나도 dApp 개발해보자 강좌 - 5 본문
※ 이 글은 chaintalk의 atomrigs 님이 쓰신 글을 인용하였습니다.
(5) 첫번째 dApp 의 완성
dApp 은 스마트 컨트랙트 + 사용자 인터페이스 라고 했습니다. 우리는 전편까지 스마트 컨트랙트를 만들고 이것을 블록체인에 올리고, web3 와 ABI 를 이용해 엑세스하는 것을 해보았습니다. 여기에 사용자 인터페이스를 더하면 최종적인 dApp이 완성됩니다. 위의 스크린 샷은 완성된 첫번째 dApp 입니다.
이 dApp 은 깃허브에 올려 놓았습니다.
https://atomrigs.github.io/simplestorage.html
구체적인 HTML/Javascript 코드를 보기 전에 이 dApp 이 사용자에게 제공할 인터페이스는 무엇인가를 정리해 봅시다.
- (1) 지정된 주소에 올려져 있는 simple storage 컨트랙트의 storedData 에 저장된 값을 불러와서 사용자에게 보여준다.
- (2) 사용자가 지정한 정수값을 입력 받아서 위의 storedData 에 저장한다.
- (3) 지정한 컨트랙트의 주소를 화면에 보여주고, 이 주소를 클릭하면 블록체인에 기록된 트랜잭션들을 보여준다.
- (4) 현재 사용중인 어카운트 주소를 보여주고, 이주소를 클릭하면 블록체인에 기록된 트랜잭션들을 보여준다.
- (5) 현재 storedData 에 기록된 값을 보여줄 때, 현재의 블록넘버도 같이 보여준다.
- (6) 사용자가 새로운 값을 입력해서 저장하면, 이 트랜잭션 아이디를 보여주고, 이 트랜잭션이 새 블록체인에 기록되면 자동으로 현재 저장된 값과 트랜잭션의 상태를 변경해서 보여준다.
<!DOCTYPE html><html><head><meta charset="UTF-8"><meta http-equiv="CACHE-CONTROL" content="NO-CACHE"><link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.2.3/milligram.min.css"><title>Simple Storage Dapp 예제 </title><style>body {margin-left:50px;}#storedData {font-size:300%; margin-right:10px;}#newValue {width: 200px; margin-right:10px; text-align:right;}</style></head><body><h3>Simple Storage dApp 예제</h3><ul><li>컨트랙트 주소: <span id="contractAddr"></span></li><li>내 어카운트 주소: <span id="accountAddr"></span></li><li>컨트랙트에 저장된 값: <span id="storedData"></span><button onclick="getValue()">새로고침</button> (현재블록: <span id="lastBlock"></span>)</li><li><input id="newValue" type="text"><button onclick="setValue()">새 값으로 저장하기</button><div id="result"></div></li><li>새 값을 저장한 후 팬딩 트랜잭션이 블록에 포함되면 자동으로 페이지가 업데이트될 것입니다.</li></ul>컨트랙트 소스<script src="https://gist.github.com/atomrigs/7c633570496b79623bed5d1286f93f3a.js"></script>HTML 소스<br><a href="https://github.com/atomrigs/atomrigs.github.io/blob/master/simplestorage.html">https://github.com/atomrigs/atomrigs.github.io/blob/master/simplestorage.html</a>;<br><br><p><a href="http://www.chaintalk.io/archive/lecture?sca=%EB%82%98%EB%8F%84+dApp+%EA%B0%9C%EB%B0%9C"><i>나도 dApp 개발해 보자 시리즈 by Atomrigs © 2017</i></a></p></body><script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script><!-- script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script --><script>var contractAddress = '0xc5244053ecA508a11951400fc7Af28738Fd0ce77';var abi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}];var simpleStorageContract;var simpleStorage;window.addEventListener('load', function() {// Checking if Web3 has been injected by the browser (Mist/MetaMask)if (typeof web3 !== 'undefined') {// Use Mist/MetaMask's providerwindow.web3 = new Web3(web3.currentProvider);} else {console.log('No web3? You should consider trying MetaMask!')// fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)window.web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));}// Now you can start your app & access web3 freely:startApp();});function startApp() {simpleStorageContract = web3.eth.contract(abi);simpleStorage = simpleStorageContract.at(contractAddress);document.getElementById('contractAddr').innerHTML = getLink(contractAddress);web3.eth.getAccounts(function(e,r){document.getElementById('accountAddr').innerHTML = getLink(r[0]);});getValue();}function getLink(addr) {return '<a target="_blank" href=https://testnet.etherscan.io/address/' + addr + '>' + addr +'</a>';}function getValue() {simpleStorage.get(function(e,r){document.getElementById('storedData').innerHTML=r.toNumber();});web3.eth.getBlockNumber(function(e,r){document.getElementById('lastBlock').innerHTML = r;});}function setValue() {var newValue = document.getElementById('newValue').value;var txidsimpleStorage.set(newValue, function(e,r){document.getElementById('result').innerHTML = 'Transaction id: ' + r + '<span id="pending" style="color:red;">(Pending)</span>';txid = r;});var filter = web3.eth.filter('latest');filter.watch(function(e, r) {getValue();web3.eth.getTransaction(txid, function(e,r){if (r != null && r.blockNumber > 0) {document.getElementById('pending').innerHTML = '(기록된 블록: ' + r.blockNumber + ')';document.getElementById('pending').style.cssText ='color:green;';document.getElementById('storedData').style.cssText ='color:green; font-size:300%;';filter.stopWatching();}});});}</script></html>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.2.3/milligram.min.css">
화면에 실제로 뿌려지는 내용은 <body>...</body> 안에 정의되어 있습니다. 그 중에서도 다음이 주 내용입니다.
<ul><li>컨트랙트 주소: <span id="contractAddr"></span></li><li>내 어카운트 주소: <span id="accountAddr"></span></li><li>컨트랙트에 저장된 값: <span id="storedData"></span><button onclick="getValue()">새로고침</button> (현재블록: <span id="lastBlock"></span>)</li><li><input id="newValue" type="text"><button onclick="setValue()">새 값으로 저장하기</button><div id="result"></div></li><li>새 값을 저장한 후 팬딩 트랜잭션이 블록에 포함되면 자동으로 페이지가 업데이트될 것입니다.</li></ul>
<button onclick="getValue()">새로고침</button>
<input id="newValue" type="text"><button onclick="setValue()">새 값으로 저장하기</button>
<script src="https://cdn.rawgit.com/ethereum/web3.js/develop/dist/web3.js"></script><!-- script src="https://code.jquery.com/jquery-3.1.1.slim.min.js"></script -->
var contractAddress = '0xc5244053ecA508a11951400fc7Af28738Fd0ce77';var abi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"}];var simpleStorageContract;var simpleStorage;
window.addEventListener('load', function() {// Checking if Web3 has been injected by the browser (Mist/MetaMask)if (typeof web3 !== 'undefined') {// Use Mist/MetaMask's providerwindow.web3 = new Web3(web3.currentProvider);} else {console.log('No web3? You should consider trying MetaMask!')// fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)}// Now you can start your app & access web3 freely:startApp();});
function startApp() {simpleStorageContract = web3.eth.contract(abi);simpleStorage = simpleStorageContract.at(contractAddress);document.getElementById('contractAddr').innerHTML = getLink(contractAddress);web3.eth.getAccounts(function(e,r){document.getElementById('accountAddr').innerHTML = getLink(r[0]);});getValue();}
동기식(synchronous) 호출 | 비동기식(asynchronous) 호출 |
결과값이 리턴 될 때까지 프로세스를 중지하고 기다림 (블록킹) | 결과값 리턴을 기다리지 않고 다음 프로세스로 진행 |
var newValue = getNewValue(); document.getElementById('target_id').innerHTML = newValue; doAnotherThing(); | getNewValue(function(e,r) { var newValue = r; document.getElementById('target_id').innerHTML = newValue; } doAnotherThing(); |
getNewValue()가 리턴값을 newValue에 할당할 까지 대기함. 따라서 이 작업이 끝나지 않으면 뒤의 작업들은 실행되지 않음 | getNewValue()가 리턴값 r 을 주지 않더라도, 그냥 다음번 명령인 doAnotherThing() 을 실행함. 나중에 r 값이 오면 그 때 document.getElementById('target_id').innerHTML = newValue 를 실행 |
리턴값을 받는데 시간이 걸릴 경우, 그 시간 동안 브라우저가 먹통됨. 하지만 특정 시점에서 여러가지 변수 값을 확인할 때는 정확함. | 시간이 지체 되는 호출을 병렬로 즉시 보내고 다음 프로세스로 넘어갈 수 있으나, 일정한 시점에 어떤 리턴 값이 돌아 왔는지 비교 확인하는데 주의가 필요. |
function getValue() {simpleStorage.get(function(e,r){document.getElementById('storedData').innerHTML=r.toNumber();});web3.eth.getBlockNumber(function(e,r){document.getElementById('lastBlock').innerHTML = r;});}
function setValue() {var newValue = document.getElementById('newValue').value;var txidsimpleStorage.set(newValue, function(e,r){document.getElementById('result').innerHTML = 'Transaction id: ' + r + '<span id="pending" style="color:red;">(Pending)</span>';txid = r;});var filter = web3.eth.filter('latest');filter.watch(function(e, r) {getValue();web3.eth.getTransaction(txid, function(e,r){if (r != null && r.blockNumber > 0) {document.getElementById('pending').innerHTML = '(기록된 블록: ' + r.blockNumber + ')';document.getElementById('pending').style.cssText ='color:green;';document.getElementById('storedData').style.cssText ='color:green; font-size:300%;';filter.stopWatching();}});});}
var newValue = document.getElementById('newValue').value;var txidsimpleStorage.set(newValue, function(e,r){document.getElementById('result').innerHTML = 'Transaction id: ' + r + '<span id="pending" style="color:red;">(Pending)</span>';txid = r;});
여기는 그렇게 생소하지 않습니다. 인풋 박스에 있는 값을 가져다가 simpleStorage.get() 에 넣어줌으로써 블록체인에 저장합니다. 'result' id 를 가진 span 에 리턴되어온 트랜잭션 id 와 함께 붉은색으로 (Pending) 이라고 넣어줍니다.
var filter = web3.eth.filter('latest');filter.watch(function(e, r) {... }
여기서 web3.eth.filter() 라는게 나오는데요, 블록체인 상의 변화를 체크하는 역할을 합니다. 'lastest' 라는 조건은 새 블록을 의미합니다. 그래서 filter.watch() 한다는 것은 새 블록이 발견되면, watch() 안에 포함된 명령을 실행하라는 것입니다.
getValue();web3.eth.getTransaction(txid, function(e,r){if (r != null && r.blockNumber > 0) {document.getElementById('pending').innerHTML = '(기록된 블록: ' + r.blockNumber + ')';document.getElementById('pending').style.cssText ='color:green;';document.getElementById('storedData').style.cssText ='color:green; font-size:300%;';filter.stopWatching();}});
watch() 안에 포함된 첫번째 명령은 getValue() 함수를 실행하라는 것입니다.
앞에서 처음 페이지가 로딩될 때 실행한 함수였지요. 그걸 재 실행하라는 겁니다. 왜 재 실행합니까? 마지막 블럭넘버가 바뀌었기 때문이죠. 그에 따라 컨트랙트에 있는 storedData 값도 변화되어 있을 수 있기 때문에 최신 값으로 바꾸라는 겁니다. 지금 컨트랙트에는 다른 사용자가 있을 수도 있습니다. 한 사용자가 업데이트 하지 않았다 하더라도 다른 사용자가 값을 바꾸었을 수도 있습니다. 그래서 새 블록이 나왔으면, storedData 에 있는 값을 갱신해 놓자는 거지요.
그 다음 라인에 있는 web3.eth.getTransaction(txid, function(e,r){}) 는 우리가 앞에서 받았던 업데이트 트랜잭션 id (txid) 로 블록체인에서 그 기록을 한번 가져와 보는 겁니다. 만일 이 txid 가 새 블록에 포함되었다면 그 결과값에 블럭넘버가 포함되어 있겠지요. 가끔은 내가 보낸 트랜잭션이 포함되지 않은 채 새 블록이 생성되었을 수도 있습니다. 그럴 경우에는 아예 r 이 없거나, r.blockNumber 가 배정되지 않았겠지요. 그것은 우리가 보낸 값이 아직 저장되지 않았다는 것을 의미합니다. 그렇게 되면 if (r != null && r.blockNumber > 0) 에서 거짓이 되겠지요. "&&" 라는 것은 "AND" 의 의미입니다. 두 가지가 다 충족되어야 합니다. 리턴값이 null 이 아니고 거기에 블록넘버가 나온다(>0)는 것은 정상적으로 블록체인에 포함이 되었다는 뜻입니다. 그렇다면 아까 '(Panding)' 이라고 해두었던 내용 대신 '기록된 블럭' 이라는 안내 문구와 이 트랜잭션이 포함된 블록넘버를 보여주게 됩니다. 그리고 storedData 값도 새 트랜잭션이 반영된 것이기 때문에 녹색으로 문자색을 바꾸어줍니다.
watch() 안에서 실행되는 두 함수가 반드시 동시에 실행되는 것은 아니기 때문에, 현재 storedData 값과 업데이트 트랜잭션 관련 화면갱신에 시간차가 날 수 있습니다.
filter.stopWatching();
그리고 나서 정상적으로 트랜잭션이 반영되었다면, 더 이상 새 블록을 모니터링 하지 않고 필터링을 끝내게 됩니다. 만일 여기서 filter를 끝내지 않는다면, 이 페이지를 오픈해 놓으면 블록이 업데이트 될 때 마다 getValue()가 계속 실행 됩니다. 이런 식의 지속적인 watch가 필요한 dApp 들도 있겠지만 본 예제에서는 업데이트 값이 반영되고 나면 더 이상 watch 하지 않도록 했습니다.
이로써 우리는 Simple Storage 컨트랙트를 사용한 하나의 dApp을 완성했습니다. 매우 단순한 기능의 구현이지만, dApp 의 핵심 개념을 보여주기에는 충분하다고 봅니다. 더 어렵고 복잡한 dApp 을 만들기 전에 정확한 기본 개념을 파악하는 것도 도움이 많이 됩니다.
자 그렇다면 이 소스 코드를 가지고 직접 테스트하고 위의 샘플 페이지처럼 직접 외부에 오픈 하려면 어떻게 해야 할까요?
가능하면 별다른 소프트웨어를 깔지 않고 진행하려 했지만, 여기서부터는 기본적인 개발환경 셋팅부터 시작 해야겠습니다.
전문적인 개발환경을 제대로 갖추려면 상당히 세심한 셋팅과 노력이 필요하지만, 일단 꼭 필요한 것부터 해봅시다.
GitHub 계좌 오픈
에 가서 등록하시면 됩니다. 전문적인 개발을 직업으로 하지 않는다 해도 어카운트 하나 쯤 오픈해 둘만 합니다.
일반문서도 여기에 보관하면 버전관리도 쉽게 하고 백업하는 용도로도 쓸 수 있습니다.
어카운트 오픈한 다음 심플한 HTML 페이지를 호스팅 할 수 공간을 하나 만들어야 되는데, 제 프로젝트를 방문해 보세요.
https://github.com/atomrigs/atomrigs.github.io
이왕 가셨으면 저 한테 별하나 날려 주시구요.
위의 Repository 는 다른 것과 좀 틀린데 여기에 올린 페이지는 깃서버로 웹 호스팅이 자동으로 된다는 점입니다. 이 서비스를 깃 페이지라고 부릅니다. 이 깃 페이지를 셋업하려면 오른쪽 위 코너에 있는 '+' 를 눌러서 New Repository 를 선택하고 다음과 같이 셋팅합니다.
반드시 본인의 id 와 .github.io 앞에 있는 이름이 같아야 합니다. 틀리면 나중에 atomrigs.github.io 도메인으로 파일 접근이 안됩니다.
그 다음 부터 자세한 깃허브 사용법은 조금 검색해보면 많이 나올 겁니다. 너무 전문적인 학습은 아직 필요 없고 기본 컨셉만 잡고 파일 만들 수 있으면 됩니다.
그 다음 본인의 깃허브 페이지와 로컬 컴퓨터에서 작업할 파일을 서로 싱크시킬 필요가 있습니다. 직접 깃허브에서 에디팅할 수도 있지만, 효율성이 매우 떨어집니다. 그래서 깃허브 데스크탑을 깔고 온라인 버전을 클론해서 서로 싱크세팅을 해야 합니다.
에 가셔서 자신의 시스템에 맞는 버전을 깔고 데스크탑 버전을 오픈하세요.
그런후에 clone 탭을 누르고 본인의 계정 밑에 있는 본인아이디.github.io 를 선택하고 클론하세요.
온라인 쪽과 로컬 파일을 생성하고 수정해보고 싱크해보고 왔다 갔다 해보세요. 조금 해보면 왜 이런 툴을 쓰는지 감이 올 겁니다.
이로써 온라인에 위에서 작업한 본인의 파일을 올리고 직접 dApp 을 돌릴 수 있게 되었습니다.
그러나 로컬에서 작업한 파일을 직접 브라우저에서 file:/// 로 오픈해 보면 메타마스크가 잘 작동하지 않을 껍니다. 이것은 브라우저에서 보안상 일부 기능을 막아 놓기 때문입니다. 결국 로컬에서도 웹 서버가 하나 필요합니다. 웹 서버라고 하니 거창한데 이것 역시 쉽게 구할 수 있습니다.
앞으로 우리가 제일 많이 보게 될 웹서버는 node.js 기반입니다. 만일 지금 프로그래밍 공부를 시작한다면, 그냥 javascript, jquery, node.js 부터 시작하는게 좋습니다. 그래서 node.js 를 깝니다.
명색이 개발자인데 가장 최신 버전을 까는게 좋겠지요.
인스톨이 끝나면 노드를 실행해서 다음 명령어를 날리세요. 웹 개발에 아주 좋은 서버 모듈를 인스톨합니다.
npm install live-server -g
live-server 는 http-server 와 비슷한 패키지인데 다른 특징은 호스팅하고 있는 파일이 변경되면 페이지가 자동으로 리로드가 됩니다. 잠깐이라도 수고를 덜어 줍니다.
그런다음 node.js command prompt 를 클릭해서 도스창(cmd 창)으로 나갑니다.
여기서 아까 로컬에 싱크했던 atomrigs.github.io 디렉토리로 이동한 다음
live-server 명령어 하나만 쳐주면 됩니다.
자 이제 작업하던 디렉토리를 베이스로 해서 웹서버가 가동되기 시작했습니다. 이제 로컬서버에서 페이지를 불러 올 수 있습니다.
http://127.0.0.1:8080/simplestorage.html
그리고 소스 페이지를 수정하면 자동으로 페이지가 리로딩됩니다.
로컬과 깃허브에서 위의 예제를 다 테스트해볼 수 있는 환경이 완성된 것입니다.
텍스트 파일 에디터는 평소 자주 쓰는 것을 사용하면 되겠지만, 앞으로 solidity 문법지원과 깃허브 지원 등의 기능을 고려한다면 atom 에디터를 추천합니다.
요즘 아톰이 너무 유행이네요 ㅋㅋ
-------------
내용이 너무 길었나요?
여기까지 오시느라 너무 수고가 많았습니다. 설명히 부족한 부분들은 댓글로 많이 질문해 주세요.
이번에는 숙제를 면제해드리고 싶지만, 앞으로 갈 길을 생각하니 더 내도록 하겠습니다. ㅋㅋ
(1) 공통 숙제
위의 예제들을 본인의 로컬 컴퓨터와 깃트허브에 작성하시고
본인의 페이지를 아래의 url 처럼 방문할 수 있도록 해주시고 그 주소를 댓글로 올려주세요.
https://atomrigs.github.io/simplestorage.html
물론 여기서 atomrigs 는 본인의 아이디로 대체되어야 합니다.
(2) 도전자 숙제
좀 더 도전 의식이 있는 분들을 위해 중급문제 하나 추가합니다. 이 숙제는 꼭 안 하셔도 되지만 성공하면 특별 상장을 수여합니다.
위의 예제는 정해진 컨트랙트 주소만을 사용해 기능을 구현했습니다.
이 과제는 사용자에게서 다른 컨트랙트 (물론 동일한 내용이어야겠지만)의 주소를 입력 받아 이를 이용해 동일한 기능을 수행하게 하는 것입니다.
결과물은
https://atomrigs.github.io/simplestorage2.html
로 작성해서 올려 주세요.
(3) 노가다 숙제
시간이 너무 많아서 뭔가를 더하고 싶은 분을 위한 숙제입니다.
컨트랙트 히스토리 라는 버튼을 만들도 클릭했을 때 주어진 컨트랙트의 최근 10개의 트랜잭션을 화면에 리스트로 보여주어야 합니다.
보여줄 정보 내용은 알아서 결정하세요. 최소한 txid 는 포함되어야겠지요.
결과물은
https://atomrigs.github.io/simplestorage3.html
로 올려주세요.
'Technology > 블록 체인' 카테고리의 다른 글
나도 dApp 개발해보자 강좌 - 4 (0) | 2018.04.05 |
---|---|
나도 dApp 개발해보자 강좌 - 3 (0) | 2018.04.03 |
나도 dApp 개발해보자 강좌 - 2 (0) | 2018.04.03 |
나도 dApp 개발해보자 강좌 - 1 (0) | 2018.04.03 |
이더리움 전용 브라우저 - MIST (0) | 2018.04.03 |