이더리움 탈중앙화 투표 어플리케이션 개발하기

in #kr7 years ago

스팀 가입한지는 꽤 되었는데, 이제야 첫 글 포스팅 하네요 ㅎㅎ. 앞으로 블록체인과 관련한 개발 및 기술 관련한 포스팅 위주로 꾸리려고 합니다. 잘 부탁드려요.

이 가이드는 다음의 내용(https://medium.freecodecamp.org/developing-an-ethereum-decentralized-voting-application-a99de24992d9)을 번역한 것입니다. 컴팩트하게 이더리움 디앱을 개발하는 과정을 설명하였기에 좋은 내용이라 판단되어 공유하였습니다.



전체 암호화폐 시장이 7000억 달러를 넘긴 후, 지난 몇 달동안 폭풍적인 성장을 기록했다. 그러나 이것은 시작일 뿐이다. 블록체인 시스템은 지속적으로 성장해오고 있고, 이 새로운 기술과 세계에 빠져드는 좋은 방법은 디앱(dApp)이라 불리는 탈중앙화 앱을 살펴보는 것이다.

크립토키티는 이더리움 블록체인계에서 유명한 디앱의 예제이다. 특히, 고양이를 번식하고, 수집하는 컨셉을 블록체인과 잘 섞었다는 점이 주효하다.

겉으로는 복잡해보여도, 특정 프레임워크와 툴은 블록체인과 스마트 컨트랙트의 인터랙션을 추상화하도록 개발되었다. 이 블로그 포스트를 통해 이더리움을 이용한 탈중앙화 투표 어플리케이션을 만드는 방법에 대해 서술할 것이다. 이더리움에 대해 간략하게 다룰 것인데 완벽히 이해하기 위해서는 어느정도 내용을 이해해야 할것이다. 그리고, 자바스크립트를 잘 안다고 가정한다.

왜 탈중앙화 투표 앱을 만드는가?

본질적으로 블록체인 기술을 이용한 훌륭한 탈중앙화 앱은 믿을만한 서드파티 없이 현재와 같은 액션(돈을 보내는 것 같은)을 수행하길 기대한다. 훌륭한 디앱은 구체적인 실생활의 유즈 케이스를 가지고, 블록체인의 고유한 특성을 활용한다.

In essence, the blockchain is a shared, programmable, cryptographically secure and therefore trusted ledger which no single user controls and which can be inspected by anyone.- Klaus Schwab

투표 앱이 소비자에게 훌륭한 앱이 아닐지 몰라도 블록체인이 풀려는 주요한 문제 때문에 이 가이드를 위해 선택하였다. 그것은 투명성, 보안성, 접근성, 공정성으로서, 현재 민주주의 선거의 주요 문제점을 지적한다.

블록체인에서 분산된 트랜잭션(투표)이 영원히 저장되기 때문에, 모든 투표에 의의를 제기할 수 없고 정확히 언제 어디서 시행했는지 투표자의 신원을 공개하지 않고도 확인할 수 있다. 게다가 과거 투표는 변경이 불가능하다. 모든 트랜잭션이 네트워크의 개별 싱글 노드에 의해 증명되었기 때문이다. 레코드를 조작하기 위해서는 내부 및 외부 공격자가 51%의 노드의 점유율을 가지고 있어야 한다.

만약, 공격자가 실제 ID를 가지고 잘못된 투표를 수행해도 결국에는 투표 시스템 안에서 그 투표가 옳은지를 판단하게 된다.

이더리움의 코어 컴포넌트

이 나머지 가이드를 위해 블록체인과 이더리움에 대하여 어느정도 이해를 하고 있기를 바란다. 여기서 몇가지 코어 컴포넌트에 대해 개괄을 하고자 한다.

1. 스마트 컨트랙트

스마트 컨트랙트는 백앤드 로직과 저장소로 활용되며, 스마트 컨트랙트 언어인 솔리디티(Solidity)로 쓰여진다. 코드와 데이터 컬렉션은 이더리움 블록체인의 특정 주소에 위치하게 된다. 이는 객체 지향 프로그래밍의 클래스와 비슷한 개념이며, 이는 함수와 상태 변수를 포함한다. 블록체인과 함께 스마트 컨트랙트는 모든 탈중앙화 어플리케이션의 기반이며, 불변성과 분산화의 특징이 있다. 그 의미는 만약 이미 이더리움 네트워크에 존재하는 경우 그것들을 업데이트 하기 위해서 무척 힘들다는 것이다. 여기서 ( https://consensys.github.io/smart-contract-best-practices/software_engineering/ ) 이를 위한 방법을 볼 수 있다.

2. 이더리움 가상머신(EVM)

EVM은 내부 상태와 전체 이더리움 네트워크의 계산을 다룬다. EVM을 거대한 탈중앙화 컴퓨터로서 코드를 실행하고 데이터를 변조하고, 서로 상호작용하는 것을 가능하게 하는 “주소들”이 포함되었다고 할 수 있다.

3. Web3.js

Web3.js 는 자바스크립트 API로서, 블록체인과 상호작용할 수 있도록, 트랙잰션을 만들고 스마트 컨트랙트를 호출하는 것을 지원한다. 이 API는 이더리움 클라이언트 와 커뮤니케이션하는 것을 추상화하고, 개발자가 어플리케이션의 컨텐츠에 보다 초점을 맞추게 한다. 이를 위해, web3 인스턴스를 브라우저에 임베드 시켜야 한다.

우리가 사용할 다른 도구들

1. 트러플(Truffle)

트러플은 이더리움을 위한 유명한 개발 테스팅 프레임워크로서, 블록체인 개발과 컴파일, 블록체인에 컨트랙트를 배포하는 스크립트 마이그레이션, 컨트렉트 테스팅 등을 지원한다. 이는 보다 개발을 쉽게 만든다!

2. 트러플 컨트랙트(Truffle Contracts)

트러플 컨트랙트는 Web3 자바스크립트 API 상위를 추상화하여 스마트 컨트랙트에 쉽게 연결하고 상호작용할 수 있도록 만든다.

3. 메타마스크(Metamask)

메타마스크는 이더리움을 브라우저로 가지고 오는 도구다. 이는 브라우저 익스텐션으로서, 이더리움 주소와 보안적으로 web3 인스턴스와 연결을 하도록 지원한다. 우리는 이 튜토리얼에서 이 메타마스크를 사용하지는 않을 것이다. 그러나 상용화하는 단계에서 사람들이 디앱과 상호작용하는 창구로서 작용할 것이다. 대신에 우리는 개발 중에 자체 web3 인스턴스를 주입할 것이다. 더 궁금한 분은 여기(http://truffleframework.com/docs/advanced/truffle-with-metamask )를 참고하길 바란다.

이제 시작해 보자!

단순함을 위해 내가 이미 언급한 풀 보팅 시스템을 만들지는 않을 것이다. 쉬운 설명을 위해 이는 유저가 그들의 ID와 후보자에 대한 투표를 입력할 수 있는 원 페이지 어플리케이션이 될 것이다. 또한, 각 후보자에 투표 수를 표시하고 계산하는 버튼이 있을 것이다.

이 방식으로 어플리케이션 내부에 스마트 컨트랙트를 생성하고 상호작용하는 프로세스에 초점을 맞추고자 한다. 이 어플리케이션의 전체 풀 소스 코드는 여기(https://github.com/tko22/eth-voting-dapp )에 있다. 그리고, node.js와 npm이 설치가 이미 되어 있어야 한다.먼저, 트러플을 글로벌하게 설치해보자.

$ npm install -g truffle

트러플 명령을 사용하기 위해, 기존 프로젝트에 다음과 같이 실행해야 한다.

$ git clone https://github.com/tko22/truffle-webpack-boilerplate
$ cd truffle-webpack-boilerplate
$ npm install

이 리포지토리는 트러플 박스의 뼈대로서 이는 한 명령(truffle unbox [box name])으로 얻을 수 있는 보일러플레이트 혹은 예제 어플리케이션이다. 하지만, 웹팩과 트러플 박스는 최신 버전으로 업데이트 되지 않고, 예제 어플리케이션을 포함한다. 그래서 나는 이 리포지토리(https://github.com/tko22/truffle-webpack-boilerplate )를 생성했다.

  • 디렉터리 구조

디렉터리 구조는 다음을 포함해야 한다.

— contracts/ : 모든 컨트랙트를 가지고 있는 폴더. Migrations.sol 파일을 삭제하지 마세요!

— migrations/ : Migrations 파일을 가지고 있는 폴더로서, 블록체인으로 스마트 컨트랙트를 배포하는 역할을 담당.

— src/ : 어플리케이션의 HTML/CSS, Javascript 파일

— truffle.js : 트러플 구성 파일

— build/ : 컨트랙트가 컴파일되기 전까지는 보이지 않을 것이다. 이 폴더는 빌드 아티팩츠(artifacts)를 가지는 것으로서, 이 파일을 수정하지 않는다. 빌드 아티팩츠는 컨트랙트의 함수와 아키텍처를 표현한다. 그리고 트러플 컨트랙트와 블록체인 상의 스마트 컨트랙트와 상호작용하는 web3 정보가 주어진다.

1. 스마트 컨트랙트 작성하기

설치와 소개가 충분히 되었으니 코드로 직접 들어가보자! 첫번째로 솔리디티로 스마트 컨트랙트를 작성할 것이다. 무서워보일지 모르지만, 그렇지는 않다.

어플리케이션에서는 스마트 컨트랙트가 가능한 단순하길 원할 것이다. 당신이 만들어내는 모든 계산과 트랜잭션에 비용이 들어간다는 것을 기억하라. 그리고 스마트 컨트랙트 블록체인 상에 영원히 기록된다. 그래서, 정말 이것들이 완벽하게 돌아가길 원할 것이다.

우리의 컨트랙트는 다음을 포함한다.

  1. 상태 변수 — 이는 블록체인 상에 영원히 저장되는 값을 가진다. 우리는 이를 투표자와 후보자의 수와 리스트를 위해 사용할 것이다.
  2. 함수 — 함수는 스마트 컨트랙트에서 실행 가능하다. 이것은 블록체인과 상호작용하기 위해 호출하는 것으로서, 여러가지의 공개성, 내부성, 외부성을 가진다. 해당 변수의 상태나 값을 바꿀 때마다 트랜잭션은 이더(Ether)를 소비한다는 것을 기억하라. ‘calls’를 통해 이더의 소비 없이 블록체인을 호출할 수 있는데 이는 변화를 무시하기 때문이다. 이는 섹션3에서 transactions와 calls에 대해 다뤄보도록 한다.
  3. 이벤트 — 이벤트가 호출되면, 이벤트로 전달되는 값이 트랜잭션 로그에 쓰여질 것이다. 이는 자바스크립트 콜백 함수 또는 리졸브(resolved)된 프라미스에서 트랜잭션 후에 돌려받고자 하는 특정 값을 보기위해 사용된다. 이는 트랜잭션이 이루어질 때마다 트랜잭션 로그가 반환될 것이기 때문이다. 우리는 새로 생성된 후보자의 ID에 로그를 남기고 이를 표시하기 위해 사용할 것이다.
  4. 스트럭트 타입 — 이는 C언어의 스트럭트(struct)와 매우 비슷하다. 스트럭트는 다양한 변수를 가지게하고, 여러개의 어트리뷰트를 포함할 수 있다. Candidates 는 name과 party라는 이름을 가지는 것을 포함한다. 그러나 이에 다른 어트리뷰트를 추가할 수 있다.
  5. 매핑 — 이는 해시 맵이나 딕셔너리와 같다고 보면 된다. 이는 키 값 페어로서, 우리는 이 두 매핑을 사용할 것이다.

여기에 소개 되지 않은 여러개의 타입이 있다. 하지만, 그것들 중 몇개는 약간 복잡하다. 이 다섯가지는 스마트 컨트랙트가 일반적으로 사용하는 구조를 모두 포함한다. 이 타입에 대해 보다 구체적인 사항은 다음을 참고하자. ( http://solidity.readthedocs.io/en/develop/structure-of-a-contract.html# )

참조를 위해 여기에 스마트 컨트랙트 코드가 있다. 이 파일은 Voting.sol 로 이름 붙여진 파일인 것을 확인하자. 그러나 필자는 스타일링을 가지는 Github gist를 가지고 .js 익스텐션으로 전달하길 원한다. 이 가이드의 남은 부분과 같이 이 코드에 주석이 포함되었고 이는 무엇을 의미하는지 알 수 있다. 그리고, 특정한 주의사항 및 로직을 가리키는 동안 큰 그림에 대해 설명할 것이다.

pragma solidity ^0.4.18;
// written for Solidity version 0.4.18 and above that doesnt break functionality

contract Voting {
   // an event that is called whenever a Candidate is added so the frontend could
   // appropriately display the candidate with the right element id (it is used
   // to vote for the candidate, since it is one of arguments for the function "vote")
   event AddedCandidate(uint candidateID);

   // describes a Voter, which has an id and the ID of the candidate they voted for
   struct Voter {
       bytes32 uid; // bytes32 type are basically strings
       uint candidateIDVote;
   }
   // describes a Candidate
   struct Candidate {
       bytes32 name;
       bytes32 party;
       // "bool doesExist" is to check if this Struct exists
       // This is so we can keep track of the candidates
       bool doesExist;
   }

   // These state variables are used keep track of the number of Candidates/Voters
   // and used to as a way to index them     
   uint numCandidates; // declares a state variable - number Of Candidates
   uint numVoters;

   
   // Think of these as a hash table, with the key as a uint and value of
   // the struct Candidate/Voter. These mappings will be used in the majority
   // of our transactions/calls
   // These mappings will hold all the candidates and Voters respectively
   mapping (uint => Candidate) candidates;
   mapping (uint => Voter) voters;
   
   /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
    *  These functions perform transactions, editing the mappings *
    * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

   function addCandidate(bytes32 name, bytes32 party) public {
       // candidateID is the return variable
       uint candidateID = numCandidates++;
       // Create new Candidate Struct with name and saves it to storage.
       candidates[candidateID] = Candidate(name,party,true);
       AddedCandidate(candidateID);
   }

   function vote(bytes32 uid, uint candidateID) public {
       // checks if the struct exists for that candidate
       if (candidates[candidateID].doesExist == true) {
           uint voterID = numVoters++; //voterID is the return variable
           voters[voterID] = Voter(uid,candidateID);
       }
   }

   /* * * * * * * * * * * * * * * * * * * * * * * * * *
    *  Getter Functions, marked by the key word "view" *
    * * * * * * * * * * * * * * * * * * * * * * * * * */
   

   // finds the total amount of votes for a specific candidate by looping
   // through voters
   function totalVotes(uint candidateID) view public returns (uint) {
       uint numOfVotes = 0; // we will return this
       for (uint i = 0; i < numVoters; i++) {
           // if the voter votes for this specific candidate, we increment the number
           if (voters[i].candidateIDVote == candidateID) {
               numOfVotes++;
           }
       }
       return numOfVotes;
   }

   function getNumOfCandidates() public view returns(uint) {
       return numCandidates;
   }

   function getNumOfVoters() public view returns(uint) {
       return numVoters;
   }
   // returns candidate information, including its ID, name, and party
   function getCandidate(uint candidateID) public view returns (uint,bytes32, bytes32) {
       return (candidateID,candidates[candidateID].name,candidates[candidateID].party);
   }
}

https://gist.githubusercontent.com/tko22/53c0d52c0c4dcc90226c6783e43b61d4/raw/610b33568386232c4f4350dd172175301b010a21/Voting.sol.js

기본적으로, 투표자와 후보자를 지칭하는 두개의 스트럭트를 가진다. 스트럭트에는 이메일과 주소와 같은 다양한 프로퍼티를 할당할 수 있다.

투표자와 후보자를 살펴보기 위해, 이것들을 인덱스된 정수의 분리된 매핑에 넣어볼 것이다. 투표자나 후보자의 인덱스 및 키는(ID라 부르도록 하자) 함수가 이를 접근하기 위한 유일한 경로이다.

투표자와 후보자의 수를 살펴보도록 하자. 그리고 이는 인덱스하도록 도와줄 것이다. 추가적으로 라인 8줄의 이벤트를 잊지 말아라. 이는 추가될 때, 후보자의 ID가 기록될 것이다. 이 이벤트는 우리의 인터페이스에 의해 사용될 것이고, 이는 후보자를 투표하기 위해 후보자 ID를 살펴볼 필요가 있을 때 살펴보겠다.

  1. 앞서 말했던, 스마트 컨트랙트가 무척 단순하다는 것과 반대로 실제 어플리케이션이 하는 것에 비해 보다 복잡하게 만들었다는 것을 안다. 하지만, 여러분이 이에 편집하고 새로운 특징을 어플리케이션에 추가하는 것이 보다 쉬워지도록 만들었다. 만약 여러분이 보다 단순한 투표 어플리케이션을 만들고자 한다면, 스마트 컨트랙트 코드가 15줄 안으로 작동할 것이다.
  2. 스테이트 변수인 numCandidates 와 numVoters는 public으로 선언되지 않았다는 것을 확인해보자. 기본적으로, 이 변수들은 internal 의 공개 범위를 가지고 있다. 이것이 의미하는바는 이 변수들이 현재 컨트랙트나 파생된 컨트랙트에 의해 바로 접근 할 수 있다는 것이다.
  3. 우리는 string 타입 대신에 32bytes 타입을 사용할 것이다. EVM은 32bytes의 단어 크기를 가지고 있다. 그래서, 32 바이트의 청크 데이터를 다루는데 최적화되어 있다. (솔리디티와 같은 컴파일러는 데이터가 32 바이트의 청크 데이터가 아닐 경우, 더욱 많은 작업과 바이트코드 생성을 해야만 한다. 이는 보다 높은 가스 비용을 초래한다)
  4. 유저가 투표했을 때, 새로운 Voter 스트럭트는 매핑에 생성되고 추가된다. 특정 후보자가 가지고 있는 투표의 수를 계산하기 위해 모든 투표자와 투표의 수를 계산해야 한다. 후보자는 같은 형태로 작동한다. 그래서 이 매핑은 후보자와 투표자의 히스토리를 가질 것이다.

2. web3와 컨트랙트 인스턴스화

스마트 컨트랙트가 완료되면, 이제는 테스트 블록체인을 실행하고 이 컨트랙트를 블록체인으로 배포해야 한다. 우리는 web3.js를 통해 이를 상호작용할 수 있는 방법이 필요할 것이다.

테스트 블록체인을 시작하기 전에 2_deploy_contracts.js 파일을 /contracts 폴더 안에 생성하자. 이 폴더는 마이그레이트할 때, 투표 스마트 컨트랙트가 포함되었다고 알려준다.

var Voting = artifacts.require("Voting")

module.exports = function(deployer) {
 deployer.deploy(Voting)
}

https://gist.github.com/tko22/d21daa165043a637df59b1df4ccdbd49

이더리움 블록체인 개발을 시작하기 위해, 터미널에서 다음 커맨드를 실행한다.

$ truffle develop

솔리디티는 컴파일 언어이기 때문에, 우리는 이것을 바이트코드로 EVM이 시작할 때쯤 컴파일해야만 한다.

$ compile

이제 디렉터리 안에 /build 라는 폴더를 볼 수 있을 것이다. 이 폴더는 빌드 아키택츠를 가지는데, 이는 트러플 작동을 위해 무척 중요한 요소이다. 그러니, 만지지 말기를!

다음으로, 컨트랙트를 마이그레이션해야한다. 마이그레이션이란, 트러플 스크립트로서, 개발 시에 당신의 어플리케이션 컨트랙트의 상태를 변경할 때 도움을 준다. 당신의 컨트랙트가 블록체인 상의 특정 주소로 배포되는 것을 기억하라. 그래서 변경이 일어날 때 마다, 당신의 컨트랙트는 다른 주소에 위치할 것이다. 마이그레이션은 이를 도와 데이터를 이동시키도록 할 것이다.

$ migrate

축하한다! 여러분의 스마트 컨트랙트가 블록체인 상에 영원히 기록되었다. 사실 완벽한 것은 아닌데.. 왜냐하면 truffle develop 코드는 멈출 때마다 리프레쉬 되기 때문이다.

만약, 블록체인에 영속적으로 기록하고자 한다면, Ganache( http://truffleframework.com/ganache/ )를 고려해보길 바란다. 이는 역시, 트러플에 의해 개발되었고 만약 Ganache를 사용하면 truffle develop 코드를 호출할 필요가 없다. 대신에, truffle compile 과 truffle migrate 를 호출할 것이다. 트러플 없이 컨트랙트를 진행하는데 필요한 것을 이해하려면 다음의 블로그 포스트를 확인해보라. ( https://medium.com/@gus_tavo_guim/deploying-a-smart-contract-the-hard-way-8aae778d4f2a )

스마트 컨트랙트를 블록체인에 배포하면, 어플리케이션이 시작할 때마다 브라우저 상에 자바스크립트의 web3.0 인스턴스를 설치해야만 할 것이다. 그래서, 다음의 코드를 js/app.js 파일의 하단에 위치시킨다. 우리가 사용하는 web3.0 버전은 0.20.1 인 것을 확인하라.

// When the page loads, we create a web3 instance and set a provider. We then set up the app
window.addEventListener("load", function() {
 // Is there an injected web3 instance?
 if (typeof web3 !== "undefined") {
   console.warn("Using web3 detected from external source like Metamask")
   // If there is a web3 instance(in Mist/Metamask), then we use its provider to create our web3object
   window.web3 = new Web3(web3.currentProvider)
 } else {
   console.warn("No web3 detected. Falling back to http://localhost:9545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-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:9545"))
 }
 // initializing the App
 window.App.start()
})

https://gist.github.com/tko22/5e6c51282b51a0209eaf12287d19cdc2

이 코드를 이해하지 못한다고 해도, 너무 염려하지 말길 바란다. 단지, 이것은 어플리케이션 시작 시에 실행되고 브라우저에 메타마스크(Metamask)와 같은 web3 인스턴스가 있는지 확인할 것이다. 만약, 그렇지 않으면 localhost:9545 에 통신하는 인스턴스를 생성할 것이다. 이것은 트러플 개발 블록체인이다.

만약, Ganache를 사용하면, 포트를 7545로 변경해야만 한다. 인스턴스가 생성되면 start 함수를 호출할 것이다. (이는 다음 섹션에서 알아보자)

3. 기능 추가하기

우리에게 필요한 마지막 일은 어플리케이션을 위한 인터페이스를 작성하는 것이다. 이는 웹 어플리케이션의 기본 요소들을 포함한다. HTML, CSS, Javascript와 같은 것 말이다. (이미 앞서, web3 인스턴스를 생성하기 위해 약간의 자바스크립트를 작성하였다.) 먼저, HTML 파일을 생성해보자.

<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
   <title>Ethereum Voting Dapp</title>

   <!-- Bootstrap -->
   <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
   
 </head>
 <body>
   <div class="container">
     <div class="row">
       <div>
         <h1 class="text-center">Ethereum Voting Dapp</h1>
         <hr/>
         <br/>
       </div>
     </div>
     <div class="row">
       <div class="col-md-4">
         <p>Add ID and click candidate to vote</p>
         <div class="input-group mb-3">
           <input type="text" class="form-control" id="id-input" placeholder="Enter ID">
         </div>
         <div class="candidate-box"></div>
         <button class="btn btn-primary" onclick="App.vote()">Vote</button>
         <div class="msg"></div>
       </div>
       <div class="col-md-6">
           <button class="btn btn-primary" onclick="App.findNumOfVotes()">Count Votes</button>
           <div id="vote-box"></div>
       </div>
     </div>
   </div>

   <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
   <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
   <!-- Include all compiled plugins (below), or include individual files as needed -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>

   <!-- Custom Scripts -->
   <script src="app.js"></script>
 </body>
</html>

https://gist.github.com/tko22/0c7025b8f6effab8149d8dc5dba97ba3

이는 매우 단순한 페이지로서, user ID를 위한 인풋 폼과 투표와 투표수를 계산하는 버튼을 포함한다. 이 버튼이 클릭되면, 이는 투표를 위한 특정 함수를 호출하고, 후보자를 위한 투표 수를 찾을 것이다.

세가지 중요한 div 엘리먼트가 있는데, candidate-box, msg, vote-box 라는 ID를 가지는 것들로서, 각 후보자와 메시지, 투표수를 각각 담고 있는 체크박스를 포함한다. 또한, jQuery, 부트스트랩, app.js를 임포트한다.

이제, 컨트랙트와 상호작용이 필요하고, 투표와 각 후보자의 투표수에 대한 카운팅을 위한 함수를 구현해야할 것이다. jQuery는 DOM을 조작할 것이고, 프라미스(Promises) 를 이용하여 블록체인에 호출 및 트랜잭션을 만들어 볼 것이다. 아래 코드는 app.js에 대한 것이다.

// import CSS. Webpack with deal with it
import "../css/style.css"

// Import libraries we need.
import { default as Web3} from "web3"
import { default as contract } from "truffle-contract"

// get build artifacts from compiled smart contract and create the truffle contract
import votingArtifacts from "../../build/contracts/Voting.json"
var VotingContract = contract(votingArtifacts)

/*
* This holds all the functions for the app
*/
window.App = {
 // called when web3 is set up
 start: function() {
   // setting up contract providers and transaction defaults for ALL contract instances
   VotingContract.setProvider(window.web3.currentProvider)
   VotingContract.defaults({from: window.web3.eth.accounts[0],gas:6721975})

   // creates an VotingContract instance that represents default address managed by VotingContract
   VotingContract.deployed().then(function(instance){

     // calls getNumOfCandidates() function in Smart Contract,
     // this is not a transaction though, since the function is marked with "view" and
     // truffle contract automatically knows this
     instance.getNumOfCandidates().then(function(numOfCandidates){

       // adds candidates to Contract if there aren't any
       if (numOfCandidates == 0){
         // calls addCandidate() function in Smart Contract and adds candidate with name "Candidate1"
         // the return value "result" is just the transaction, which holds the logs,
         // which is an array of trigger events (1 item in this case - "addedCandidate" event)
         // We use this to get the candidateID
         instance.addCandidate("Candidate1","Democratic").then(function(result){
           $("#candidate-box").append(`<div class='form-check'><input class='form-check-input' type='checkbox' value='' id=${result.logs[0].args.candidateID}><label class='form-check-label' for=0>Candidate1</label></div>`)
         })
         instance.addCandidate("Candidate2","Republican").then(function(result){
           $("#candidate-box").append(`<div class='form-check'><input class='form-check-input' type='checkbox' value='' id=${result.logs[0].args.candidateID}><label class='form-check-label' for=1>Candidate1</label></div>`)
         })
         // the global variable will take the value of this variable
         numOfCandidates = 2
       }
       else { // if candidates were already added to the contract we loop through them and display them
         for (var i = 0; i < numOfCandidates; i++ ){
           // gets candidates and displays them
           instance.getCandidate(i).then(function(data){
             $("#candidate-box").append(`<div class="form-check"><input class="form-check-input" type="checkbox" value="" id=${data[0]}><label class="form-check-label" for=${data[0]}>${window.web3.toAscii(data[1])}</label></div>`)
           })
         }
       }
       // sets global variable for number of Candidates
       // displaying and counting the number of Votes depends on this
       window.numOfCandidates = numOfCandidates
     })
   }).catch(function(err){
     console.error("ERROR! " + err.message)
   })
 },

 // Function that is called when user clicks the "vote" button
 vote: function() {
   var uid = $("#id-input").val() //getting user inputted id

   // Application Logic
   if (uid == ""){
     $("#msg").html("<p>Please enter id.</p>")
     return
   }
   // Checks whether a candidate is chosen or not.
   // if it is, we get the Candidate's ID, which we will use
   // when we call the vote function in Smart Contracts
   if ($("#candidate-box :checkbox:checked").length > 0){
     // just takes the first checked box and gets its id
     var candidateID = $("#candidate-box :checkbox:checked")[0].id
   }
   else {
     // print message if user didn't vote for candidate
     $("#msg").html("<p>Please vote for a candidate.</p>")
     return
   }
   // Actually voting for the Candidate using the Contract and displaying "Voted"
   VotingContract.deployed().then(function(instance){
     instance.vote(uid,parseInt(candidateID)).then(function(result){
       $("#msg").html("<p>Voted</p>")
     })
   }).catch(function(err){
     console.error("ERROR! " + err.message)
   })
 },

 // function called when the "Count Votes" button is clicked
 findNumOfVotes: function() {
   VotingContract.deployed().then(function(instance){
     // this is where we will add the candidate vote Info before replacing whatever is in #vote-box
     var box = $("<section></section>")

     // loop through the number of candidates and display their votes
     for (var i = 0; i < window.numOfCandidates; i++){
       // calls two smart contract functions
       var candidatePromise = instance.getCandidate(i)
       var votesPromise = instance.totalVotes(i)

       // resolves Promises by adding them to the variable box
       Promise.all([candidatePromise,votesPromise]).then(function(data){
         box.append(`<p>${window.web3.toAscii(data[0][1])}: ${data[1]}</p>`)
       }).catch(function(err){
         console.error("ERROR! " + err.message)
       })
     }
     $("#vote-box").html(box) // displays the "box" and replaces everything that was in it before
   })
 }
}

// When the page loads, we create a web3 instance and set a provider. We then set up the app
window.addEventListener("load", function() {
 // Is there an injected web3 instance?
 if (typeof web3 !== "undefined") {
   console.warn("Using web3 detected from external source like Metamask")
   // If there is a web3 instance(in Mist/Metamask), then we use its provider to create our web3object
   window.web3 = new Web3(web3.currentProvider)
 } else {
   console.warn("No web3 detected. Falling back to http://localhost:9545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for deployment. More info here: http://truffleframework.com/tutorials/truffle-and-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:9545"))
 }
 // initializing the App
 window.App.start()
})

https://gist.github.com/tko22/8a8892b5e2dd23a7880e41b2bcb455f3

이전 단계에서 web3 인스턴스를 생성하는 코드가 여기에 있는 것을 볼 수 있다. 먼저, 웹팩 이나 web3, 트러플 컨트랙트와 같은 필요한 라이브러리들을 임포트한다. 우리는 트러플 컨트랙트를 사용할 것으로서, 이는 블록체인과 web3 상위에서 상호작용이 가능하도록 만들어졌다.

이를 사용하기 위해, 빌드 아티팩츠를 다룰 것인데, 이는 투표 스마트 컨트랙트를 컴파일할 때 자동적으로 만들어질 것이다. 그리고, 이는 트러플 컨트랙트를 생성하기위해 사용된다. 마지막으로, 글로벌 변수인 window 안에 함수를 작성할 것으로서, 이는 app 을 시작하거나, 후보자를 투표하거나, 투표수를 찾는데 사용된다.

블록체인과 실제로 상호작용하기 위해, deployed 함수를 사용함으로써, 트러플 컨트랙트의 인스턴스를 생성해야만 한다. 이는 차례로 스마트 컨트랙트로부터 함수를 호출하는데 사용하는 반환 값에 대한 인스턴스를 프라미스로 반환한 것이다.

상호작용을 위해 transactions와 calls 라는 함수가 있다. transaction은 쓰기-작동으로서, 전체 네트워크에 브로드캐스트되고 마이너에 의해 프로세스가 진행된다. (물론 이더가 소비된다.). 여러분은 만약, 스테이트 변수가 변화되면 이것이 블록체인의 스테이트를 변경하기 때문에, 트랜잭션을 수행해야 한다.

call은 읽기-작동함수로서, 트랜잭션을 시뮬레이팅하지만 상태에 대한 변화는 무시한다. 그러므로, 이는 이더를 소비하지 않는다. 이는 getter 함수로서, 유용하다. (이전 스마트 컨트랙트에서 작성한 네 개의 getter 함수를 살펴보라.)

트러플 컨트랙트와 트랜잭션이 이뤄지기 위해, instance.functionName(param1, param2) 라는 것을 작성해야하는데, 여기서 instance는 deployed 함수로 반환 받은(예제의 36줄을 확인하라) 인스턴스를 지칭한다. 이 트랜잭션은 반환 값인 트랜잭션 데이터와 함께 프라미스를 반환할 것이다. 그러므로, 만약 스마트 컨트랙트 함수에 값을 반환하는데 같은 함수로 트랜잭션을 수행하는 경우에는 값을 반환하지 않을 것이다.

이러한 이유로, 반환받는 트랜잭션 데이터로 작성을 원하는 것 무엇이든 기록할 수 있는 이벤트를 가지는 것이다. 36–37 줄을 보면, 후보자를 추가하는 트랜잭션이 있다. 프라미스가 리졸브될 때, result에 트랜잭션 데이터를 갖게 된다.

AddedCandidate()라는 이벤트를 기록하기 위한 candidateID를 얻기 위해, logs를 통해 다음과 같이 값을 얻을 수 있다. result.logs[0].args.candidateID

무슨 일이 일어나는지 보기 위해, 크롬 개발자 도구를 열고, result를 출력하고 result의 구조를 살펴보자.

호출을 위해, instance.functionName.call(param1, param2) 를 작성할 것이다. 하지만 함수가 view 키워드를 가지고 있으면, 트러플 컨트랙트는 자동적으로 함수를 호출할 것이고 따라서 .call을 추가할 필요가 없다.

이러한 이유로, getter 함수들이 view 키워드를 가지는 것이다. 트랜잭션 호출과 달리, call 에 의한 프라미스 반환값은 스마트 컨트랙트 함수에 의해 반환된 값 중 어떤 것이든 가질 것이다.

이제 3개의 함수를 간단하게 설명할 것이다. 데이터 스토어의 데이터를 가지고 오거나 변조시키는 어플리케이션을 만들거나 DOM을 조작하는 것에 익숙하지 않을 수 있다. 블록체인을 당신의 데이터베이스라고 생각하여 트러플 컨트랙트를 데이터베이스로부터 데이터를 가지고오는 API라고 생각해보자.

-App.start()

이 함수는 web3 인스턴스가 생성된 후 바로 호출된다. 트러플 컨트랙트를 얻기 위해 web3 인스턴스를 생성하고 기본값을 세팅하는 프로바이더를 만들어야 한다. (기본값은 사용하고 있는 어카운트 정보나 트랜잭션 시 사용되는 가스의 양 등을 설정할수 있다.)

우리는 현재, 개발 모드에 있기 때문에 어떠한 가스나 어카운트를 자유롭게 설정할 수 있다. 프로덕션 상에서는 메타마스크에의해 제공되는 어카운트를 가지고, 사용할 수 있는 가장 저렴한 가스를 책정하는데 주의를 기울일 것이다. 왜냐하면, 이것은 실제 돈이기 때문이다.

모든 것이 완료되었으면, 유저가 투표하는 개별 후보자의 체크박스를 표시할 것이다. 이를 위해, 컨트랙트의 인스턴스를 생성하고 후보자의 정보를 가져와야 한다. 만약 후보자가 아무도 없으면, 이들을 생성해야 한다. 유저가 후보자에게 투표하는 것을 위해, 특정 후보자의 ID를 제공해야 한다. 그러므로, 개별 체크박스 엘리먼트는 id를 가지는 것이다. 이는 후보자의 ID이다. 추가적으로, 후보자의 수를 글로벌 변수인 numOfCandidates로 추가할 것이고, 이는 App.findNumOfVotes()를 통해 호출된다. jQuery는 각 체크박스를 append하고 개별 후보자의 name은 .candidate-box 가 된다.

-App.vote()

이 함수는 특정 후보자에 투표하며, 이는 어떤 체크박스가 체크되었는지와 체크된 id 어트리뷰트를 가지고 판단한다.

첫번째로, 그들의 userID가 담긴 인풋을 유저가 가지고 있는지 체크할 것이고, 이는 그들의 신원증명이 된다. 만약 가지고 있지 않으면, 그들에게 알려주는 메시지를 출력한다.

두번째로, 유저가 후보자에게 투표를 했는지 안했는지를 체크한다. 적어도 체크박스가 하나 이상 클릭되었는지 확인하는 것이다. 아무도 체크박스를 클릭하지 않았으면 그들에게 후보자를 클릭하라고 메시지를 출력할 것이다. 하나라도 체크되었으면, 체크박스의 id를 가져오는데 이는 후보자의 ID와 연동되어 있다. 그리고 이를 후보자에게 투표하는데 사용한다.

트랜잭션이 완료되면, 완료된 프라미스를 리졸브하고, “Voted” 라는 메시지를 띄운다.

-App.findNumOfVotes()

이 마지막 함수는 개별 후보자의 투표 수를 찾고 이를 표시할 것이다. 우리는 이 후보자를 살펴보고 두 개의 스마트 컨트랙트 함수를 호출할 것이다. 이 함수는 getCandidate 와 totalVotes 이다. 이 프라미스를 리졸브하고 특정 후보자를 위한 HTML 엘리먼트를 생성할 것이다.

이제 어플리케이션을 시작하면, http://localhost:8080/ 에서 확인할 수 있다.

$ npm run dev

리소스

이더리움 및 트러플 등에 대한 리소스를 다음과 같이 링크했다.

결론

이더리움 어플리케이션을 개발하는 것은 백엔드 서비스라 불리는 일반 어플리케이션과 많이 비슷하다. 가장 어려운 부분은 견고하고 완전한 스마트 컨트랙트를 작성하는 것이다. 이 가이드가 분산화된 어플리케이션과 이더리움의 핵심 지식을 이해하는데 도움이 되었길 바란다. 그리고 이것들에 대한 개발의 관심을 바로 시작할 수 있도록 도울 것이다.

또한, 어떤 일이 일어났을때의 방책으로서 다음과 같은 팁이 있다.

  • 무언가 이상하게 작동하지 않으면, 스마트 컨트랙트 함수를 두번 세번 확인해보라. 나는 내 함수 중 하나에서 반환되는 잘못된 값으로 버그를 찾는데 두 시간을 헤맸다.
  • 개발용 블록체인에 연결되어 있을 때, URL과 포트가 정확한지 체크하라. 7545는 truffle develop을 위한 것이고, 9545는 genache를 위한 것이라고 기억하라. 이것들은 기본 설정이며, 만약 블록체인에 연결할 수 없으면, 이것들을 바꿔야 한다.
  • 이 가이드에서는 너무 길어서 언급하지 않았지만, 아마도 다른 포스트에서 다룰 예정이다. 다음 링크( http://truffleframework.com/docs/getting_started/testing )로 컨트랙트를 테스팅할 수 있을 것이다. 이는 도움이 많이 된다.
  • 프라미스 와 친숙하지 않다면, 다음 링크( https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise ) 에 들어가 어떻게 작동하고 사용하는지 확인해보라. 트러플 컨트랙트는 프라미스를 사용하고, web3 베타 버전 역시, 프라미스를 서포트할 것이다. 만약 이를 잘못하면, 받아오는 많은 데이터를 어지럽힐 것이다.

분산화되고 안전한 인터넷을 향하여 건배~ Web3.0!

#epitomecl

Sort:  

잘 봤습니다. 오늘 하루 행복하세요

Congratulations @willpark! You have completed some achievement on Steemit and have been rewarded with new badge(s) :

You published your First Post
You got a First Vote

Click on any badge to view your own Board of Honor on SteemitBoard.

To support your work, I also upvoted your post!
For more information about SteemitBoard, click here

If you no longer want to receive notifications, reply to this comment with the word STOP

Upvote this notification to help all Steemit users. Learn why here!