블록체인 Block-Chain - 이더리움 DApp - 투표앱 만들기

dev_swan·2022년 7월 18일
0

블록체인

목록 보기
25/36
post-thumbnail

Solidity Code 작성

후보자 등록

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

contract Voting {
    // 후보자 담을 배열 선언
    string[] public candidateList;
    // 후보자 투표기능을 할 mapping 추가
    mapping(string => uint8) public votesReceived;
    /*
        예시

        {
            "후보자" : 득표수
        }
    */

    // 컨트랙트를 배포될 때 후보자 담을 배열에 후보자를 추가하도록 합니다.
    constructor(string[] memory candiatenames){
        candidateList = candiatenames;
    }
}
  • Voting이란 Contract를 생성한 뒤 후보자를 담을 배열인 candidateList와 각 후보자의 득표수를 담당할 votesReceived라는 상태변수를 만들어주었습니다.
  • 해당 Contract가 처음 배포 될 때 인자값으로 string으로 후보자 배열을 받아 후보자를 담을 배열인 candidateList에 할당하도록 하였습니다.

후보자에 대한 투표 기능 추가

// 후보자에 대한 투표기능 추가
    function voteForcandidate(string memory candidate) public {
        // 후보군에 투표하고자 하는 후보자가 있을경우에만 투표실행
        require(validCandidate(candidate),"There are no matching candidates.");
        votesReceived[candidate]++;
    }

// 후보자 검증 기능 추가
    function validCandidate(string memory _candidate)private view returns(bool){
        // 1. 후보자 리스트 가져오기 candidateList
        // 2. 입력한 후보자와 candidateList안에 후보자가 일치하는것이 있는가 확인
        for(uint8 i=0; i < candidateList.length; i++){
            // string 비교가 안됨;;
            // keccak256 가지고 16진수 내용으로 변경후 비교 합니다.
            if(keccak256(abi.encodePacked(candidateList[i])) == keccak256(abi.encodePacked(_candidate))){
                return true;
            }
        }

        return false;
    }
  • 작성한 Contract에 voteForcandidate 함수를 추가하여 인자값으로 후보자의 명(candidate)을 받습니다.
  • 인자값으로 받은 candidate가 처음 Contract를 배포할 때 생성한 후보자 리스트(candidateList)에 포함되어 있는지 검증하는 validCandidate 함수를 추가하여 인자값으로 해당 후보자 명을 받은뒤 후보자 리스트를 반복문을 사용하여 인자값으로 받은 후보자명과 후보자 리스트안에 일치하는 후보자가 있는지 검증한뒤 있다면 true, 없다면 falsereturn 해주었습니다.
  • require() 메서드 안에 후보자를 검증하는 validCandidate 함수를 넣어 후보자가 없다면 falsereturn 받아 즉시 함수가 종료되도록 해주었고 후보자 리스트에 후보자가 있다면 해당 후보자명이란 key값에 value++하여 1이 증가하도록 처리하였습니다.

후보자의 총 득표수 확인 기능 추가

// 후보자의 총 득표수 확인기능 추가
    function totalVotesFor(string memory _candidate) public view returns(uint8){
        // 후보자의 득표수를 가져올 때 후보군에 후보자가 있을경우에만 투표실행
        require(validCandidate(_candidate),"There are no matching candidates.");
        // 배열안에 해당 후보자의 득표수를 return
        return votesReceived[_candidate];
    }
  • 마지막으로 후보자의 총 득표수를 확인하기 위해 후보자의 명(_candidate)을 인자값으로 받은 뒤 해당 후보자가 후보자리스트에 있는지 검증을 한번하고 해당 후보자명이란 key값의 valuereturn하여 해당 후보자가 몇표를 획득했는지를 처리했습니다.

Front MetaMask 연동

  • 그동안 truffleganache는 세팅을 많이 했으니 생략하도록 하겠습니다.
  • truffle을 사용하여 작성한 Contract를 배포한 뒤 작업하였습니다.

Custom Hook 작성하기

  • npm install web3
import { useEffect, useState } from 'react';
import Web3 from 'web3/dist/web3.min';

const useWeb3 = () => {
	const [account, setAccount] = useState('');
	const [web3, setWeb3] = useState(null);

	useEffect(() => {
		(async () => {
			if (!window.ethereum) return;

			const address = await window.ethereum.request({
				method: 'eth_requestAccounts',
			});

			setAccount(address);

			const web3 = new Web3(window.ethereum);

			setWeb3(web3);
		})();
	}, []);

	return [web3, account];
};

export default useWeb3;
  • 지난번 Counter dApp을 만들때와 마찬가지로 reactuseEffect를 활용하여 MetaMask에서 제공한 window.etherum이 있다면 request() 메서드를 사용하여 연동된 지갑 주소를 가져오고 setAccountaccount의 상태를 변경해주었습니다.
  • 마지막으로 web3MetaMask를 연결하는 새로운 Web3 인스턴스를 생성하고 마찬가지로 setWeb3를 사용하여 web3의 상태를 변경해주었습니다.

투표 컴포넌트 생성

const Voting = () => {
	return <div>Hello Voting</div>
}

export default Voting;
  • 간단하게 투표 컴포넌트를 생성해주었습니다.

App 컴포넌트 수정

import useWeb3 from './hooks/useWeb3';
import Voting from './components/Voting';

function App() {
	const [web3, account] = useWeb3();

	if (!account) return <>메타마스크 연결이 필요합니다.</>;

	return <Voting web3={web3} account={account} />;
}

export default App;
  • 미리 만들어둔 Custom Hook useWeb3를 사용하여 MetaMask와 연결된 web3accountVoting 컴포넌트에 props로 전달해줍니다.

투표 컴포넌트 수정

import { useEffect, useState } from 'react';
import VotingContract from '../contracts/Voting';

const Voting = ({ web3, account }) => {
	const [deployed, setDeployed] = useState();
	const [candiList, setCandiList] = useState();
	const [votesList, setVotesList] = useState();

	useEffect(() => {
		(async () => {
			if (deployed) return;

			const networkId = await web3.eth.net.getId();

			const ContractAddress = VotingContract.networks[networkId].address;

			const deploy = await new web3.eth.Contract(VotingContract.abi, ContractAddress);

			const candiData = [deploy.methods.candidateList(0).call(), deploy.methods.candidateList(1).call(), deploy.methods.candidateList(2).call(), deploy.methods.candidateList(3).call()];
			const candiList = await Promise.all(candiData);

			const votesData = [deploy.methods.totalVotesFor(candiList[0]).call(), deploy.methods.totalVotesFor(candiList[1]).call(), deploy.methods.totalVotesFor(candiList[2]).call(), deploy.methods.totalVotesFor(candiList[3]).call()];
			const votesCount = await Promise.all(votesData);

			setCandiList(candiList);
			setVotesList(votesCount);
			setDeployed(deploy);
		})();
	});

	const vote = async (_index) => {
		await deployed.methods.voteForcandidate(candiList[_index]).send({ from: account[0] });

		const votesData = [deployed.methods.totalVotesFor(candiList[0]).call(), deployed.methods.totalVotesFor(candiList[1]).call(), deployed.methods.totalVotesFor(candiList[2]).call(), deployed.methods.totalVotesFor(candiList[3]).call()];
		const votesCount = await Promise.all(votesData);

		setVotesList(votesCount);
	};

	const showList = () => {
		return candiList.map((v, k) => {
			return (
				<div key={k}>
					<ul>
						<li>
							{`${v} : ${votesList[k]} `}
							<button
								onClick={() => {
									vote(k);
								}}
							>
								투표
							</button>
						</li>
					</ul>
				</div>
			);
		});
	};

	return <>{candiList ? showList() : null}</>;
};

export default Voting;
  • 먼저 배포된 Voting Contractjson 파일을 가져옵니다.
  • useState를 사용하여 작성한 Contract를 실행할 deployed, 후보자 리스트를 담을 candiList, 후보자의 득표수를 담을 votesList 이렇게 세가지 상태를 생성해주었습니다.
  • useEffct를 사용하여 props로 전달받은 인자값 web3를 사용하여 networkId를 가져옵니다.
  • 가져온 networkId를 가지고 배포한 Voting.json 파일의 가져온 networkId를 사용하여 연결된 networkcontract address을 가져옵니다.
  • contract address값을 가지고 배포한 Contract를 실행할 수 있도록 Contract 메서드의 인자값에 Voting.json파일에 있는 abiCA값을 넣은 실행값을 deploy에 할당합니다.
  • deploy 안에 있는 배포한 ContractmethodscandidateList 함수를 사용하여 후보자 리스트를 가져올 것입니다. ethereum network에서 배열안에 있는 리스트들은 한번에 뽑아오면 지금은 괜찮지만 데이터가 많을 경우 ethereum network에 과부하가 올 수 있어 조금 불편하지만 모든 메서드들을 promise 객체로 떨어지게 만들고 Promise.all 메서드를 사용하여 전부 백그라운드에 넣어놓고 완료되는 순서대로 테스트 큐에 쌓이면서 콜스택으로 넘어오게끔 처리하였습니다.
  • 가져온 후보자 리스트 (candiList)를 setCandiList 메서드를 사용하여 candiList의 상태를 변경해주었고 방금 했던것과 같은 방식으로 후보자의 득표수도 가져온 뒤 상태를 변경해주었습니다.
  • 투표 버튼을 눌렀을때 onClick 메서드를 활용하여 vote 함수를 실행시키는데 인자값으로 k값을 주어 index를 가져오도록 하였습니다. vote 함수는 인자값으로 받은 index를 활용하여 후보자 리스트에서 index값이 일치하는 후보자명을 가져오고 후보자명을 인자값으로 넣어 Contract에서 작성한 voteForcandidate 함수를 실행시켜 해당 후보자의 value값을 1증가 시켜 득표를 처리했습니다.
  • 마지막으로 투표를 했다면 다시 후보자들의 총 득표수를 가져와 상태를 변경해주었습니다.

테스트

투표전

투표후

0개의 댓글