Decentralization

블록체인 기반 투표 시스템 구현

이영훈닷컴 2025. 3. 17. 03:21
728x90

1. 개요

오늘은 Solidity 스마트 컨트랙트와 Web3.js를 활용하여 블록체인 기반 투표 시스템을 구축하는 과정을 학습했다. Solidity로 작성된 스마트 컨트랙트와 이를 프론트엔드에서 다루는 JavaScript 코드를 분석하고 이해하는 것이 목표였다.


2. 스마트 컨트랙트 (Migrations.sol)

먼저, Truffle을 사용하여 마이그레이션을 관리하는 스마트 컨트랙트를 작성했다.

pragma solidity ^0.5.0; // Solidity 컴파일러 버전 지정

contract Migrations { // 마이그레이션을 관리하는 스마트 컨트랙트 선언
  address public owner; // 컨트랙트 소유자의 주소 저장
  uint public last_completed_migration; // 마지막으로 완료된 마이그레이션의 번호 저장

  // 제한된 접근을 위한 modifier 정의 (owner만 실행 가능)
  modifier restricted() {
    if (msg.sender == owner) _; // 호출자가 owner일 경우 함수 실행
  }

  // 컨트랙트 생성자, 배포 시 실행됨
  constructor () public {
    owner = msg.sender; // 배포한 사람(계정)을 owner로 설정
  }

  // 마이그레이션 완료 상태를 설정하는 함수
  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed; // 마이그레이션 번호 업데이트
  }

  // 새로운 마이그레이션 컨트랙트로 업그레이드하는 함수
  function upgrade(address new_address) public restricted {
    Migrations upgraded = Migrations(new_address); // 새로운 컨트랙트 인스턴스 생성
    upgraded.setCompleted(last_completed_migration); // 이전 마이그레이션 상태를 새 컨트랙트로 전달
  }
}

학습한 내용

modifier restricted()를 사용하여 owner만 특정 기능을 수행할 수 있도록 제한했다.
upgrade(address new_address)를 통해 새로운 마이그레이션을 적용할 수 있다.
setCompleted(uint completed)를 사용하여 마이그레이션 완료 상태를 업데이트한다.


3. 프론트엔드 (app.js)

Web3.js와 Truffle-contract를 활용하여 스마트 컨트랙트와 상호작용하는 JavaScript 코드를 작성했다.

// CSS 파일을 불러와 스타일 적용
import "../stylesheets/app.css";

// Web3 라이브러리 불러오기 (Ethereum 블록체인과 상호작용하기 위해 필요)
import { default as Web3} from 'web3';

// Truffle-contract 라이브러리 불러오기 (스마트 컨트랙트와 상호작용을 쉽게 하기 위해 사용)
import { default as contract } from 'truffle-contract';

// 컴파일된 스마트 컨트랙트 정보를 JSON 파일에서 가져오기
import voting_artifacts from '../../build/contracts/Voting.json';

// Voting 스마트 컨트랙트 객체 생성
var Voting = contract(voting_artifacts);

// 후보자 목록과 해당 후보자를 표시할 HTML 요소의 ID 매핑
let candidates = {
  "Rama": "candidate-1",
  "Nick": "candidate-2",
  "Jose": "candidate-3"
};

let account; // 사용자의 이더리움 계정을 저장할 변수

// 사용자가 특정 후보자에게 투표하는 함수
window.voteForCandidate = function(candidate) {
  let candidateName = $("#candidate").val(); // 사용자가 입력한 후보자 이름 가져오기
  try {
    // 사용자에게 메시지를 표시하여 투표가 진행 중임을 알림
    $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.");
    $("#candidate").val(""); // 입력 필드 초기화

    // 배포된 스마트 컨트랙트 인스턴스를 가져와서 실행
    Voting.deployed().then(function(contractInstance) {
      // 스마트 컨트랙트의 voteForCandidate 함수 호출 (가스를 지정하고 사용자 계정에서 실행)
      contractInstance.voteForCandidate(candidateName, {gas: 140000, from: account}).then(function() {
        let div_id = candidates[candidateName]; // 후보자의 ID를 가져옴
        // 블록체인에서 해당 후보자의 총 투표 수 가져오기
        return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
          $("#" + div_id).html(v.toString()); // UI 업데이트
          $("#msg").html(""); // 메시지 초기화
        });
      });
    });
  } catch (err) {
    console.log(err); // 오류 발생 시 콘솔에 출력
  }
};

// 페이지가 로드될 때 실행되는 함수
$(document).ready(function () {
    if (typeof web3 !== 'undefined') { // 사용자의 브라우저에 Web3 인스턴스가 있는지 확인
        if (window.ethereum) { // 최신 Metamask 브라우저 확장 프로그램을 지원하는 경우
            web3 = new Web3(window.ethereum);
            try {
                // 사용자에게 계정 접근 권한 요청
                window.ethereum.enable();
            } catch (error) {
                console.error("User denied account access"); // 사용자가 권한 요청을 거부한 경우
            }
        } else if (window.web3) { // 이전 버전의 Metamask 지원
            web3 = new Web3(window.web3.currentProvider);
        } else { // 사용자가 Metamask를 설치하지 않은 경우, 로컬 Ganache 노드에 연결
            web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
        }
    }

    //////////////////////////////////////////////////////////////////

    // 사용자의 이더리움 계정을 가져옴
    web3.eth.getAccounts(function (err, accs) {
      if (err != null) {
        alert('There was an error fetching your accounts.'); // 계정 가져오기 실패 시 알림
        return;
      }
      if (accs.length === 0) { // 계정이 없는 경우
        alert("Couldn't get any accounts! Make sure your Ethereum client is configured correctly.");
        return;
      }
      account = accs[0]; // 첫 번째 계정을 기본 계정으로 설정
    });

    // 스마트 컨트랙트를 Web3의 현재 공급자와 연결
    Voting.setProvider(web3.currentProvider);

    // 후보자들의 초기 투표 수를 블록체인에서 가져와 화면에 표시
    let candidateNames = Object.keys(candidates); // 후보자 이름 목록 가져오기
    for (var i = 0; i < candidateNames.length; i++) {
        let name = candidateNames[i];
        Voting.deployed().then(function(contractInstance) {
            contractInstance.totalVotesFor.call(name).then(function(v) {
                $("#" + candidates[name]).html(v.toString()); // 투표 수를 UI에 업데이트
            });
        });
    }
});

학습한 내용

Web3.js를 활용하여 이더리움 네트워크와 연결하는 방법을 익혔다.
window.ethereum.enable();을 사용하여 Metamask와 상호작용하는 방법을 학습했다.
voteForCandidate() 함수에서 투표 기능을 구현하고 UI를 업데이트하는 방법을 배웠다.


4. 결론

Solidity 스마트 컨트랙트를 작성하고 배포하는 방법을 익혔다.
Web3.js를 활용하여 프론트엔드에서 스마트 컨트랙트와 상호작용하는 방법을 이해했다.
Metamask와 같은 지갑을 이용해 블록체인과 연동하는 방법을 배웠다.

다음 학습 목표

🔹 투표 시스템의 보안성을 강화하는 방법 (예: 중복 투표 방지)
🔹 이벤트를 활용하여 실시간으로 UI 업데이트 구현
🔹 프론트엔드를 React로 개선하여 더 나은 UX 제공

오늘 배운 내용들을 바탕으로 블록체인 기반 완전한 탈중앙화 투표 시스템을 구축하는 것이 목표! 🚀

728x90